diff --git a/docs/source/markdown/podman-kube-play.1.md.in b/docs/source/markdown/podman-kube-play.1.md.in index e72248b70b..6f4f45ef89 100644 --- a/docs/source/markdown/podman-kube-play.1.md.in +++ b/docs/source/markdown/podman-kube-play.1.md.in @@ -46,6 +46,9 @@ A Kubernetes PersistentVolumeClaim represents a Podman named volume. Only the Pe - volume.podman.io/uid - volume.podman.io/gid - volume.podman.io/mount-options +- volume.podman.io/import-source + +Use `volume.podman.io/import-source` to import the contents of the tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz) specified in the annotation's value into the created Podman volume Kube play is capable of building images on the fly given the correct directory layout and Containerfiles. This option is not available for remote clients, including Mac and Windows (excluding WSL2) machines, yet. Consider the following excerpt from a YAML file: diff --git a/libpod/volume.go b/libpod/volume.go index 2d4ea4280e..a85dbca325 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -280,3 +280,7 @@ func (v *Volume) Unmount() error { defer v.lock.Unlock() return v.unmount(false) } + +func (v *Volume) NeedsMount() bool { + return v.needsMount() +} diff --git a/pkg/api/handlers/libpod/kube.go b/pkg/api/handlers/libpod/kube.go index 2a61d17234..9e9ef52a50 100644 --- a/pkg/api/handlers/libpod/kube.go +++ b/pkg/api/handlers/libpod/kube.go @@ -93,6 +93,7 @@ func KubePlay(w http.ResponseWriter, r *http.Request) { LogOptions: query.LogOptions, StaticIPs: staticIPs, StaticMACs: staticMACs, + IsRemote: true, } if _, found := r.URL.Query()["tlsVerify"]; found { options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify) diff --git a/pkg/domain/entities/play.go b/pkg/domain/entities/play.go index 5bb438d7d8..86bd479170 100644 --- a/pkg/domain/entities/play.go +++ b/pkg/domain/entities/play.go @@ -58,6 +58,8 @@ type PlayKubeOptions struct { ServiceContainer bool // Userns - define the user namespace to use. Userns string + // IsRemote - was the request triggered by running podman-remote + IsRemote bool } // PlayKubePod represents a single pod and associated containers created by play kube diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index d97cda7c16..4080dcfebb 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -18,6 +18,7 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/common/pkg/secrets" "github.com/containers/image/v5/types" + "github.com/containers/podman/v4/cmd/podman/parse" "github.com/containers/podman/v4/libpod" "github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/pkg/domain/entities" @@ -29,6 +30,7 @@ import ( "github.com/containers/podman/v4/pkg/specgenutil" "github.com/containers/podman/v4/pkg/systemd/notifyproxy" "github.com/containers/podman/v4/pkg/util" + "github.com/containers/podman/v4/utils" "github.com/coreos/go-systemd/v22/daemon" "github.com/ghodss/yaml" "github.com/opencontainers/go-digest" @@ -233,6 +235,19 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, body io.Reader, options return nil, fmt.Errorf("unable to read YAML as Kube PersistentVolumeClaim: %w", err) } + for name, val := range options.Annotations { + if pvcYAML.Annotations == nil { + pvcYAML.Annotations = make(map[string]string) + } + pvcYAML.Annotations[name] = val + } + + if options.IsRemote { + if _, ok := pvcYAML.Annotations[util.VolumeImportSourceAnnotation]; ok { + return nil, fmt.Errorf("importing volumes is not supported for remote requests") + } + } + r, err := ic.playKubePVC(ctx, &pvcYAML) if err != nil { return nil, err @@ -859,6 +874,7 @@ func (ic *ContainerEngine) playKubePVC(ctx context.Context, pvcYAML *v1.Persiste // Get pvc annotations and create remaining podman volume options if available. // These are podman volume options that do not match any of the persistent volume claim // attributes, so they can be configured using annotations since they will not affect k8s. + var importFrom string for k, v := range pvcYAML.Annotations { switch k { case util.VolumeDriverAnnotation: @@ -883,16 +899,45 @@ func (ic *ContainerEngine) playKubePVC(ctx context.Context, pvcYAML *v1.Persiste opts["GID"] = v case util.VolumeMountOptsAnnotation: opts["o"] = v + case util.VolumeImportSourceAnnotation: + importFrom = v } } volOptions = append(volOptions, libpod.WithVolumeOptions(opts)) + // Validate the file and open it before creating the volume for fast-fail + var tarFile *os.File + if len(importFrom) > 0 { + err := parse.ValidateFileName(importFrom) + if err != nil { + return nil, err + } + + // open tar file + tarFile, err = os.Open(importFrom) + if err != nil { + return nil, err + } + defer tarFile.Close() + } + // Create volume. vol, err := ic.Libpod.NewVolume(ctx, volOptions...) if err != nil { return nil, err } + if tarFile != nil { + err = ic.importVolume(ctx, vol, tarFile) + if err != nil { + // Remove the volume to avoid partial success + if rmErr := ic.Libpod.RemoveVolume(ctx, vol, true, nil); rmErr != nil { + logrus.Debug(rmErr) + } + return nil, err + } + } + report.Volumes = append(report.Volumes, entities.PlayKubeVolume{ Name: vol.Name(), }) @@ -900,6 +945,42 @@ func (ic *ContainerEngine) playKubePVC(ctx context.Context, pvcYAML *v1.Persiste return &report, nil } +func (ic *ContainerEngine) importVolume(ctx context.Context, vol *libpod.Volume, tarFile *os.File) error { + volumeConfig, err := vol.Config() + if err != nil { + return err + } + + mountPoint := volumeConfig.MountPoint + if len(mountPoint) == 0 { + return errors.New("volume is not mounted anywhere on host") + } + + driver := volumeConfig.Driver + volumeOptions := volumeConfig.Options + volumeMountStatus, err := ic.VolumeMounted(ctx, vol.Name()) + if err != nil { + return err + } + + // Check if volume needs a mount and export only if volume is mounted + if vol.NeedsMount() && !volumeMountStatus.Value { + return fmt.Errorf("volume needs to be mounted but is not mounted on %s", mountPoint) + } + + // Check if volume is using `local` driver and has mount options type other than tmpfs + if len(driver) == 0 || driver == define.VolumeDriverLocal { + if mountOptionType, ok := volumeOptions["type"]; ok { + if mountOptionType != "tmpfs" && !volumeMountStatus.Value { + return fmt.Errorf("volume is using a driver %s and volume is not mounted on %s", driver, mountPoint) + } + } + } + + // dont care if volume is mounted or not we are gonna import everything to mountPoint + return utils.UntarToFileSystem(mountPoint, tarFile, nil) +} + // readConfigMapFromFile returns a kubernetes configMap obtained from --configmap flag func readConfigMapFromFile(r io.Reader) (v1.ConfigMap, error) { var cm v1.ConfigMap diff --git a/pkg/util/kube.go b/pkg/util/kube.go index 1255cdfc59..1a70ed0518 100644 --- a/pkg/util/kube.go +++ b/pkg/util/kube.go @@ -13,4 +13,6 @@ const ( VolumeGIDAnnotation = "volume.podman.io/gid" // Kube annotation for podman volume mount options. VolumeMountOptsAnnotation = "volume.podman.io/mount-options" + // Kube annotation for podman volume import source. + VolumeImportSourceAnnotation = "volume.podman.io/import-source" ) diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index 97823e232b..a556675aea 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -3,6 +3,7 @@ package integration import ( "bytes" "context" + "encoding/json" "fmt" "net" "net/url" @@ -19,6 +20,7 @@ import ( "github.com/containers/podman/v4/pkg/bindings/play" "github.com/containers/podman/v4/pkg/util" . "github.com/containers/podman/v4/test/utils" + "github.com/containers/podman/v4/utils" "github.com/containers/storage/pkg/stringid" "github.com/google/uuid" . "github.com/onsi/ginkgo" @@ -1326,6 +1328,36 @@ func milliCPUToQuota(milliCPU string) int { return milli * defaultCPUPeriod } +func createSourceTarFile(fileName, fileContent, tarFilePath string) error { + dir, err := os.MkdirTemp("", "podmanTest") + if err != nil { + return err + } + + file, err := os.Create(filepath.Join(dir, fileName)) + if err != nil { + return err + } + + _, err = file.Write([]byte(fileContent)) + if err != nil { + return err + } + + err = file.Close() + if err != nil { + return err + } + + tarFile, err := os.Create(tarFilePath) + if err != nil { + return err + } + defer tarFile.Close() + + return utils.TarToFilesystem(dir, tarFile) +} + var _ = Describe("Podman play kube", func() { var ( tempdir string @@ -3075,6 +3107,46 @@ o: {{ .Options.o }}`}) Expect(inspect.OutputToString()).To(ContainSubstring("o: " + volOpts)) }) + It("podman play kube persistentVolumeClaim with source", func() { + fileName := "data" + expectedFileContent := "Test" + tarFilePath := filepath.Join(os.TempDir(), "podmanVolumeSource.tgz") + err := createSourceTarFile(fileName, expectedFileContent, tarFilePath) + Expect(err).To(BeNil()) + + volName := "myVolWithStorage" + pvc := getPVC(withPVCName(volName), + withPVCAnnotations(util.VolumeImportSourceAnnotation, tarFilePath), + ) + err = generateKubeYaml("persistentVolumeClaim", pvc, kubeYaml) + Expect(err).To(BeNil()) + + kube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) + kube.WaitWithDefaultTimeout() + if IsRemote() { + Expect(kube).Error() + Expect(kube.ErrorToString()).To(ContainSubstring("importing volumes is not supported for remote requests")) + return + } + Expect(kube).Should(Exit(0)) + + inspect := podmanTest.Podman([]string{"inspect", volName, "--format", ` +{ + "Name": "{{ .Name }}", + "Mountpoint": "{{ .Mountpoint }}" +}`}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(Exit(0)) + mp := make(map[string]string) + err = json.Unmarshal([]byte(inspect.OutputToString()), &mp) + Expect(err).To(BeNil()) + Expect(mp["Name"]).To(Equal(volName)) + files, err := os.ReadDir(mp["Mountpoint"]) + Expect(err).To(BeNil()) + Expect(len(files)).To(Equal(1)) + Expect(files[0].Name()).To(Equal(fileName)) + }) + // Multi doc related tests It("podman play kube multi doc yaml with persistentVolumeClaim, service and deployment", func() { yamlDocs := []string{}