Skip to content

Commit 9e94dc5

Browse files
committed
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 <[email protected]>
1 parent f6e2d94 commit 9e94dc5

File tree

13 files changed

+490
-17
lines changed

13 files changed

+490
-17
lines changed

docs/source/markdown/options/mount.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66

77
Attach a filesystem mount to the container
88

9-
Current supported mount TYPEs are **bind**, **devpts**, **glob**, **image**, **ramfs**, **tmpfs** and **volume**.
9+
Current supported mount TYPEs are **artifact**, **bind**, **devpts**, **glob**, **image**, **ramfs**, **tmpfs** and **volume**.
1010

1111
Options common to all mount types:
1212

1313
- *src*, *source*: mount source spec for **bind**, **glob**, and **volume**.
14-
Mandatory for **bind** and **glob**.
14+
Mandatory for **artifact**, **bind**, **glob**, **image** and **volume**.
1515

1616
- *dst*, *destination*, *target*: mount destination spec.
1717

@@ -24,6 +24,25 @@ on the destination directory are mounted. The option
2424
to mount host files matching /foo* to the /tmp/bar/
2525
directory in the container.
2626

27+
Options specific to type=**artifact**:
28+
29+
- *digest*: If the artifact source contains multiple blobs a digest can be
30+
specified to only mount the one specific blob with the digest.
31+
32+
- *title*: If the artifact source contains multiple blobs a title can be set
33+
which is compared against `org.opencontainers.image.title` annotation.
34+
35+
The *src* argument contains the name of the artifact, it must already exist locally.
36+
The *dst* argument contains the target path, if the path in the container is a
37+
directory or does not exist the blob title (`org.opencontainers.image.title`
38+
annotation) will be used as filename and joined to the path. If the annotation
39+
does not exist the digest will be used as filename instead. This results in all blobs
40+
of the artifact mounted into the container at the given path.
41+
42+
However if the *dst* path is a existing file in the container then the blob will be
43+
mounted directly on it. This only works when the artifact contains of a single blob
44+
or when either *digest* or *title* are specified.
45+
2746
Options specific to type=**volume**:
2847

2948
- *ro*, *readonly*: *true* or *false* (default if unspecified: *false*).
@@ -104,4 +123,6 @@ Examples:
104123

105124
- `type=tmpfs,destination=/path/in/container,noswap`
106125

107-
- `type=volume,source=vol1,destination=/path/in/container,ro=true`
126+
- `type=artifact,src=quay.io/libpod/testartifact:20250206-single,dst=/data`
127+
128+
- `type=artifact,src=quay.io/libpod/testartifact:20250206-multi,dst=/data,title=test1`

libpod/container.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,28 @@ type ContainerImageVolume struct {
280280
SubPath string `json:"subPath,omitempty"`
281281
}
282282

283+
// ContainerArtifactVolume is a volume based on a artifact. The artifact blobs will
284+
// be bind mounted directly as files and must always be read only.
285+
type ContainerArtifactVolume struct {
286+
// Source is the name or digest of the artifact that should be mounted
287+
Source string `json:"source"`
288+
// Dest is the absolute path of the mount in the container.
289+
// If path is a file in the container, then the artifact must consist of a single blob.
290+
// Otherwise if it is a directory or does not exists all artifact blobs will be mounted
291+
// into this path as files. As name the "org.opencontainers.image.title" will be used if
292+
// available otherwise the digest is used as name.
293+
Dest string `json:"dest"`
294+
// Title can be used for multi blob artifacts to only mount the one specific blob that
295+
// matches the "org.opencontainers.image.title" annotation.
296+
// Optional. Conflicts with Digest.
297+
Title string `json:"title"`
298+
// Digest can be used to filter a single blob from a multi blob artifact by the given digest.
299+
// When this option is set the file name in the container defaults to the digest even when
300+
// the title annotation exist.
301+
// Optional. Conflicts with Title.
302+
Digest string `json:"digest"`
303+
}
304+
283305
// ContainerSecret is a secret that is mounted in a container
284306
type ContainerSecret struct {
285307
// Secret is the secret

libpod/container_config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ type ContainerRootFSConfig struct {
162162
// moved out of Libpod into pkg/specgen).
163163
// Please DO NOT reuse the `imageVolumes` name in container JSON again.
164164
ImageVolumes []*ContainerImageVolume `json:"ctrImageVolumes,omitempty"`
165+
// ArtifactVolumes lists the artifact volumes to mount into the container.
166+
ArtifactVolumes []*ContainerArtifactVolume `json:"artifactVolumes,omitempty"`
165167
// CreateWorkingDir indicates that Libpod should create the container's
166168
// working directory if it does not exist. Some OCI runtimes do this by
167169
// default, but others do not.

libpod/container_internal_common.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
"github.com/containers/podman/v5/pkg/annotations"
4242
"github.com/containers/podman/v5/pkg/checkpoint/crutils"
4343
"github.com/containers/podman/v5/pkg/criu"
44+
libartTypes "github.com/containers/podman/v5/pkg/libartifact/types"
4445
"github.com/containers/podman/v5/pkg/lookup"
4546
"github.com/containers/podman/v5/pkg/rootless"
4647
"github.com/containers/podman/v5/pkg/util"
@@ -483,6 +484,52 @@ func (c *Container) generateSpec(ctx context.Context) (s *spec.Spec, cleanupFunc
483484
g.AddMount(overlayMount)
484485
}
485486

487+
if len(c.config.ArtifactVolumes) > 0 {
488+
artStore, err := c.runtime.ArtifactStore()
489+
if err != nil {
490+
return nil, nil, err
491+
}
492+
for _, artifactMount := range c.config.ArtifactVolumes {
493+
paths, err := artStore.BlobMountPaths(ctx, artifactMount.Source, &libartTypes.BlobMountPathOptions{
494+
FilterBlobOptions: libartTypes.FilterBlobOptions{
495+
Title: artifactMount.Title,
496+
Digest: artifactMount.Digest,
497+
},
498+
})
499+
if err != nil {
500+
return nil, nil, err
501+
}
502+
503+
// Ignore the error, destIsFile will return false with errors so if the file does not exist
504+
// we treat it as dir, the oci runtime will always create the target bind mount path.
505+
destIsFile, _ := containerPathIsFile(c.state.Mountpoint, artifactMount.Dest)
506+
if destIsFile && len(paths) > 1 {
507+
return nil, nil, fmt.Errorf("artifact %q contains more than one blob and container path %q is a file", artifactMount.Source, artifactMount.Dest)
508+
}
509+
510+
for _, path := range paths {
511+
var dest string
512+
if destIsFile {
513+
dest = artifactMount.Dest
514+
} else {
515+
dest = filepath.Join(artifactMount.Dest, path.Name)
516+
}
517+
518+
logrus.Debugf("Mounting artifact %q in container %s, mount blob %q to %q", artifactMount.Source, c.ID(), path.SourcePath, dest)
519+
520+
g.AddMount(spec.Mount{
521+
Destination: dest,
522+
Source: path.SourcePath,
523+
Type: define.TypeBind,
524+
// Important: This must always be mounted read only here, we are using
525+
// the source in the artifact store directly and because that is digest
526+
// based a write will break the layout.
527+
Options: []string{define.TypeBind, "ro"},
528+
})
529+
}
530+
}
531+
}
532+
486533
err = c.setHomeEnvIfNeeded()
487534
if err != nil {
488535
return nil, nil, err

libpod/container_internal_freebsd.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/containers/common/libnetwork/types"
1515
"github.com/containers/podman/v5/pkg/rootless"
16+
securejoin "github.com/cyphar/filepath-securejoin"
1617
spec "github.com/opencontainers/runtime-spec/specs-go"
1718
"github.com/opencontainers/runtime-tools/generate"
1819
"github.com/sirupsen/logrus"
@@ -415,3 +416,20 @@ func (c *Container) hasPrivateUTS() bool {
415416
func hasCapSysResource() (bool, error) {
416417
return true, nil
417418
}
419+
420+
// containerPathIsFile returns true if the given containerPath is a file
421+
func containerPathIsFile(unsafeRoot string, containerPath string) (bool, error) {
422+
// Note freebsd does not have support for OpenInRoot() so us the less safe way
423+
// with the old SecureJoin(), but given this is only called before the container
424+
// is started it is not subject to race conditions with the container process.
425+
path, err := securejoin.SecureJoin(unsafeRoot, containerPath)
426+
if err != nil {
427+
return false, err
428+
}
429+
430+
st, err := os.Lstat(path)
431+
if err == nil && !st.IsDir() {
432+
return true, nil
433+
}
434+
return false, err
435+
}

libpod/container_internal_linux.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/containers/podman/v5/libpod/define"
2222
"github.com/containers/podman/v5/libpod/shutdown"
2323
"github.com/containers/podman/v5/pkg/rootless"
24+
securejoin "github.com/cyphar/filepath-securejoin"
2425
"github.com/moby/sys/capability"
2526
spec "github.com/opencontainers/runtime-spec/specs-go"
2627
"github.com/opencontainers/runtime-tools/generate"
@@ -848,3 +849,18 @@ var hasCapSysResource = sync.OnceValues(func() (bool, error) {
848849
}
849850
return currentCaps.Get(capability.EFFECTIVE, capability.CAP_SYS_RESOURCE), nil
850851
})
852+
853+
// containerPathIsFile returns true if the given containerPath is a file
854+
func containerPathIsFile(unsafeRoot string, containerPath string) (bool, error) {
855+
f, err := securejoin.OpenInRoot(unsafeRoot, containerPath)
856+
if err != nil {
857+
return false, err
858+
}
859+
defer f.Close()
860+
861+
st, err := f.Stat()
862+
if err == nil && !st.IsDir() {
863+
return true, nil
864+
}
865+
return false, err
866+
}

libpod/options.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,6 +1515,19 @@ func WithImageVolumes(volumes []*ContainerImageVolume) CtrCreateOption {
15151515
}
15161516
}
15171517

1518+
// WithImageVolumes adds the given image volumes to the container.
1519+
func WithArtifactVolumes(volumes []*ContainerArtifactVolume) CtrCreateOption {
1520+
return func(ctr *Container) error {
1521+
if ctr.valid {
1522+
return define.ErrCtrFinalized
1523+
}
1524+
1525+
ctr.config.ArtifactVolumes = volumes
1526+
1527+
return nil
1528+
}
1529+
}
1530+
15181531
// WithHealthCheck adds the healthcheck to the container config
15191532
func WithHealthCheck(healthCheck *manifest.Schema2HealthConfig) CtrCreateOption {
15201533
return func(ctr *Container) error {

pkg/specgen/generate/container_create.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,19 @@ func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator, pod *l
507507
options = append(options, libpod.WithImageVolumes(vols))
508508
}
509509

510+
if len(s.ArtifactVolumes) != 0 {
511+
vols := make([]*libpod.ContainerArtifactVolume, 0, len(s.ArtifactVolumes))
512+
for _, v := range s.ArtifactVolumes {
513+
vols = append(vols, &libpod.ContainerArtifactVolume{
514+
Dest: v.Destination,
515+
Source: v.Source,
516+
Digest: v.Digest,
517+
Title: v.Title,
518+
})
519+
}
520+
options = append(options, libpod.WithArtifactVolumes(vols))
521+
}
522+
510523
if s.Command != nil {
511524
options = append(options, libpod.WithCommand(s.Command))
512525
}

pkg/specgen/specgen.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ type ContainerStorageConfig struct {
305305
// Image volumes bind-mount a container-image mount into the container.
306306
// Optional.
307307
ImageVolumes []*ImageVolume `json:"image_volumes,omitempty"`
308+
// ArtifactVolumes volumes based on an existing artifact.
309+
ArtifactVolumes []*ArtifactVolume `json:"artifact_volumes,omitempty"`
308310
// Devices are devices that will be added to the container.
309311
// Optional.
310312
Devices []spec.LinuxDevice `json:"devices,omitempty"`

pkg/specgen/volumes.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,28 @@ type ImageVolume struct {
5858
SubPath string `json:"subPath,omitempty"`
5959
}
6060

61+
// ArtifactVolume is a volume based on a artifact. The artifact blobs will
62+
// be bind mounted directly as files and must always be read only.
63+
type ArtifactVolume struct {
64+
// Source is the name or digest of the artifact that should be mounted
65+
Source string `json:"source"`
66+
// Destination is the absolute path of the mount in the container.
67+
// If path is a file in the container, then the artifact must consist of a single blob.
68+
// Otherwise if it is a directory or does not exists all artifact blobs will be mounted
69+
// into this path as files. As name the "org.opencontainers.image.title" will be used if
70+
// available otherwise the digest is used as name.
71+
Destination string `json:"destination"`
72+
// Title can be used for multi blob artifacts to only mount the one specific blob that
73+
// matches the "org.opencontainers.image.title" annotation.
74+
// Optional. Conflicts with Digest.
75+
Title string `json:"title,omitempty"`
76+
// Digest can be used to filter a single blob from a multi blob artifact by the given digest.
77+
// When this option is set the file name in the container defaults to the digest even when
78+
// the title annotation exist.
79+
// Optional. Conflicts with Title.
80+
Digest string `json:"digest,omitempty"`
81+
}
82+
6183
// GenVolumeMounts parses user input into mounts, volumes and overlay volumes
6284
func GenVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*NamedVolume, map[string]*OverlayVolume, error) {
6385
mounts := make(map[string]spec.Mount)

pkg/specgenutil/specgen.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,9 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions
790790
if len(s.ImageVolumes) == 0 {
791791
s.ImageVolumes = containerMounts.imageVolumes
792792
}
793+
if len(s.ArtifactVolumes) == 0 {
794+
s.ArtifactVolumes = containerMounts.artifactVolumes
795+
}
793796

794797
devices := c.Devices
795798
for _, gpu := range c.GPUs {

0 commit comments

Comments
 (0)