mirror of
https://github.com/containers/podman.git
synced 2025-05-17 15:18:43 +08:00
add new artifact mount type
Add a new option to allow for mounting artifacts in the container, the syntax is added to the existing --mount option: type=artifact,src=$artifactName,dest=/path[,digest=x][,title=x] This works very similar to image mounts. The name is passed down into the container config and then on each start we lookup the artifact and the figure out which blobs to mount. There is no protaction against a user removing the artifact while still being used in a container. When the container is running the bind mounted files will stay there (as the kernel keeps the mounts active even if the bind source was deleted). On the next start it will fail to start as if it does not find the artifact. The good thing is that this technically allows someone to update the artifact with the new file by creating a new artifact with the same name. Signed-off-by: Paul Holzinger <pholzing@redhat.com>
This commit is contained in:
@ -6,12 +6,12 @@
|
|||||||
|
|
||||||
Attach a filesystem mount to the container
|
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:
|
Options common to all mount types:
|
||||||
|
|
||||||
- *src*, *source*: mount source spec for **bind**, **glob**, and **volume**.
|
- *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.
|
- *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/
|
to mount host files matching /foo* to the /tmp/bar/
|
||||||
directory in the container.
|
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**:
|
Options specific to type=**volume**:
|
||||||
|
|
||||||
- *ro*, *readonly*: *true* or *false* (default if unspecified: *false*).
|
- *ro*, *readonly*: *true* or *false* (default if unspecified: *false*).
|
||||||
@ -104,4 +123,6 @@ Examples:
|
|||||||
|
|
||||||
- `type=tmpfs,destination=/path/in/container,noswap`
|
- `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`
|
||||||
|
@ -280,6 +280,28 @@ type ContainerImageVolume struct {
|
|||||||
SubPath string `json:"subPath,omitempty"`
|
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
|
// ContainerSecret is a secret that is mounted in a container
|
||||||
type ContainerSecret struct {
|
type ContainerSecret struct {
|
||||||
// Secret is the secret
|
// Secret is the secret
|
||||||
|
@ -162,6 +162,8 @@ type ContainerRootFSConfig struct {
|
|||||||
// moved out of Libpod into pkg/specgen).
|
// moved out of Libpod into pkg/specgen).
|
||||||
// Please DO NOT reuse the `imageVolumes` name in container JSON again.
|
// Please DO NOT reuse the `imageVolumes` name in container JSON again.
|
||||||
ImageVolumes []*ContainerImageVolume `json:"ctrImageVolumes,omitempty"`
|
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
|
// CreateWorkingDir indicates that Libpod should create the container's
|
||||||
// working directory if it does not exist. Some OCI runtimes do this by
|
// working directory if it does not exist. Some OCI runtimes do this by
|
||||||
// default, but others do not.
|
// default, but others do not.
|
||||||
|
@ -41,6 +41,7 @@ import (
|
|||||||
"github.com/containers/podman/v5/pkg/annotations"
|
"github.com/containers/podman/v5/pkg/annotations"
|
||||||
"github.com/containers/podman/v5/pkg/checkpoint/crutils"
|
"github.com/containers/podman/v5/pkg/checkpoint/crutils"
|
||||||
"github.com/containers/podman/v5/pkg/criu"
|
"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/lookup"
|
||||||
"github.com/containers/podman/v5/pkg/rootless"
|
"github.com/containers/podman/v5/pkg/rootless"
|
||||||
"github.com/containers/podman/v5/pkg/util"
|
"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)
|
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()
|
err = c.setHomeEnvIfNeeded()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/containers/common/libnetwork/types"
|
"github.com/containers/common/libnetwork/types"
|
||||||
"github.com/containers/podman/v5/pkg/rootless"
|
"github.com/containers/podman/v5/pkg/rootless"
|
||||||
|
securejoin "github.com/cyphar/filepath-securejoin"
|
||||||
spec "github.com/opencontainers/runtime-spec/specs-go"
|
spec "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
"github.com/opencontainers/runtime-tools/generate"
|
"github.com/opencontainers/runtime-tools/generate"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -415,3 +416,20 @@ func (c *Container) hasPrivateUTS() bool {
|
|||||||
func hasCapSysResource() (bool, error) {
|
func hasCapSysResource() (bool, error) {
|
||||||
return true, nil
|
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
|
||||||
|
}
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/containers/podman/v5/libpod/define"
|
"github.com/containers/podman/v5/libpod/define"
|
||||||
"github.com/containers/podman/v5/libpod/shutdown"
|
"github.com/containers/podman/v5/libpod/shutdown"
|
||||||
"github.com/containers/podman/v5/pkg/rootless"
|
"github.com/containers/podman/v5/pkg/rootless"
|
||||||
|
securejoin "github.com/cyphar/filepath-securejoin"
|
||||||
"github.com/moby/sys/capability"
|
"github.com/moby/sys/capability"
|
||||||
spec "github.com/opencontainers/runtime-spec/specs-go"
|
spec "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
"github.com/opencontainers/runtime-tools/generate"
|
"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
|
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
|
||||||
|
}
|
||||||
|
@ -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
|
// WithHealthCheck adds the healthcheck to the container config
|
||||||
func WithHealthCheck(healthCheck *manifest.Schema2HealthConfig) CtrCreateOption {
|
func WithHealthCheck(healthCheck *manifest.Schema2HealthConfig) CtrCreateOption {
|
||||||
return func(ctr *Container) error {
|
return func(ctr *Container) error {
|
||||||
|
@ -507,6 +507,19 @@ func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator, pod *l
|
|||||||
options = append(options, libpod.WithImageVolumes(vols))
|
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 {
|
if s.Command != nil {
|
||||||
options = append(options, libpod.WithCommand(s.Command))
|
options = append(options, libpod.WithCommand(s.Command))
|
||||||
}
|
}
|
||||||
|
@ -305,6 +305,8 @@ type ContainerStorageConfig struct {
|
|||||||
// Image volumes bind-mount a container-image mount into the container.
|
// Image volumes bind-mount a container-image mount into the container.
|
||||||
// Optional.
|
// Optional.
|
||||||
ImageVolumes []*ImageVolume `json:"image_volumes,omitempty"`
|
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.
|
// Devices are devices that will be added to the container.
|
||||||
// Optional.
|
// Optional.
|
||||||
Devices []spec.LinuxDevice `json:"devices,omitempty"`
|
Devices []spec.LinuxDevice `json:"devices,omitempty"`
|
||||||
|
@ -58,6 +58,28 @@ type ImageVolume struct {
|
|||||||
SubPath string `json:"subPath,omitempty"`
|
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
|
// 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) {
|
func GenVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*NamedVolume, map[string]*OverlayVolume, error) {
|
||||||
mounts := make(map[string]spec.Mount)
|
mounts := make(map[string]spec.Mount)
|
||||||
|
@ -790,6 +790,9 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions
|
|||||||
if len(s.ImageVolumes) == 0 {
|
if len(s.ImageVolumes) == 0 {
|
||||||
s.ImageVolumes = containerMounts.imageVolumes
|
s.ImageVolumes = containerMounts.imageVolumes
|
||||||
}
|
}
|
||||||
|
if len(s.ArtifactVolumes) == 0 {
|
||||||
|
s.ArtifactVolumes = containerMounts.artifactVolumes
|
||||||
|
}
|
||||||
|
|
||||||
devices := c.Devices
|
devices := c.Devices
|
||||||
for _, gpu := range c.GPUs {
|
for _, gpu := range c.GPUs {
|
||||||
|
@ -23,17 +23,19 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type containerMountSlice struct {
|
type containerMountSlice struct {
|
||||||
mounts []spec.Mount
|
mounts []spec.Mount
|
||||||
volumes []*specgen.NamedVolume
|
volumes []*specgen.NamedVolume
|
||||||
overlayVolumes []*specgen.OverlayVolume
|
overlayVolumes []*specgen.OverlayVolume
|
||||||
imageVolumes []*specgen.ImageVolume
|
imageVolumes []*specgen.ImageVolume
|
||||||
|
artifactVolumes []*specgen.ArtifactVolume
|
||||||
}
|
}
|
||||||
|
|
||||||
// containerMountMap contains the container mounts with the destination path as map key
|
// containerMountMap contains the container mounts with the destination path as map key
|
||||||
type containerMountMap struct {
|
type containerMountMap struct {
|
||||||
mounts map[string]spec.Mount
|
mounts map[string]spec.Mount
|
||||||
volumes map[string]*specgen.NamedVolume
|
volumes map[string]*specgen.NamedVolume
|
||||||
imageVolumes map[string]*specgen.ImageVolume
|
imageVolumes map[string]*specgen.ImageVolume
|
||||||
|
artifactVolumes map[string]*specgen.ArtifactVolume
|
||||||
}
|
}
|
||||||
|
|
||||||
type universalMount struct {
|
type universalMount struct {
|
||||||
@ -131,6 +133,11 @@ func parseVolumes(rtc *config.Config, volumeFlag, mountFlag, tmpfsFlag []string)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for dest := range unifiedContainerMounts.artifactVolumes {
|
||||||
|
if err := testAndSet(dest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Final step: maps to arrays
|
// Final step: maps to arrays
|
||||||
finalMounts := make([]spec.Mount, 0, len(unifiedContainerMounts.mounts))
|
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 {
|
for _, volume := range unifiedContainerMounts.imageVolumes {
|
||||||
finalImageVolumes = append(finalImageVolumes, volume)
|
finalImageVolumes = append(finalImageVolumes, volume)
|
||||||
}
|
}
|
||||||
|
finalArtifactVolumes := make([]*specgen.ArtifactVolume, 0, len(unifiedContainerMounts.artifactVolumes))
|
||||||
|
for _, volume := range unifiedContainerMounts.artifactVolumes {
|
||||||
|
finalArtifactVolumes = append(finalArtifactVolumes, volume)
|
||||||
|
}
|
||||||
|
|
||||||
return &containerMountSlice{
|
return &containerMountSlice{
|
||||||
mounts: finalMounts,
|
mounts: finalMounts,
|
||||||
volumes: finalVolumes,
|
volumes: finalVolumes,
|
||||||
overlayVolumes: finalOverlayVolume,
|
overlayVolumes: finalOverlayVolume,
|
||||||
imageVolumes: finalImageVolumes,
|
imageVolumes: finalImageVolumes,
|
||||||
|
artifactVolumes: finalArtifactVolumes,
|
||||||
}, nil
|
}, 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=bind,src=/etc/resolv.conf,target=/etc/resolv.conf ...
|
||||||
// podman run --mount type=tmpfs,target=/dev/shm ...
|
// podman run --mount type=tmpfs,target=/dev/shm ...
|
||||||
// podman run --mount type=volume,source=test-volume, ...
|
// podman run --mount type=volume,source=test-volume, ...
|
||||||
|
// podman run --mount type=artifact,source=$artifact,dest=...
|
||||||
func mounts(mountFlag []string, configMounts []string) (*containerMountMap, error) {
|
func mounts(mountFlag []string, configMounts []string) (*containerMountMap, error) {
|
||||||
finalMounts := make(map[string]spec.Mount)
|
finalMounts := make(map[string]spec.Mount)
|
||||||
finalNamedVolumes := make(map[string]*specgen.NamedVolume)
|
finalNamedVolumes := make(map[string]*specgen.NamedVolume)
|
||||||
finalImageVolumes := make(map[string]*specgen.ImageVolume)
|
finalImageVolumes := make(map[string]*specgen.ImageVolume)
|
||||||
|
finalArtifactVolumes := make(map[string]*specgen.ArtifactVolume)
|
||||||
parseMounts := func(mounts []string, ignoreDup bool) error {
|
parseMounts := func(mounts []string, ignoreDup bool) error {
|
||||||
for _, mount := range mounts {
|
for _, mount := range mounts {
|
||||||
// TODO: Docker defaults to "volume" if no mount type is specified.
|
// 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)
|
return fmt.Errorf("%v: %w", volume.Destination, specgen.ErrDuplicateDest)
|
||||||
}
|
}
|
||||||
finalImageVolumes[volume.Destination] = volume
|
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":
|
case "volume":
|
||||||
volume, err := getNamedVolume(tokens)
|
volume, err := getNamedVolume(tokens)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -276,9 +302,10 @@ func mounts(mountFlag []string, configMounts []string) (*containerMountMap, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &containerMountMap{
|
return &containerMountMap{
|
||||||
mounts: finalMounts,
|
mounts: finalMounts,
|
||||||
volumes: finalNamedVolumes,
|
volumes: finalNamedVolumes,
|
||||||
imageVolumes: finalImageVolumes,
|
imageVolumes: finalImageVolumes,
|
||||||
|
artifactVolumes: finalArtifactVolumes,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -683,6 +710,50 @@ func getImageVolume(args []string) (*specgen.ImageVolume, error) {
|
|||||||
return newVolume, nil
|
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
|
// GetTmpfsMounts creates spec.Mount structs for user-requested tmpfs mounts
|
||||||
func getTmpfsMounts(tmpfsFlag []string) (map[string]spec.Mount, error) {
|
func getTmpfsMounts(tmpfsFlag []string) (map[string]spec.Mount, error) {
|
||||||
m := make(map[string]spec.Mount)
|
m := make(map[string]spec.Mount)
|
||||||
|
223
test/e2e/artifact_mount_test.go
Normal file
223
test/e2e/artifact_mount_test.go
Normal file
@ -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"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
Reference in New Issue
Block a user