Merge pull request #25397 from Luap99/artifact-mount
add artifact mount support
This commit is contained in:
		
						commit
						6e34514553
					
				|  | @ -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` | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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.
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -34,6 +34,7 @@ import ( | |||
| 	"github.com/containers/podman/v5/libpod/shutdown" | ||||
| 	"github.com/containers/podman/v5/pkg/domain/entities" | ||||
| 	"github.com/containers/podman/v5/pkg/domain/entities/reports" | ||||
| 	artStore "github.com/containers/podman/v5/pkg/libartifact/store" | ||||
| 	"github.com/containers/podman/v5/pkg/rootless" | ||||
| 	"github.com/containers/podman/v5/pkg/systemd" | ||||
| 	"github.com/containers/podman/v5/pkg/util" | ||||
|  | @ -83,6 +84,9 @@ type Runtime struct { | |||
| 	libimageEventsShutdown chan bool | ||||
| 	lockManager            lock.Manager | ||||
| 
 | ||||
| 	// ArtifactStore returns the artifact store created from the runtime.
 | ||||
| 	ArtifactStore func() (*artStore.ArtifactStore, error) | ||||
| 
 | ||||
| 	// Worker
 | ||||
| 	workerChannel chan func() | ||||
| 	workerGroup   sync.WaitGroup | ||||
|  | @ -533,6 +537,11 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) { | |||
| 		} | ||||
| 		runtime.config.Network.NetworkBackend = string(netBackend) | ||||
| 		runtime.network = netInterface | ||||
| 
 | ||||
| 		// Using sync once value to only init the store exactly once and only when it will be actually be used.
 | ||||
| 		runtime.ArtifactStore = sync.OnceValues(func() (*artStore.ArtifactStore, error) { | ||||
| 			return artStore.NewArtifactStore(filepath.Join(runtime.storageConfig.GraphRoot, "artifacts"), runtime.SystemContext()) | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	// We now need to see if the system has restarted
 | ||||
|  |  | |||
|  | @ -5,22 +5,16 @@ package abi | |||
| import ( | ||||
| 	"context" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/containers/common/libimage" | ||||
| 	"github.com/containers/podman/v5/pkg/domain/entities" | ||||
| 	"github.com/containers/podman/v5/pkg/libartifact/store" | ||||
| 	"github.com/containers/podman/v5/pkg/libartifact/types" | ||||
| 	"github.com/opencontainers/go-digest" | ||||
| ) | ||||
| 
 | ||||
| func getDefaultArtifactStore(ir *ImageEngine) string { | ||||
| 	return filepath.Join(ir.Libpod.StorageConfig().GraphRoot, "artifacts") | ||||
| } | ||||
| 
 | ||||
| func (ir *ImageEngine) ArtifactInspect(ctx context.Context, name string, _ entities.ArtifactInspectOptions) (*entities.ArtifactInspectReport, error) { | ||||
| 	artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) | ||||
| 	artStore, err := ir.Libpod.ArtifactStore() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | @ -41,7 +35,7 @@ func (ir *ImageEngine) ArtifactInspect(ctx context.Context, name string, _ entit | |||
| 
 | ||||
| func (ir *ImageEngine) ArtifactList(ctx context.Context, _ entities.ArtifactListOptions) ([]*entities.ArtifactListReport, error) { | ||||
| 	reports := make([]*entities.ArtifactListReport, 0) | ||||
| 	artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) | ||||
| 	artStore, err := ir.Libpod.ArtifactStore() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | @ -80,7 +74,7 @@ func (ir *ImageEngine) ArtifactPull(ctx context.Context, name string, opts entit | |||
| 	if !opts.Quiet && pullOptions.Writer == nil { | ||||
| 		pullOptions.Writer = os.Stderr | ||||
| 	} | ||||
| 	artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) | ||||
| 	artStore, err := ir.Libpod.ArtifactStore() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | @ -91,8 +85,7 @@ func (ir *ImageEngine) ArtifactRm(ctx context.Context, name string, opts entitie | |||
| 	var ( | ||||
| 		namesOrDigests []string | ||||
| 	) | ||||
| 	artifactDigests := make([]*digest.Digest, 0, len(namesOrDigests)) | ||||
| 	artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) | ||||
| 	artStore, err := ir.Libpod.ArtifactStore() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | @ -117,6 +110,7 @@ func (ir *ImageEngine) ArtifactRm(ctx context.Context, name string, opts entitie | |||
| 		namesOrDigests = append(namesOrDigests, name) | ||||
| 	} | ||||
| 
 | ||||
| 	artifactDigests := make([]*digest.Digest, 0, len(namesOrDigests)) | ||||
| 	for _, namesOrDigest := range namesOrDigests { | ||||
| 		artifactDigest, err := artStore.Remove(ctx, namesOrDigest) | ||||
| 		if err != nil { | ||||
|  | @ -133,7 +127,7 @@ func (ir *ImageEngine) ArtifactRm(ctx context.Context, name string, opts entitie | |||
| func (ir *ImageEngine) ArtifactPush(ctx context.Context, name string, opts entities.ArtifactPushOptions) (*entities.ArtifactPushReport, error) { | ||||
| 	var retryDelay *time.Duration | ||||
| 
 | ||||
| 	artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) | ||||
| 	artStore, err := ir.Libpod.ArtifactStore() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | @ -189,7 +183,7 @@ func (ir *ImageEngine) ArtifactPush(ctx context.Context, name string, opts entit | |||
| 	return &entities.ArtifactPushReport{}, err | ||||
| } | ||||
| func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, paths []string, opts *entities.ArtifactAddOptions) (*entities.ArtifactAddReport, error) { | ||||
| 	artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) | ||||
| 	artStore, err := ir.Libpod.ArtifactStore() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | @ -210,13 +204,15 @@ func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, paths []str | |||
| } | ||||
| 
 | ||||
| func (ir *ImageEngine) ArtifactExtract(ctx context.Context, name string, target string, opts *entities.ArtifactExtractOptions) error { | ||||
| 	artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) | ||||
| 	artStore, err := ir.Libpod.ArtifactStore() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	extractOpt := &types.ExtractOptions{ | ||||
| 		Digest: opts.Digest, | ||||
| 		Title:  opts.Title, | ||||
| 		FilterBlobOptions: types.FilterBlobOptions{ | ||||
| 			Digest: opts.Digest, | ||||
| 			Title:  opts.Title, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	return artStore.Extract(ctx, name, target, extractOpt) | ||||
|  |  | |||
|  | @ -47,6 +47,10 @@ func NewArtifactStore(storePath string, sc *types.SystemContext) (*ArtifactStore | |||
| 	if storePath == "" { | ||||
| 		return nil, errors.New("store path cannot be empty") | ||||
| 	} | ||||
| 	if !filepath.IsAbs(storePath) { | ||||
| 		return nil, fmt.Errorf("store path %q must be absolute", storePath) | ||||
| 	} | ||||
| 
 | ||||
| 	logrus.Debugf("Using artifact store path: %s", storePath) | ||||
| 
 | ||||
| 	artifactStore := &ArtifactStore{ | ||||
|  | @ -312,23 +316,22 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, op | |||
| 	return &artifactManifestDigest, nil | ||||
| } | ||||
| 
 | ||||
| // Inspect an artifact in a local store
 | ||||
| func (as ArtifactStore) Extract(ctx context.Context, nameOrDigest string, target string, options *libartTypes.ExtractOptions) error { | ||||
| func getArtifactAndImageSource(ctx context.Context, as ArtifactStore, nameOrDigest string, options *libartTypes.FilterBlobOptions) (*libartifact.Artifact, types.ImageSource, error) { | ||||
| 	if len(options.Digest) > 0 && len(options.Title) > 0 { | ||||
| 		return errors.New("cannot specify both digest and title") | ||||
| 		return nil, nil, errors.New("cannot specify both digest and title") | ||||
| 	} | ||||
| 	if len(nameOrDigest) == 0 { | ||||
| 		return ErrEmptyArtifactName | ||||
| 		return nil, nil, ErrEmptyArtifactName | ||||
| 	} | ||||
| 
 | ||||
| 	artifacts, err := as.getArtifacts(ctx, nil) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	arty, nameIsDigest, err := artifacts.GetByNameOrDigest(nameOrDigest) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| 	name := nameOrDigest | ||||
| 	if nameIsDigest { | ||||
|  | @ -336,14 +339,70 @@ func (as ArtifactStore) Extract(ctx context.Context, nameOrDigest string, target | |||
| 	} | ||||
| 
 | ||||
| 	if len(arty.Manifest.Layers) == 0 { | ||||
| 		return fmt.Errorf("the artifact has no blobs, nothing to extract") | ||||
| 		return nil, nil, fmt.Errorf("the artifact has no blobs, nothing to extract") | ||||
| 	} | ||||
| 
 | ||||
| 	ir, err := layout.NewReference(as.storePath, name) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| 	imgSrc, err := ir.NewImageSource(ctx, as.SystemContext) | ||||
| 	return arty, imgSrc, err | ||||
| } | ||||
| 
 | ||||
| // BlobMountPaths allows the caller to access the file names from the store and how they should be mounted.
 | ||||
| func (as ArtifactStore) BlobMountPaths(ctx context.Context, nameOrDigest string, options *libartTypes.BlobMountPathOptions) ([]libartTypes.BlobMountPath, error) { | ||||
| 	arty, imgSrc, err := getArtifactAndImageSource(ctx, as, nameOrDigest, &options.FilterBlobOptions) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer imgSrc.Close() | ||||
| 
 | ||||
| 	if len(options.Digest) > 0 || len(options.Title) > 0 { | ||||
| 		digest, err := findDigest(arty, &options.FilterBlobOptions) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		// In case the digest is set we always use it as target name
 | ||||
| 		// so we do not have to get the actual title annotation form the blob.
 | ||||
| 		// Passing options.Title is enough because we know it is empty when digest
 | ||||
| 		// is set as we only allow either one.
 | ||||
| 		filename, err := generateArtifactBlobName(options.Title, digest) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		path, err := layout.GetLocalBlobPath(ctx, imgSrc, digest) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		return []libartTypes.BlobMountPath{{ | ||||
| 			SourcePath: path, | ||||
| 			Name:       filename, | ||||
| 		}}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	mountPaths := make([]libartTypes.BlobMountPath, 0, len(arty.Manifest.Layers)) | ||||
| 	for _, l := range arty.Manifest.Layers { | ||||
| 		title := l.Annotations[specV1.AnnotationTitle] | ||||
| 		filename, err := generateArtifactBlobName(title, l.Digest) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		path, err := layout.GetLocalBlobPath(ctx, imgSrc, l.Digest) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		mountPaths = append(mountPaths, libartTypes.BlobMountPath{ | ||||
| 			SourcePath: path, | ||||
| 			Name:       filename, | ||||
| 		}) | ||||
| 	} | ||||
| 	return mountPaths, nil | ||||
| } | ||||
| 
 | ||||
| // Extract an artifact to local file or directory
 | ||||
| func (as ArtifactStore) Extract(ctx context.Context, nameOrDigest string, target string, options *libartTypes.ExtractOptions) error { | ||||
| 	arty, imgSrc, err := getArtifactAndImageSource(ctx, as, nameOrDigest, &options.FilterBlobOptions) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | @ -364,7 +423,7 @@ func (as ArtifactStore) Extract(ctx context.Context, nameOrDigest string, target | |||
| 			if len(options.Digest) == 0 && len(options.Title) == 0 { | ||||
| 				return fmt.Errorf("the artifact consists of several blobs and the target %q is not a directory and neither digest or title was specified to only copy a single blob", target) | ||||
| 			} | ||||
| 			digest, err = findDigest(arty, options) | ||||
| 			digest, err = findDigest(arty, &options.FilterBlobOptions) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | @ -376,7 +435,7 @@ func (as ArtifactStore) Extract(ctx context.Context, nameOrDigest string, target | |||
| 	} | ||||
| 
 | ||||
| 	if len(options.Digest) > 0 || len(options.Title) > 0 { | ||||
| 		digest, err := findDigest(arty, options) | ||||
| 		digest, err := findDigest(arty, &options.FilterBlobOptions) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | @ -427,7 +486,7 @@ func generateArtifactBlobName(title string, digest digest.Digest) (string, error | |||
| 	return filename, nil | ||||
| } | ||||
| 
 | ||||
| func findDigest(arty *libartifact.Artifact, options *libartTypes.ExtractOptions) (digest.Digest, error) { | ||||
| func findDigest(arty *libartifact.Artifact, options *libartTypes.FilterBlobOptions) (digest.Digest, error) { | ||||
| 	var digest digest.Digest | ||||
| 	for _, l := range arty.Manifest.Layers { | ||||
| 		if options.Digest == l.Digest.String() { | ||||
|  |  | |||
|  | @ -12,9 +12,28 @@ type AddOptions struct { | |||
| 	Append bool `json:",omitempty"` | ||||
| } | ||||
| 
 | ||||
| type ExtractOptions struct { | ||||
| 	// Title annotation value to extract only a single blob matching that name. Optional.
 | ||||
| // FilterBlobOptions options used to filter for a single blob in an artifact
 | ||||
| type FilterBlobOptions struct { | ||||
| 	// Title annotation value to extract only a single blob matching that name.
 | ||||
| 	// Optional. Conflicts with Digest.
 | ||||
| 	Title string | ||||
| 	// Digest of the blob to extract. Optional.
 | ||||
| 	// Digest of the blob to extract.
 | ||||
| 	// Optional. Conflicts with Title.
 | ||||
| 	Digest string | ||||
| } | ||||
| 
 | ||||
| type ExtractOptions struct { | ||||
| 	FilterBlobOptions | ||||
| } | ||||
| 
 | ||||
| type BlobMountPathOptions struct { | ||||
| 	FilterBlobOptions | ||||
| } | ||||
| 
 | ||||
| // BlobMountPath contains the info on how the artifact must be mounted
 | ||||
| type BlobMountPath struct { | ||||
| 	// Source path of the blob, i.e. full path in the blob dir.
 | ||||
| 	SourcePath string | ||||
| 	// Name of the file in the container.
 | ||||
| 	Name string | ||||
| } | ||||
|  |  | |||
|  | @ -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)) | ||||
| 	} | ||||
|  |  | |||
|  | @ -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"` | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -762,15 +762,15 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions | |||
| 
 | ||||
