Skip to content

Commit 6e34514

Browse files
Merge pull request #25397 from Luap99/artifact-mount
add artifact mount support
2 parents ff20289 + c05908a commit 6e34514

File tree

19 files changed

+666
-79
lines changed

19 files changed

+666
-79
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 {

libpod/runtime.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/containers/podman/v5/libpod/shutdown"
3535
"github.com/containers/podman/v5/pkg/domain/entities"
3636
"github.com/containers/podman/v5/pkg/domain/entities/reports"
37+
artStore "github.com/containers/podman/v5/pkg/libartifact/store"
3738
"github.com/containers/podman/v5/pkg/rootless"
3839
"github.com/containers/podman/v5/pkg/systemd"
3940
"github.com/containers/podman/v5/pkg/util"
@@ -83,6 +84,9 @@ type Runtime struct {
8384
libimageEventsShutdown chan bool
8485
lockManager lock.Manager
8586

87+
// ArtifactStore returns the artifact store created from the runtime.
88+
ArtifactStore func() (*artStore.ArtifactStore, error)
89+
8690
// Worker
8791
workerChannel chan func()
8892
workerGroup sync.WaitGroup
@@ -533,6 +537,11 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) {
533537
}
534538
runtime.config.Network.NetworkBackend = string(netBackend)
535539
runtime.network = netInterface
540+
541+
// Using sync once value to only init the store exactly once and only when it will be actually be used.
542+
runtime.ArtifactStore = sync.OnceValues(func() (*artStore.ArtifactStore, error) {
543+
return artStore.NewArtifactStore(filepath.Join(runtime.storageConfig.GraphRoot, "artifacts"), runtime.SystemContext())
544+
})
536545
}
537546

538547
// We now need to see if the system has restarted

pkg/domain/infra/abi/artifact.go

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,16 @@ package abi
55
import (
66
"context"
77
"os"
8-
"path/filepath"
98
"time"
109

1110
"github.com/containers/common/libimage"
1211
"github.com/containers/podman/v5/pkg/domain/entities"
13-
"github.com/containers/podman/v5/pkg/libartifact/store"
1412
"github.com/containers/podman/v5/pkg/libartifact/types"
1513
"github.com/opencontainers/go-digest"
1614
)
1715

18-
func getDefaultArtifactStore(ir *ImageEngine) string {
19-
return filepath.Join(ir.Libpod.StorageConfig().GraphRoot, "artifacts")
20-
}
21-
2216
func (ir *ImageEngine) ArtifactInspect(ctx context.Context, name string, _ entities.ArtifactInspectOptions) (*entities.ArtifactInspectReport, error) {
23-
artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext())
17+
artStore, err := ir.Libpod.ArtifactStore()
2418
if err != nil {
2519
return nil, err
2620
}
@@ -41,7 +35,7 @@ func (ir *ImageEngine) ArtifactInspect(ctx context.Context, name string, _ entit
4135

4236
func (ir *ImageEngine) ArtifactList(ctx context.Context, _ entities.ArtifactListOptions) ([]*entities.ArtifactListReport, error) {
4337
reports := make([]*entities.ArtifactListReport, 0)
44-
artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext())
38+
artStore, err := ir.Libpod.ArtifactStore()
4539
if err != nil {
4640
return nil, err
4741
}
@@ -80,7 +74,7 @@ func (ir *ImageEngine) ArtifactPull(ctx context.Context, name string, opts entit
8074
if !opts.Quiet && pullOptions.Writer == nil {
8175
pullOptions.Writer = os.Stderr
8276
}
83-
artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext())
77+
artStore, err := ir.Libpod.ArtifactStore()
8478
if err != nil {
8579
return nil, err
8680
}
@@ -91,8 +85,7 @@ func (ir *ImageEngine) ArtifactRm(ctx context.Context, name string, opts entitie
9185
var (
9286
namesOrDigests []string
9387
)
94-
artifactDigests := make([]*digest.Digest, 0, len(namesOrDigests))
95-
artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext())
88+
artStore, err := ir.Libpod.ArtifactStore()
9689
if err != nil {
9790
return nil, err
9891
}
@@ -117,6 +110,7 @@ func (ir *ImageEngine) ArtifactRm(ctx context.Context, name string, opts entitie
117110
namesOrDigests = append(namesOrDigests, name)
118111
}
119112

113+
artifactDigests := make([]*digest.Digest, 0, len(namesOrDigests))
120114
for _, namesOrDigest := range namesOrDigests {
121115
artifactDigest, err := artStore.Remove(ctx, namesOrDigest)
122116
if err != nil {
@@ -133,7 +127,7 @@ func (ir *ImageEngine) ArtifactRm(ctx context.Context, name string, opts entitie
133127
func (ir *ImageEngine) ArtifactPush(ctx context.Context, name string, opts entities.ArtifactPushOptions) (*entities.ArtifactPushReport, error) {
134128
var retryDelay *time.Duration
135129

136-
artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext())
130+
artStore, err := ir.Libpod.ArtifactStore()
137131
if err != nil {
138132
return nil, err
139133
}
@@ -189,7 +183,7 @@ func (ir *ImageEngine) ArtifactPush(ctx context.Context, name string, opts entit
189183
return &entities.ArtifactPushReport{}, err
190184
}
191185
func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, paths []string, opts *entities.ArtifactAddOptions) (*entities.ArtifactAddReport, error) {
192-
artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext())
186+
artStore, err := ir.Libpod.ArtifactStore()
193187
if err != nil {
194188
return nil, err
195189
}
@@ -210,13 +204,15 @@ func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, paths []str
210204
}
211205

212206
func (ir *ImageEngine) ArtifactExtract(ctx context.Context, name string, target string, opts *entities.ArtifactExtractOptions) error {
213-
artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext())
207+
artStore, err := ir.Libpod.ArtifactStore()
214208
if err != nil {
215209
return err
216210
}
217211
extractOpt := &types.ExtractOptions{
218-
Digest: opts.Digest,
219-
Title: opts.Title,
212+
FilterBlobOptions: types.FilterBlobOptions{
213+
Digest: opts.Digest,
214+
Title: opts.Title,
215+
},
220216
}
221217

222218
return artStore.Extract(ctx, name, target, extractOpt)

0 commit comments

Comments
 (0)