diff --git a/docs/source/markdown/options/mount.md b/docs/source/markdown/options/mount.md index e0e6e50c93..8e05ab804c 100644 --- a/docs/source/markdown/options/mount.md +++ b/docs/source/markdown/options/mount.md @@ -6,12 +6,12 @@ Attach a filesystem mount to the container -Current supported mount TYPEs are **bind**, **devpts**, **glob**, **image**, **ramfs**, **tmpfs** and **volume**. +Current supported mount TYPEs are **artifact**, **bind**, **devpts**, **glob**, **image**, **ramfs**, **tmpfs** and **volume**. Options common to all mount types: - *src*, *source*: mount source spec for **bind**, **glob**, and **volume**. - Mandatory for **bind** and **glob**. + Mandatory for **artifact**, **bind**, **glob**, **image** and **volume**. - *dst*, *destination*, *target*: mount destination spec. @@ -24,6 +24,25 @@ on the destination directory are mounted. The option to mount host files matching /foo* to the /tmp/bar/ directory in the container. +Options specific to type=**artifact**: + +- *digest*: If the artifact source contains multiple blobs a digest can be + specified to only mount the one specific blob with the digest. + +- *title*: If the artifact source contains multiple blobs a title can be set + which is compared against `org.opencontainers.image.title` annotation. + +The *src* argument contains the name of the artifact, it must already exist locally. +The *dst* argument contains the target path, if the path in the container is a +directory or does not exist the blob title (`org.opencontainers.image.title` +annotation) will be used as filename and joined to the path. If the annotation +does not exist the digest will be used as filename instead. This results in all blobs +of the artifact mounted into the container at the given path. + +However if the *dst* path is a existing file in the container then the blob will be +mounted directly on it. This only works when the artifact contains of a single blob +or when either *digest* or *title* are specified. + Options specific to type=**volume**: - *ro*, *readonly*: *true* or *false* (default if unspecified: *false*). @@ -104,4 +123,6 @@ Examples: - `type=tmpfs,destination=/path/in/container,noswap` -- `type=volume,source=vol1,destination=/path/in/container,ro=true` +- `type=artifact,src=quay.io/libpod/testartifact:20250206-single,dst=/data` + +- `type=artifact,src=quay.io/libpod/testartifact:20250206-multi,dst=/data,title=test1` diff --git a/libpod/container.go b/libpod/container.go index fb9ca11800..479ef80829 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -280,6 +280,28 @@ type ContainerImageVolume struct { SubPath string `json:"subPath,omitempty"` } +// ContainerArtifactVolume is a volume based on a artifact. The artifact blobs will +// be bind mounted directly as files and must always be read only. +type ContainerArtifactVolume struct { + // Source is the name or digest of the artifact that should be mounted + Source string `json:"source"` + // Dest is the absolute path of the mount in the container. + // If path is a file in the container, then the artifact must consist of a single blob. + // Otherwise if it is a directory or does not exists all artifact blobs will be mounted + // into this path as files. As name the "org.opencontainers.image.title" will be used if + // available otherwise the digest is used as name. + Dest string `json:"dest"` + // Title can be used for multi blob artifacts to only mount the one specific blob that + // matches the "org.opencontainers.image.title" annotation. + // Optional. Conflicts with Digest. + Title string `json:"title"` + // Digest can be used to filter a single blob from a multi blob artifact by the given digest. + // When this option is set the file name in the container defaults to the digest even when + // the title annotation exist. + // Optional. Conflicts with Title. + Digest string `json:"digest"` +} + // ContainerSecret is a secret that is mounted in a container type ContainerSecret struct { // Secret is the secret diff --git a/libpod/container_config.go b/libpod/container_config.go index 3ccfad43c1..888b4636e7 100644 --- a/libpod/container_config.go +++ b/libpod/container_config.go @@ -162,6 +162,8 @@ type ContainerRootFSConfig struct { // moved out of Libpod into pkg/specgen). // Please DO NOT reuse the `imageVolumes` name in container JSON again. ImageVolumes []*ContainerImageVolume `json:"ctrImageVolumes,omitempty"` + // ArtifactVolumes lists the artifact volumes to mount into the container. + ArtifactVolumes []*ContainerArtifactVolume `json:"artifactVolumes,omitempty"` // CreateWorkingDir indicates that Libpod should create the container's // working directory if it does not exist. Some OCI runtimes do this by // default, but others do not. diff --git a/libpod/container_internal_common.go b/libpod/container_internal_common.go index 5b8d898247..c240ff225e 100644 --- a/libpod/container_internal_common.go +++ b/libpod/container_internal_common.go @@ -41,6 +41,7 @@ import ( "github.com/containers/podman/v5/pkg/annotations" "github.com/containers/podman/v5/pkg/checkpoint/crutils" "github.com/containers/podman/v5/pkg/criu" + libartTypes "github.com/containers/podman/v5/pkg/libartifact/types" "github.com/containers/podman/v5/pkg/lookup" "github.com/containers/podman/v5/pkg/rootless" "github.com/containers/podman/v5/pkg/util" @@ -483,6 +484,52 @@ func (c *Container) generateSpec(ctx context.Context) (s *spec.Spec, cleanupFunc g.AddMount(overlayMount) } + if len(c.config.ArtifactVolumes) > 0 { + artStore, err := c.runtime.ArtifactStore() + if err != nil { + return nil, nil, err + } + for _, artifactMount := range c.config.ArtifactVolumes { + paths, err := artStore.BlobMountPaths(ctx, artifactMount.Source, &libartTypes.BlobMountPathOptions{ + FilterBlobOptions: libartTypes.FilterBlobOptions{ + Title: artifactMount.Title, + Digest: artifactMount.Digest, + }, + }) + if err != nil { + return nil, nil, err + } + + // Ignore the error, destIsFile will return false with errors so if the file does not exist + // we treat it as dir, the oci runtime will always create the target bind mount path. + destIsFile, _ := containerPathIsFile(c.state.Mountpoint, artifactMount.Dest) + if destIsFile && len(paths) > 1 { + return nil, nil, fmt.Errorf("artifact %q contains more than one blob and container path %q is a file", artifactMount.Source, artifactMount.Dest) + } + + for _, path := range paths { + var dest string + if destIsFile { + dest = artifactMount.Dest + } else { + dest = filepath.Join(artifactMount.Dest, path.Name) + } + + logrus.Debugf("Mounting artifact %q in container %s, mount blob %q to %q", artifactMount.Source, c.ID(), path.SourcePath, dest) + + g.AddMount(spec.Mount{ + Destination: dest, + Source: path.SourcePath, + Type: define.TypeBind, + // Important: This must always be mounted read only here, we are using + // the source in the artifact store directly and because that is digest + // based a write will break the layout. + Options: []string{define.TypeBind, "ro"}, + }) + } + } + } + err = c.setHomeEnvIfNeeded() if err != nil { return nil, nil, err diff --git a/libpod/container_internal_freebsd.go b/libpod/container_internal_freebsd.go index a3e0c5d613..64d377b1fc 100644 --- a/libpod/container_internal_freebsd.go +++ b/libpod/container_internal_freebsd.go @@ -13,6 +13,7 @@ import ( "github.com/containers/common/libnetwork/types" "github.com/containers/podman/v5/pkg/rootless" + securejoin "github.com/cyphar/filepath-securejoin" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/sirupsen/logrus" @@ -415,3 +416,20 @@ func (c *Container) hasPrivateUTS() bool { func hasCapSysResource() (bool, error) { return true, nil } + +// containerPathIsFile returns true if the given containerPath is a file +func containerPathIsFile(unsafeRoot string, containerPath string) (bool, error) { + // Note freebsd does not have support for OpenInRoot() so us the less safe way + // with the old SecureJoin(), but given this is only called before the container + // is started it is not subject to race conditions with the container process. + path, err := securejoin.SecureJoin(unsafeRoot, containerPath) + if err != nil { + return false, err + } + + st, err := os.Lstat(path) + if err == nil && !st.IsDir() { + return true, nil + } + return false, err +} diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 0f6e5905d3..3a3704c2ff 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -21,6 +21,7 @@ import ( "github.com/containers/podman/v5/libpod/define" "github.com/containers/podman/v5/libpod/shutdown" "github.com/containers/podman/v5/pkg/rootless" + securejoin "github.com/cyphar/filepath-securejoin" "github.com/moby/sys/capability" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" @@ -848,3 +849,18 @@ var hasCapSysResource = sync.OnceValues(func() (bool, error) { } return currentCaps.Get(capability.EFFECTIVE, capability.CAP_SYS_RESOURCE), nil }) + +// containerPathIsFile returns true if the given containerPath is a file +func containerPathIsFile(unsafeRoot string, containerPath string) (bool, error) { + f, err := securejoin.OpenInRoot(unsafeRoot, containerPath) + if err != nil { + return false, err + } + defer f.Close() + + st, err := f.Stat() + if err == nil && !st.IsDir() { + return true, nil + } + return false, err +} diff --git a/libpod/options.go b/libpod/options.go index 4578a8ed26..980ce60b13 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1515,6 +1515,19 @@ func WithImageVolumes(volumes []*ContainerImageVolume) CtrCreateOption { } } +// WithImageVolumes adds the given image volumes to the container. +func WithArtifactVolumes(volumes []*ContainerArtifactVolume) CtrCreateOption { + return func(ctr *Container) error { + if ctr.valid { + return define.ErrCtrFinalized + } + + ctr.config.ArtifactVolumes = volumes + + return nil + } +} + // WithHealthCheck adds the healthcheck to the container config func WithHealthCheck(healthCheck *manifest.Schema2HealthConfig) CtrCreateOption { return func(ctr *Container) error { diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index ca77218945..73ec4a5f95 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -507,6 +507,19 @@ func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator, pod *l options = append(options, libpod.WithImageVolumes(vols)) } + if len(s.ArtifactVolumes) != 0 { + vols := make([]*libpod.ContainerArtifactVolume, 0, len(s.ArtifactVolumes)) + for _, v := range s.ArtifactVolumes { + vols = append(vols, &libpod.ContainerArtifactVolume{ + Dest: v.Destination, + Source: v.Source, + Digest: v.Digest, + Title: v.Title, + }) + } + options = append(options, libpod.WithArtifactVolumes(vols)) + } + if s.Command != nil { options = append(options, libpod.WithCommand(s.Command)) } diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index c5ba5fe17f..a62f4c7008 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -305,6 +305,8 @@ type ContainerStorageConfig struct { // Image volumes bind-mount a container-image mount into the container. // Optional. ImageVolumes []*ImageVolume `json:"image_volumes,omitempty"` + // ArtifactVolumes volumes based on an existing artifact. + ArtifactVolumes []*ArtifactVolume `json:"artifact_volumes,omitempty"` // Devices are devices that will be added to the container. // Optional. Devices []spec.LinuxDevice `json:"devices,omitempty"` diff --git a/pkg/specgen/volumes.go b/pkg/specgen/volumes.go index 77c89f8ce4..dbe987d439 100644 --- a/pkg/specgen/volumes.go +++ b/pkg/specgen/volumes.go @@ -58,6 +58,28 @@ type ImageVolume struct { SubPath string `json:"subPath,omitempty"` } +// ArtifactVolume is a volume based on a artifact. The artifact blobs will +// be bind mounted directly as files and must always be read only. +type ArtifactVolume struct { + // Source is the name or digest of the artifact that should be mounted + Source string `json:"source"` + // Destination is the absolute path of the mount in the container. + // If path is a file in the container, then the artifact must consist of a single blob. + // Otherwise if it is a directory or does not exists all artifact blobs will be mounted + // into this path as files. As name the "org.opencontainers.image.title" will be used if + // available otherwise the digest is used as name. + Destination string `json:"destination"` + // Title can be used for multi blob artifacts to only mount the one specific blob that + // matches the "org.opencontainers.image.title" annotation. + // Optional. Conflicts with Digest. + Title string `json:"title,omitempty"` + // Digest can be used to filter a single blob from a multi blob artifact by the given digest. + // When this option is set the file name in the container defaults to the digest even when + // the title annotation exist. + // Optional. Conflicts with Title. + Digest string `json:"digest,omitempty"` +} + // GenVolumeMounts parses user input into mounts, volumes and overlay volumes func GenVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*NamedVolume, map[string]*OverlayVolume, error) { mounts := make(map[string]spec.Mount) diff --git a/pkg/specgenutil/specgen.go b/pkg/specgenutil/specgen.go index f3ee9877cf..6163f47a0e 100644 --- a/pkg/specgenutil/specgen.go +++ b/pkg/specgenutil/specgen.go @@ -790,6 +790,9 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions if len(s.ImageVolumes) == 0 { s.ImageVolumes = containerMounts.imageVolumes } + if len(s.ArtifactVolumes) == 0 { + s.ArtifactVolumes = containerMounts.artifactVolumes + } devices := c.Devices for _, gpu := range c.GPUs { diff --git a/pkg/specgenutil/volumes.go b/pkg/specgenutil/volumes.go index 1dea8fa2a4..d8d190578c 100644 --- a/pkg/specgenutil/volumes.go +++ b/pkg/specgenutil/volumes.go @@ -23,17 +23,19 @@ var ( ) type containerMountSlice struct { - mounts []spec.Mount - volumes []*specgen.NamedVolume - overlayVolumes []*specgen.OverlayVolume - imageVolumes []*specgen.ImageVolume + mounts []spec.Mount + volumes []*specgen.NamedVolume + overlayVolumes []*specgen.OverlayVolume + imageVolumes []*specgen.ImageVolume + artifactVolumes []*specgen.ArtifactVolume } // containerMountMap contains the container mounts with the destination path as map key type containerMountMap struct { - mounts map[string]spec.Mount - volumes map[string]*specgen.NamedVolume - imageVolumes map[string]*specgen.ImageVolume + mounts map[string]spec.Mount + volumes map[string]*specgen.NamedVolume + imageVolumes map[string]*specgen.ImageVolume + artifactVolumes map[string]*specgen.ArtifactVolume } type universalMount struct { @@ -131,6 +133,11 @@ func parseVolumes(rtc *config.Config, volumeFlag, mountFlag, tmpfsFlag []string) return nil, err } } + for dest := range unifiedContainerMounts.artifactVolumes { + if err := testAndSet(dest); err != nil { + return nil, err + } + } // Final step: maps to arrays finalMounts := make([]spec.Mount, 0, len(unifiedContainerMounts.mounts)) @@ -156,12 +163,17 @@ func parseVolumes(rtc *config.Config, volumeFlag, mountFlag, tmpfsFlag []string) for _, volume := range unifiedContainerMounts.imageVolumes { finalImageVolumes = append(finalImageVolumes, volume) } + finalArtifactVolumes := make([]*specgen.ArtifactVolume, 0, len(unifiedContainerMounts.artifactVolumes)) + for _, volume := range unifiedContainerMounts.artifactVolumes { + finalArtifactVolumes = append(finalArtifactVolumes, volume) + } return &containerMountSlice{ - mounts: finalMounts, - volumes: finalVolumes, - overlayVolumes: finalOverlayVolume, - imageVolumes: finalImageVolumes, + mounts: finalMounts, + volumes: finalVolumes, + overlayVolumes: finalOverlayVolume, + imageVolumes: finalImageVolumes, + artifactVolumes: finalArtifactVolumes, }, nil } @@ -170,10 +182,12 @@ func parseVolumes(rtc *config.Config, volumeFlag, mountFlag, tmpfsFlag []string) // podman run --mount type=bind,src=/etc/resolv.conf,target=/etc/resolv.conf ... // podman run --mount type=tmpfs,target=/dev/shm ... // podman run --mount type=volume,source=test-volume, ... +// podman run --mount type=artifact,source=$artifact,dest=... func mounts(mountFlag []string, configMounts []string) (*containerMountMap, error) { finalMounts := make(map[string]spec.Mount) finalNamedVolumes := make(map[string]*specgen.NamedVolume) finalImageVolumes := make(map[string]*specgen.ImageVolume) + finalArtifactVolumes := make(map[string]*specgen.ArtifactVolume) parseMounts := func(mounts []string, ignoreDup bool) error { for _, mount := range mounts { // TODO: Docker defaults to "volume" if no mount type is specified. @@ -244,6 +258,18 @@ func mounts(mountFlag []string, configMounts []string) (*containerMountMap, erro return fmt.Errorf("%v: %w", volume.Destination, specgen.ErrDuplicateDest) } finalImageVolumes[volume.Destination] = volume + case "artifact": + volume, err := getArtifactVolume(tokens) + if err != nil { + return err + } + if _, ok := finalArtifactVolumes[volume.Destination]; ok { + if ignoreDup { + continue + } + return fmt.Errorf("%v: %w", volume.Destination, specgen.ErrDuplicateDest) + } + finalArtifactVolumes[volume.Destination] = volume case "volume": volume, err := getNamedVolume(tokens) if err != nil { @@ -276,9 +302,10 @@ func mounts(mountFlag []string, configMounts []string) (*containerMountMap, erro } return &containerMountMap{ - mounts: finalMounts, - volumes: finalNamedVolumes, - imageVolumes: finalImageVolumes, + mounts: finalMounts, + volumes: finalNamedVolumes, + imageVolumes: finalImageVolumes, + artifactVolumes: finalArtifactVolumes, }, nil } @@ -683,6 +710,50 @@ func getImageVolume(args []string) (*specgen.ImageVolume, error) { return newVolume, nil } +// Parse the arguments into an artifact volume. An artifact volume creates mounts +// based on an existing artifact in the store. +func getArtifactVolume(args []string) (*specgen.ArtifactVolume, error) { + newVolume := new(specgen.ArtifactVolume) + + for _, arg := range args { + name, value, hasValue := strings.Cut(arg, "=") + switch name { + case "src", "source": + if !hasValue { + return nil, fmt.Errorf("%v: %w", name, errOptionArg) + } + newVolume.Source = value + case "target", "dst", "destination": + if !hasValue { + return nil, fmt.Errorf("%v: %w", name, errOptionArg) + } + if err := parse.ValidateVolumeCtrDir(value); err != nil { + return nil, err + } + newVolume.Destination = unixPathClean(value) + case "title": + if !hasValue { + return nil, fmt.Errorf("%v: %w", name, errOptionArg) + } + newVolume.Title = value + + case "digest": + if !hasValue { + return nil, fmt.Errorf("%v: %w", name, errOptionArg) + } + newVolume.Digest = value + default: + return nil, fmt.Errorf("%s: %w", name, util.ErrBadMntOption) + } + } + + if len(newVolume.Source)*len(newVolume.Destination) == 0 { + return nil, errors.New("must set source and destination for artifact volume") + } + + return newVolume, nil +} + // GetTmpfsMounts creates spec.Mount structs for user-requested tmpfs mounts func getTmpfsMounts(tmpfsFlag []string) (map[string]spec.Mount, error) { m := make(map[string]spec.Mount) diff --git a/test/e2e/artifact_mount_test.go b/test/e2e/artifact_mount_test.go new file mode 100644 index 0000000000..d158dc1331 --- /dev/null +++ b/test/e2e/artifact_mount_test.go @@ -0,0 +1,223 @@ +//go:build linux || freebsd + +package integration + +import ( + "os" + "path/filepath" + + . "github.com/containers/podman/v5/test/utils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Podman artifact mount", func() { + BeforeEach(func() { + SkipIfRemote("artifacts are not supported on the remote client yet due to being in development still") + }) + + It("podman artifact mount single blob", func() { + podmanTest.PodmanExitCleanly("artifact", "pull", ARTIFACT_SINGLE) + + const artifactContent = "mRuO9ykak1Q2j" + + tests := []struct { + name string + mountOpts string + containerFile string + }{ + { + name: "single artifact mount", + mountOpts: "dst=/test", + containerFile: "/test/testfile", + }, + { + name: "single artifact mount on existing file", + mountOpts: "dst=/etc/os-release", + containerFile: "/etc/os-release", + }, + { + name: "single artifact mount with title", + mountOpts: "dst=/tmp,title=testfile", + containerFile: "/tmp/testfile", + }, + { + name: "single artifact mount with digest", + mountOpts: "dst=/data,digest=sha256:e9510923578af3632946ecf5ae479c1b5f08b47464e707b5cbab9819272a9752", + containerFile: "/data/sha256-e9510923578af3632946ecf5ae479c1b5f08b47464e707b5cbab9819272a9752", + }, + } + + for _, tt := range tests { + By(tt.name) + // FIXME: we need https://github.com/containers/container-selinux/pull/360 to fix the selinux access problem, until then disable it. + session := podmanTest.PodmanExitCleanly("run", "--security-opt=label=disable", "--rm", "--mount", "type=artifact,src="+ARTIFACT_SINGLE+","+tt.mountOpts, ALPINE, "cat", tt.containerFile) + Expect(session.OutputToString()).To(Equal(artifactContent)) + } + }) + + It("podman artifact mount multi blob", func() { + podmanTest.PodmanExitCleanly("artifact", "pull", ARTIFACT_MULTI) + podmanTest.PodmanExitCleanly("artifact", "pull", ARTIFACT_MULTI_NO_TITLE) + + const ( + artifactContent1 = "xuHWedtC0ADST" + artifactContent2 = "tAyZczFlgFsi4" + ) + + type expectedFiles struct { + file string + content string + } + + tests := []struct { + name string + mountOpts string + containerFiles []expectedFiles + }{ + { + name: "multi blob with title", + mountOpts: "src=" + ARTIFACT_MULTI + ",dst=/test", + containerFiles: []expectedFiles{ + { + file: "/test/test1", + content: artifactContent1, + }, + { + file: "/test/test2", + content: artifactContent2, + }, + }, + }, + { + name: "multi blob without title", + mountOpts: "src=" + ARTIFACT_MULTI_NO_TITLE + ",dst=/test", + containerFiles: []expectedFiles{ + { + file: "/test/sha256-8257bba28b9d19ac353c4b713b470860278857767935ef7e139afd596cb1bb2d", + content: artifactContent1, + }, + { + file: "/test/sha256-63700c54129c6daaafe3a20850079f82d6d658d69de73d6158d81f920c6fbdd7", + content: artifactContent2, + }, + }, + }, + { + name: "multi blob filter by title", + mountOpts: "src=" + ARTIFACT_MULTI + ",dst=/test,title=test2", + containerFiles: []expectedFiles{ + { + file: "/test/test2", + content: artifactContent2, + }, + }, + }, + { + name: "multi blob filter by digest", + mountOpts: "src=" + ARTIFACT_MULTI + ",dst=/test,digest=sha256:8257bba28b9d19ac353c4b713b470860278857767935ef7e139afd596cb1bb2d", + containerFiles: []expectedFiles{ + { + file: "/test/sha256-8257bba28b9d19ac353c4b713b470860278857767935ef7e139afd596cb1bb2d", + content: artifactContent1, + }, + }, + }, + } + for _, tt := range tests { + By(tt.name) + // FIXME: we need https://github.com/containers/container-selinux/pull/360 to fix the selinux access problem, until then disable it. + args := []string{"run", "--security-opt=label=disable", "--rm", "--mount", "type=artifact," + tt.mountOpts, ALPINE, "cat"} + for _, f := range tt.containerFiles { + args = append(args, f.file) + } + session := podmanTest.PodmanExitCleanly(args...) + outs := session.OutputToStringArray() + Expect(outs).To(HaveLen(len(tt.containerFiles))) + for i, f := range tt.containerFiles { + Expect(outs[i]).To(Equal(f.content)) + } + } + }) + + It("podman artifact mount remove while in use", func() { + ctrName := "ctr1" + artifactName := "localhost/test" + artifactFileName := "somefile" + + artifactFile := filepath.Join(podmanTest.TempDir, artifactFileName) + err := os.WriteFile(artifactFile, []byte("hello world\n"), 0o644) + Expect(err).ToNot(HaveOccurred()) + + podmanTest.PodmanExitCleanly("artifact", "add", artifactName, artifactFile) + + // FIXME: we need https://github.com/containers/container-selinux/pull/360 to fix the selinux access problem, until then disable it. + podmanTest.PodmanExitCleanly("run", "--security-opt=label=disable", "--name", ctrName, "-d", "--mount", "type=artifact,src="+artifactName+",dst=/test", ALPINE, "sleep", "100") + + podmanTest.PodmanExitCleanly("artifact", "rm", artifactName) + + // file must sill be readable after artifact removal + session := podmanTest.PodmanExitCleanly("exec", ctrName, "cat", "/test/"+artifactFileName) + Expect(session.OutputToString()).To(Equal("hello world")) + + // restart will fail if artifact does not exist + session = podmanTest.Podman([]string{"restart", "-t0", ctrName}) + session.WaitWithDefaultTimeout() + Expect(session).To(ExitWithError(125, artifactName+": artifact does not exist")) + + // create a artifact with the same name again and add another file to ensure it picks up the changes + artifactFile2Name := "otherfile" + artifactFile2 := filepath.Join(podmanTest.TempDir, artifactFile2Name) + err = os.WriteFile(artifactFile2, []byte("second file"), 0o644) + Expect(err).ToNot(HaveOccurred()) + + podmanTest.PodmanExitCleanly("artifact", "add", artifactName, artifactFile, artifactFile2) + podmanTest.PodmanExitCleanly("start", ctrName) + + session = podmanTest.PodmanExitCleanly("exec", ctrName, "cat", "/test/"+artifactFileName, "/test/"+artifactFile2Name) + Expect(session.OutputToString()).To(Equal("hello world second file")) + }) + + It("podman artifact mount dest conflict", func() { + tests := []struct { + name string + mount string + }{ + { + name: "bind mount --volume", + mount: "--volume=/tmp:/test", + }, + { + name: "overlay mount", + mount: "--volume=/tmp:/test:O", + }, + { + name: "named volume", + mount: "--volume=abc:/test:O", + }, + { + name: "bind mount --mount type=bind", + mount: "--mount=type=bind,src=/tmp,dst=/test", + }, + { + name: "image mount", + mount: "--mount=type=bind,src=someimage,dst=/test", + }, + { + name: "tmpfs mount", + mount: "--tmpfs=/test", + }, + { + name: "artifact mount", + mount: "--mount=type=artifact,src=abc,dst=/test", + }, + } + + for _, tt := range tests { + By(tt.name) + session := podmanTest.Podman([]string{"run", "--rm", "--mount", "type=artifact,src=someartifact,dst=/test", tt.mount, ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session).To(ExitWithError(125, "/test: duplicate mount destination")) + } + }) +})