| 	// Only add read-only tmpfs mounts in case that we are read-only and the
 | ||||
| 	// read-only tmpfs flag has been set.
 | ||||
| 	mounts, volumes, overlayVolumes, imageVolumes, err := parseVolumes(rtc, c.Volume, c.Mount, c.TmpFS) | ||||
| 	containerMounts, err := parseVolumes(rtc, c.Volume, c.Mount, c.TmpFS) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if len(s.Mounts) == 0 || len(c.Mount) != 0 { | ||||
| 		s.Mounts = mounts | ||||
| 		s.Mounts = containerMounts.mounts | ||||
| 	} | ||||
| 	if len(s.Volumes) == 0 || len(c.Volume) != 0 { | ||||
| 		s.Volumes = volumes | ||||
| 		s.Volumes = containerMounts.volumes | ||||
| 	} | ||||
| 
 | ||||
| 	if s.LabelNested != nil && *s.LabelNested { | ||||
|  | @ -785,10 +785,13 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions | |||
| 	} | ||||
| 	// TODO make sure these work in clone
 | ||||
| 	if len(s.OverlayVolumes) == 0 { | ||||
| 		s.OverlayVolumes = overlayVolumes | ||||
| 		s.OverlayVolumes = containerMounts.overlayVolumes | ||||
| 	} | ||||
| 	if len(s.ImageVolumes) == 0 { | ||||
| 		s.ImageVolumes = imageVolumes | ||||
| 		s.ImageVolumes = containerMounts.imageVolumes | ||||
| 	} | ||||
| 	if len(s.ArtifactVolumes) == 0 { | ||||
| 		s.ArtifactVolumes = containerMounts.artifactVolumes | ||||
| 	} | ||||
| 
 | ||||
| 	devices := c.Devices | ||||
|  |  | |||
|  | @ -22,6 +22,22 @@ var ( | |||
| 	errNoDest    = errors.New("must set volume destination") | ||||
| ) | ||||
| 
 | ||||
| type containerMountSlice struct { | ||||
| 	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 | ||||
| 	artifactVolumes map[string]*specgen.ArtifactVolume | ||||
| } | ||||
| 
 | ||||
| type universalMount struct { | ||||
| 	mount spec.Mount | ||||
| 	// Used only with Named Volume type mounts
 | ||||
|  | @ -34,57 +50,57 @@ type universalMount struct { | |||
| // Does not handle image volumes, init, and --volumes-from flags.
 | ||||
| // Can also add tmpfs mounts from read-only tmpfs.
 | ||||
| // TODO: handle options parsing/processing via containers/storage/pkg/mount
 | ||||
| func parseVolumes(rtc *config.Config, volumeFlag, mountFlag, tmpfsFlag []string) ([]spec.Mount, []*specgen.NamedVolume, []*specgen.OverlayVolume, []*specgen.ImageVolume, error) { | ||||
| func parseVolumes(rtc *config.Config, volumeFlag, mountFlag, tmpfsFlag []string) (*containerMountSlice, error) { | ||||
| 	// Get mounts from the --mounts flag.
 | ||||
| 	// TODO: The runtime config part of this needs to move into pkg/specgen/generate to avoid querying containers.conf on the client.
 | ||||
| 	unifiedMounts, unifiedVolumes, unifiedImageVolumes, err := Mounts(mountFlag, rtc.Mounts()) | ||||
| 	unifiedContainerMounts, err := mounts(mountFlag, rtc.Mounts()) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, nil, nil, err | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Next --volumes flag.
 | ||||
| 	volumeMounts, volumeVolumes, overlayVolumes, err := specgen.GenVolumeMounts(volumeFlag) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, nil, nil, err | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Next --tmpfs flag.
 | ||||
| 	tmpfsMounts, err := getTmpfsMounts(tmpfsFlag) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, nil, nil, err | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Unify mounts from --mount, --volume, --tmpfs.
 | ||||
| 	// Start with --volume.
 | ||||
| 	for dest, mount := range volumeMounts { | ||||
| 		if vol, ok := unifiedMounts[dest]; ok { | ||||
| 		if vol, ok := unifiedContainerMounts.mounts[dest]; ok { | ||||
| 			if mount.Source == vol.Source && | ||||
| 				specgen.StringSlicesEqual(vol.Options, mount.Options) { | ||||
| 				continue | ||||
| 			} | ||||
| 			return nil, nil, nil, nil, fmt.Errorf("%v: %w", dest, specgen.ErrDuplicateDest) | ||||
| 			return nil, fmt.Errorf("%v: %w", dest, specgen.ErrDuplicateDest) | ||||
| 		} | ||||
| 		unifiedMounts[dest] = mount | ||||
| 		unifiedContainerMounts.mounts[dest] = mount | ||||
| 	} | ||||
| 	for dest, volume := range volumeVolumes { | ||||
| 		if vol, ok := unifiedVolumes[dest]; ok { | ||||
| 		if vol, ok := unifiedContainerMounts.volumes[dest]; ok { | ||||
| 			if volume.Name == vol.Name && | ||||
| 				specgen.StringSlicesEqual(vol.Options, volume.Options) { | ||||
| 				continue | ||||
| 			} | ||||
| 			return nil, nil, nil, nil, fmt.Errorf("%v: %w", dest, specgen.ErrDuplicateDest) | ||||
| 			return nil, fmt.Errorf("%v: %w", dest, specgen.ErrDuplicateDest) | ||||
| 		} | ||||
| 		unifiedVolumes[dest] = volume | ||||
| 		unifiedContainerMounts.volumes[dest] = volume | ||||
| 	} | ||||
| 	// Now --tmpfs
 | ||||
| 	for dest, tmpfs := range tmpfsMounts { | ||||
| 		if vol, ok := unifiedMounts[dest]; ok { | ||||
| 		if vol, ok := unifiedContainerMounts.mounts[dest]; ok { | ||||
| 			if vol.Type != define.TypeTmpfs { | ||||
| 				return nil, nil, nil, nil, fmt.Errorf("%v: %w", dest, specgen.ErrDuplicateDest) | ||||
| 				return nil, fmt.Errorf("%v: %w", dest, specgen.ErrDuplicateDest) | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
| 		unifiedMounts[dest] = tmpfs | ||||
| 		unifiedContainerMounts.mounts[dest] = tmpfs | ||||
| 	} | ||||
| 
 | ||||
| 	// Check for conflicts between named volumes, overlay & image volumes,
 | ||||
|  | @ -97,64 +113,81 @@ func parseVolumes(rtc *config.Config, volumeFlag, mountFlag, tmpfsFlag []string) | |||
| 		allMounts[dest] = true | ||||
| 		return nil | ||||
| 	} | ||||
| 	for dest := range unifiedMounts { | ||||
| 	for dest := range unifiedContainerMounts.mounts { | ||||
| 		if err := testAndSet(dest); err != nil { | ||||
| 			return nil, nil, nil, nil, err | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 	for dest := range unifiedVolumes { | ||||
| 	for dest := range unifiedContainerMounts.volumes { | ||||
| 		if err := testAndSet(dest); err != nil { | ||||
| 			return nil, nil, nil, nil, err | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 	for dest := range overlayVolumes { | ||||
| 		if err := testAndSet(dest); err != nil { | ||||
| 			return nil, nil, nil, nil, err | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 	for dest := range unifiedImageVolumes { | ||||
| 	for dest := range unifiedContainerMounts.imageVolumes { | ||||
| 		if err := testAndSet(dest); err != nil { | ||||
| 			return nil, nil, nil, nil, err | ||||
| 			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(unifiedMounts)) | ||||
| 	for _, mount := range unifiedMounts { | ||||
| 	finalMounts := make([]spec.Mount, 0, len(unifiedContainerMounts.mounts)) | ||||
| 	for _, mount := range unifiedContainerMounts.mounts { | ||||
| 		if mount.Type == define.TypeBind { | ||||
| 			absSrc, err := specgen.ConvertWinMountPath(mount.Source) | ||||
| 			if err != nil { | ||||
| 				return nil, nil, nil, nil, fmt.Errorf("getting absolute path of %s: %w", mount.Source, err) | ||||
| 				return nil, fmt.Errorf("getting absolute path of %s: %w", mount.Source, err) | ||||
| 			} | ||||
| 			mount.Source = absSrc | ||||
| 		} | ||||
| 		finalMounts = append(finalMounts, mount) | ||||
| 	} | ||||
| 	finalVolumes := make([]*specgen.NamedVolume, 0, len(unifiedVolumes)) | ||||
| 	for _, volume := range unifiedVolumes { | ||||
| 	finalVolumes := make([]*specgen.NamedVolume, 0, len(unifiedContainerMounts.volumes)) | ||||
| 	for _, volume := range unifiedContainerMounts.volumes { | ||||
| 		finalVolumes = append(finalVolumes, volume) | ||||
| 	} | ||||
| 	finalOverlayVolume := make([]*specgen.OverlayVolume, 0) | ||||
| 	finalOverlayVolume := make([]*specgen.OverlayVolume, 0, len(overlayVolumes)) | ||||
| 	for _, volume := range overlayVolumes { | ||||
| 		finalOverlayVolume = append(finalOverlayVolume, volume) | ||||
| 	} | ||||
| 	finalImageVolumes := make([]*specgen.ImageVolume, 0, len(unifiedImageVolumes)) | ||||
| 	for _, volume := range unifiedImageVolumes { | ||||
| 	finalImageVolumes := make([]*specgen.ImageVolume, 0, len(unifiedContainerMounts.imageVolumes)) | ||||
| 	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 finalMounts, finalVolumes, finalOverlayVolume, finalImageVolumes, nil | ||||
| 	return &containerMountSlice{ | ||||
| 		mounts:          finalMounts, | ||||
| 		volumes:         finalVolumes, | ||||
| 		overlayVolumes:  finalOverlayVolume, | ||||
| 		imageVolumes:    finalImageVolumes, | ||||
| 		artifactVolumes: finalArtifactVolumes, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // Mounts takes user-provided input from the --mount flag as well as Mounts
 | ||||
| // mounts takes user-provided input from the --mount flag as well as mounts
 | ||||
| // specified in containers.conf and creates OCI spec mounts and Libpod named volumes.
 | ||||
| // 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, ...
 | ||||
| func Mounts(mountFlag []string, configMounts []string) (map[string]spec.Mount, map[string]*specgen.NamedVolume, map[string]*specgen.ImageVolume, error) { | ||||
| // 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.
 | ||||
|  | @ -225,6 +258,18 @@ func Mounts(mountFlag []string, configMounts []string) (map[string]spec.Mount, m | |||
| 					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 { | ||||
|  | @ -246,17 +291,22 @@ func Mounts(mountFlag []string, configMounts []string) (map[string]spec.Mount, m | |||
| 
 | ||||
| 	// Parse mounts passed in from the user
 | ||||
| 	if err := parseMounts(mountFlag, false); err != nil { | ||||
| 		return nil, nil, nil, err | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// If user specified a mount flag that conflicts with a containers.conf flag, then ignore
 | ||||
| 	// the duplicate. This means that the parsing of the containers.conf configMounts should always
 | ||||
| 	// happen second.
 | ||||
| 	if err := parseMounts(configMounts, true); err != nil { | ||||
| 		return nil, nil, nil, fmt.Errorf("parsing containers.conf mounts: %w", err) | ||||
| 		return nil, fmt.Errorf("parsing containers.conf mounts: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return finalMounts, finalNamedVolumes, finalImageVolumes, nil | ||||
| 	return &containerMountMap{ | ||||
| 		mounts:          finalMounts, | ||||
| 		volumes:         finalNamedVolumes, | ||||
| 		imageVolumes:    finalImageVolumes, | ||||
| 		artifactVolumes: finalArtifactVolumes, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func parseMountOptions(mountType string, args []string) (*universalMount, error) { | ||||
|  | @ -660,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) | ||||
|  |  | |||
|  | @ -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")) | ||||
| 		} | ||||
| 	}) | ||||
| }) | ||||
|  | @ -475,7 +475,7 @@ var _ = Describe("Podman artifact", func() { | |||
| 		a := podmanTest.InspectArtifact(artifact1Name) | ||||
| 
 | ||||
| 		Expect(a.Manifest.Layers).To(HaveLen(1)) | ||||
| 		Expect(a.TotalSizeBytes()).To(Equal(int64(524288))) | ||||
| 		Expect(a.TotalSizeBytes()).To(Equal(int64(1024))) | ||||
| 	}) | ||||
| 
 | ||||
| 	It("podman artifact add file already exists in artifact", func() { | ||||
|  | @ -506,7 +506,7 @@ var _ = Describe("Podman artifact", func() { | |||
| 		a := podmanTest.InspectArtifact(artifact1Name) | ||||
| 
 | ||||
| 		Expect(a.Manifest.Layers).To(HaveLen(1)) | ||||
| 		Expect(a.TotalSizeBytes()).To(Equal(int64(1048576))) | ||||
| 		Expect(a.TotalSizeBytes()).To(Equal(int64(2048))) | ||||
| 	}) | ||||
| 
 | ||||
| 	It("podman artifact add with --append and --type", func() { | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ package integration | |||
| import ( | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	crand "crypto/rand" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
|  | @ -1390,6 +1391,7 @@ func writeYaml(content string, fileName string) error { | |||
| func GetPort() int { | ||||
| 	portMin := 5000 | ||||
| 	portMax := 5999 | ||||
| 
 | ||||
| 	rng := rand.New(rand.NewSource(time.Now().UnixNano())) | ||||
| 
 | ||||
| 	// Avoid dup-allocation races between parallel ginkgo processes
 | ||||
|  | @ -1617,16 +1619,22 @@ func setupRegistry(portOverride *int) (*lockfile.LockFile, string, error) { | |||
| } | ||||
| 
 | ||||
| func createArtifactFile(numBytes int64) (string, error) { | ||||
| 	GinkgoHelper() | ||||
| 	artifactDir := filepath.Join(podmanTest.TempDir, "artifacts") | ||||
| 	if err := os.MkdirAll(artifactDir, 0755); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	filename := RandomString(8) | ||||
| 	outFile := filepath.Join(artifactDir, filename) | ||||
| 	session := podmanTest.Podman([]string{"run", "-v", fmt.Sprintf("%s:/artifacts:z", artifactDir), ALPINE, "dd", "if=/dev/urandom", fmt.Sprintf("of=%s", filepath.Join("/artifacts", filename)), "bs=1b", fmt.Sprintf("count=%d", numBytes)}) | ||||
| 	session.WaitWithDefaultTimeout() | ||||
| 	if session.ExitCode() != 0 { | ||||
| 		return "", errors.New("unable to generate artifact file") | ||||
| 
 | ||||
| 	f, err := os.Create(filepath.Join(artifactDir, filename)) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	defer f.Close() | ||||
| 	_, err = io.CopyN(f, crand.Reader, numBytes) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return outFile, nil | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue