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 | 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 { | ||||||
|  |  | ||||||
|  | @ -34,6 +34,7 @@ import ( | ||||||
| 	"github.com/containers/podman/v5/libpod/shutdown" | 	"github.com/containers/podman/v5/libpod/shutdown" | ||||||
| 	"github.com/containers/podman/v5/pkg/domain/entities" | 	"github.com/containers/podman/v5/pkg/domain/entities" | ||||||
| 	"github.com/containers/podman/v5/pkg/domain/entities/reports" | 	"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/rootless" | ||||||
| 	"github.com/containers/podman/v5/pkg/systemd" | 	"github.com/containers/podman/v5/pkg/systemd" | ||||||
| 	"github.com/containers/podman/v5/pkg/util" | 	"github.com/containers/podman/v5/pkg/util" | ||||||
|  | @ -83,6 +84,9 @@ type Runtime struct { | ||||||
| 	libimageEventsShutdown chan bool | 	libimageEventsShutdown chan bool | ||||||
| 	lockManager            lock.Manager | 	lockManager            lock.Manager | ||||||
| 
 | 
 | ||||||
|  | 	// ArtifactStore returns the artifact store created from the runtime.
 | ||||||
|  | 	ArtifactStore func() (*artStore.ArtifactStore, error) | ||||||
|  | 
 | ||||||
| 	// Worker
 | 	// Worker
 | ||||||
| 	workerChannel chan func() | 	workerChannel chan func() | ||||||
| 	workerGroup   sync.WaitGroup | 	workerGroup   sync.WaitGroup | ||||||
|  | @ -533,6 +537,11 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) { | ||||||
| 		} | 		} | ||||||
| 		runtime.config.Network.NetworkBackend = string(netBackend) | 		runtime.config.Network.NetworkBackend = string(netBackend) | ||||||
| 		runtime.network = netInterface | 		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
 | 	// We now need to see if the system has restarted
 | ||||||
|  |  | ||||||
|  | @ -5,22 +5,16 @@ package abi | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" |  | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/containers/common/libimage" | 	"github.com/containers/common/libimage" | ||||||
| 	"github.com/containers/podman/v5/pkg/domain/entities" | 	"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/containers/podman/v5/pkg/libartifact/types" | ||||||
| 	"github.com/opencontainers/go-digest" | 	"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) { | 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 { | 	if err != nil { | ||||||
| 		return nil, err | 		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) { | func (ir *ImageEngine) ArtifactList(ctx context.Context, _ entities.ArtifactListOptions) ([]*entities.ArtifactListReport, error) { | ||||||
| 	reports := make([]*entities.ArtifactListReport, 0) | 	reports := make([]*entities.ArtifactListReport, 0) | ||||||
| 	artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) | 	artStore, err := ir.Libpod.ArtifactStore() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | @ -80,7 +74,7 @@ func (ir *ImageEngine) ArtifactPull(ctx context.Context, name string, opts entit | ||||||
| 	if !opts.Quiet && pullOptions.Writer == nil { | 	if !opts.Quiet && pullOptions.Writer == nil { | ||||||
| 		pullOptions.Writer = os.Stderr | 		pullOptions.Writer = os.Stderr | ||||||
| 	} | 	} | ||||||
| 	artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) | 	artStore, err := ir.Libpod.ArtifactStore() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | @ -91,8 +85,7 @@ func (ir *ImageEngine) ArtifactRm(ctx context.Context, name string, opts entitie | ||||||
| 	var ( | 	var ( | ||||||
| 		namesOrDigests []string | 		namesOrDigests []string | ||||||
| 	) | 	) | ||||||
| 	artifactDigests := make([]*digest.Digest, 0, len(namesOrDigests)) | 	artStore, err := ir.Libpod.ArtifactStore() | ||||||
| 	artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | @ -117,6 +110,7 @@ func (ir *ImageEngine) ArtifactRm(ctx context.Context, name string, opts entitie | ||||||
| 		namesOrDigests = append(namesOrDigests, name) | 		namesOrDigests = append(namesOrDigests, name) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	artifactDigests := make([]*digest.Digest, 0, len(namesOrDigests)) | ||||||
| 	for _, namesOrDigest := range namesOrDigests { | 	for _, namesOrDigest := range namesOrDigests { | ||||||
| 		artifactDigest, err := artStore.Remove(ctx, namesOrDigest) | 		artifactDigest, err := artStore.Remove(ctx, namesOrDigest) | ||||||
| 		if err != nil { | 		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) { | func (ir *ImageEngine) ArtifactPush(ctx context.Context, name string, opts entities.ArtifactPushOptions) (*entities.ArtifactPushReport, error) { | ||||||
| 	var retryDelay *time.Duration | 	var retryDelay *time.Duration | ||||||
| 
 | 
 | ||||||
| 	artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) | 	artStore, err := ir.Libpod.ArtifactStore() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | @ -189,7 +183,7 @@ func (ir *ImageEngine) ArtifactPush(ctx context.Context, name string, opts entit | ||||||
| 	return &entities.ArtifactPushReport{}, err | 	return &entities.ArtifactPushReport{}, err | ||||||
| } | } | ||||||
| func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, paths []string, opts *entities.ArtifactAddOptions) (*entities.ArtifactAddReport, error) { | 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 { | 	if err != nil { | ||||||
| 		return nil, err | 		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 { | 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 { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	extractOpt := &types.ExtractOptions{ | 	extractOpt := &types.ExtractOptions{ | ||||||
| 		Digest: opts.Digest, | 		FilterBlobOptions: types.FilterBlobOptions{ | ||||||
| 		Title:  opts.Title, | 			Digest: opts.Digest, | ||||||
|  | 			Title:  opts.Title, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return artStore.Extract(ctx, name, target, extractOpt) | 	return artStore.Extract(ctx, name, target, extractOpt) | ||||||
|  |  | ||||||
|  | @ -47,6 +47,10 @@ func NewArtifactStore(storePath string, sc *types.SystemContext) (*ArtifactStore | ||||||
| 	if storePath == "" { | 	if storePath == "" { | ||||||
| 		return nil, errors.New("store path cannot be empty") | 		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) | 	logrus.Debugf("Using artifact store path: %s", storePath) | ||||||
| 
 | 
 | ||||||
| 	artifactStore := &ArtifactStore{ | 	artifactStore := &ArtifactStore{ | ||||||
|  | @ -312,23 +316,22 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, op | ||||||
| 	return &artifactManifestDigest, nil | 	return &artifactManifestDigest, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Inspect an artifact in a local store
 | func getArtifactAndImageSource(ctx context.Context, as ArtifactStore, nameOrDigest string, options *libartTypes.FilterBlobOptions) (*libartifact.Artifact, types.ImageSource, error) { | ||||||
| func (as ArtifactStore) Extract(ctx context.Context, nameOrDigest string, target string, options *libartTypes.ExtractOptions) error { |  | ||||||
| 	if len(options.Digest) > 0 && len(options.Title) > 0 { | 	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 { | 	if len(nameOrDigest) == 0 { | ||||||
| 		return ErrEmptyArtifactName | 		return nil, nil, ErrEmptyArtifactName | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	artifacts, err := as.getArtifacts(ctx, nil) | 	artifacts, err := as.getArtifacts(ctx, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return nil, nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	arty, nameIsDigest, err := artifacts.GetByNameOrDigest(nameOrDigest) | 	arty, nameIsDigest, err := artifacts.GetByNameOrDigest(nameOrDigest) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return nil, nil, err | ||||||
| 	} | 	} | ||||||
| 	name := nameOrDigest | 	name := nameOrDigest | ||||||
| 	if nameIsDigest { | 	if nameIsDigest { | ||||||
|  | @ -336,14 +339,70 @@ func (as ArtifactStore) Extract(ctx context.Context, nameOrDigest string, target | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if len(arty.Manifest.Layers) == 0 { | 	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) | 	ir, err := layout.NewReference(as.storePath, name) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return nil, nil, err | ||||||
| 	} | 	} | ||||||
| 	imgSrc, err := ir.NewImageSource(ctx, as.SystemContext) | 	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 { | 	if err != nil { | ||||||
| 		return err | 		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 { | 			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) | 				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 { | 			if err != nil { | ||||||
| 				return err | 				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 { | 	if len(options.Digest) > 0 || len(options.Title) > 0 { | ||||||
| 		digest, err := findDigest(arty, options) | 		digest, err := findDigest(arty, &options.FilterBlobOptions) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  | @ -427,7 +486,7 @@ func generateArtifactBlobName(title string, digest digest.Digest) (string, error | ||||||
| 	return filename, nil | 	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 | 	var digest digest.Digest | ||||||
| 	for _, l := range arty.Manifest.Layers { | 	for _, l := range arty.Manifest.Layers { | ||||||
| 		if options.Digest == l.Digest.String() { | 		if options.Digest == l.Digest.String() { | ||||||
|  |  | ||||||
|  | @ -12,9 +12,28 @@ type AddOptions struct { | ||||||
| 	Append bool `json:",omitempty"` | 	Append bool `json:",omitempty"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type ExtractOptions struct { | // FilterBlobOptions options used to filter for a single blob in an artifact
 | ||||||
| 	// Title annotation value to extract only a single blob matching that name. Optional.
 | type FilterBlobOptions struct { | ||||||
|  | 	// Title annotation value to extract only a single blob matching that name.
 | ||||||
|  | 	// Optional. Conflicts with Digest.
 | ||||||
| 	Title string | 	Title string | ||||||
| 	// Digest of the blob to extract. Optional.
 | 	// Digest of the blob to extract.
 | ||||||
|  | 	// Optional. Conflicts with Title.
 | ||||||
| 	Digest string | 	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)) | 		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) | ||||||
|  |  | ||||||
|  | @ -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
 | 	// Only add read-only tmpfs mounts in case that we are read-only and the
 | ||||||
| 	// read-only tmpfs flag has been set.
 | 	// 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 { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	if len(s.Mounts) == 0 || len(c.Mount) != 0 { | 	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 { | 	if len(s.Volumes) == 0 || len(c.Volume) != 0 { | ||||||
| 		s.Volumes = volumes | 		s.Volumes = containerMounts.volumes | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if s.LabelNested != nil && *s.LabelNested { | 	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
 | 	// TODO make sure these work in clone
 | ||||||
| 	if len(s.OverlayVolumes) == 0 { | 	if len(s.OverlayVolumes) == 0 { | ||||||
| 		s.OverlayVolumes = overlayVolumes | 		s.OverlayVolumes = containerMounts.overlayVolumes | ||||||
| 	} | 	} | ||||||
| 	if len(s.ImageVolumes) == 0 { | 	if len(s.ImageVolumes) == 0 { | ||||||
| 		s.ImageVolumes = imageVolumes | 		s.ImageVolumes = containerMounts.imageVolumes | ||||||
|  | 	} | ||||||
|  | 	if len(s.ArtifactVolumes) == 0 { | ||||||
|  | 		s.ArtifactVolumes = containerMounts.artifactVolumes | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	devices := c.Devices | 	devices := c.Devices | ||||||
|  |  | ||||||
|  | @ -22,6 +22,22 @@ var ( | ||||||
| 	errNoDest    = errors.New("must set volume destination") | 	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 { | type universalMount struct { | ||||||
| 	mount spec.Mount | 	mount spec.Mount | ||||||
| 	// Used only with Named Volume type mounts
 | 	// Used only with Named Volume type mounts
 | ||||||
|  | @ -34,57 +50,57 @@ type universalMount struct { | ||||||
| // Does not handle image volumes, init, and --volumes-from flags.
 | // Does not handle image volumes, init, and --volumes-from flags.
 | ||||||
| // Can also add tmpfs mounts from read-only tmpfs.
 | // Can also add tmpfs mounts from read-only tmpfs.
 | ||||||
| // TODO: handle options parsing/processing via containers/storage/pkg/mount
 | // 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.
 | 	// 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.
 | 	// 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 { | 	if err != nil { | ||||||
| 		return nil, nil, nil, nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Next --volumes flag.
 | 	// Next --volumes flag.
 | ||||||
| 	volumeMounts, volumeVolumes, overlayVolumes, err := specgen.GenVolumeMounts(volumeFlag) | 	volumeMounts, volumeVolumes, overlayVolumes, err := specgen.GenVolumeMounts(volumeFlag) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, nil, nil, nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Next --tmpfs flag.
 | 	// Next --tmpfs flag.
 | ||||||
| 	tmpfsMounts, err := getTmpfsMounts(tmpfsFlag) | 	tmpfsMounts, err := getTmpfsMounts(tmpfsFlag) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, nil, nil, nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Unify mounts from --mount, --volume, --tmpfs.
 | 	// Unify mounts from --mount, --volume, --tmpfs.
 | ||||||
| 	// Start with --volume.
 | 	// Start with --volume.
 | ||||||
| 	for dest, mount := range volumeMounts { | 	for dest, mount := range volumeMounts { | ||||||
| 		if vol, ok := unifiedMounts[dest]; ok { | 		if vol, ok := unifiedContainerMounts.mounts[dest]; ok { | ||||||
| 			if mount.Source == vol.Source && | 			if mount.Source == vol.Source && | ||||||
| 				specgen.StringSlicesEqual(vol.Options, mount.Options) { | 				specgen.StringSlicesEqual(vol.Options, mount.Options) { | ||||||
| 				continue | 				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 { | 	for dest, volume := range volumeVolumes { | ||||||
| 		if vol, ok := unifiedVolumes[dest]; ok { | 		if vol, ok := unifiedContainerMounts.volumes[dest]; ok { | ||||||
| 			if volume.Name == vol.Name && | 			if volume.Name == vol.Name && | ||||||
| 				specgen.StringSlicesEqual(vol.Options, volume.Options) { | 				specgen.StringSlicesEqual(vol.Options, volume.Options) { | ||||||
| 				continue | 				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
 | 	// Now --tmpfs
 | ||||||
| 	for dest, tmpfs := range tmpfsMounts { | 	for dest, tmpfs := range tmpfsMounts { | ||||||
| 		if vol, ok := unifiedMounts[dest]; ok { | 		if vol, ok := unifiedContainerMounts.mounts[dest]; ok { | ||||||
| 			if vol.Type != define.TypeTmpfs { | 			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 | 			continue | ||||||
| 		} | 		} | ||||||
| 		unifiedMounts[dest] = tmpfs | 		unifiedContainerMounts.mounts[dest] = tmpfs | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Check for conflicts between named volumes, overlay & image volumes,
 | 	// 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 | 		allMounts[dest] = true | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 	for dest := range unifiedMounts { | 	for dest := range unifiedContainerMounts.mounts { | ||||||
| 		if err := testAndSet(dest); err != nil { | 		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 { | 		if err := testAndSet(dest); err != nil { | ||||||
| 			return nil, nil, nil, nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	for dest := range overlayVolumes { | 	for dest := range overlayVolumes { | ||||||
| 		if err := testAndSet(dest); err != nil { | 		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 { | 		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
 | 	// Final step: maps to arrays
 | ||||||
| 	finalMounts := make([]spec.Mount, 0, len(unifiedMounts)) | 	finalMounts := make([]spec.Mount, 0, len(unifiedContainerMounts.mounts)) | ||||||
| 	for _, mount := range unifiedMounts { | 	for _, mount := range unifiedContainerMounts.mounts { | ||||||
| 		if mount.Type == define.TypeBind { | 		if mount.Type == define.TypeBind { | ||||||
| 			absSrc, err := specgen.ConvertWinMountPath(mount.Source) | 			absSrc, err := specgen.ConvertWinMountPath(mount.Source) | ||||||
| 			if err != nil { | 			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 | 			mount.Source = absSrc | ||||||
| 		} | 		} | ||||||
| 		finalMounts = append(finalMounts, mount) | 		finalMounts = append(finalMounts, mount) | ||||||
| 	} | 	} | ||||||
| 	finalVolumes := make([]*specgen.NamedVolume, 0, len(unifiedVolumes)) | 	finalVolumes := make([]*specgen.NamedVolume, 0, len(unifiedContainerMounts.volumes)) | ||||||
| 	for _, volume := range unifiedVolumes { | 	for _, volume := range unifiedContainerMounts.volumes { | ||||||
| 		finalVolumes = append(finalVolumes, volume) | 		finalVolumes = append(finalVolumes, volume) | ||||||
| 	} | 	} | ||||||
| 	finalOverlayVolume := make([]*specgen.OverlayVolume, 0) | 	finalOverlayVolume := make([]*specgen.OverlayVolume, 0, len(overlayVolumes)) | ||||||
| 	for _, volume := range overlayVolumes { | 	for _, volume := range overlayVolumes { | ||||||
| 		finalOverlayVolume = append(finalOverlayVolume, volume) | 		finalOverlayVolume = append(finalOverlayVolume, volume) | ||||||
| 	} | 	} | ||||||
| 	finalImageVolumes := make([]*specgen.ImageVolume, 0, len(unifiedImageVolumes)) | 	finalImageVolumes := make([]*specgen.ImageVolume, 0, len(unifiedContainerMounts.imageVolumes)) | ||||||
| 	for _, volume := range unifiedImageVolumes { | 	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 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.
 | // 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=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, ...
 | ||||||
| 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) | 	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.
 | ||||||
|  | @ -225,6 +258,18 @@ func Mounts(mountFlag []string, configMounts []string) (map[string]spec.Mount, m | ||||||
| 					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 { | ||||||
|  | @ -246,17 +291,22 @@ func Mounts(mountFlag []string, configMounts []string) (map[string]spec.Mount, m | ||||||
| 
 | 
 | ||||||
| 	// Parse mounts passed in from the user
 | 	// Parse mounts passed in from the user
 | ||||||
| 	if err := parseMounts(mountFlag, false); err != nil { | 	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
 | 	// 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
 | 	// the duplicate. This means that the parsing of the containers.conf configMounts should always
 | ||||||
| 	// happen second.
 | 	// happen second.
 | ||||||
| 	if err := parseMounts(configMounts, true); err != nil { | 	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) { | func parseMountOptions(mountType string, args []string) (*universalMount, error) { | ||||||
|  | @ -660,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) | ||||||
|  |  | ||||||
|  | @ -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) | 		a := podmanTest.InspectArtifact(artifact1Name) | ||||||
| 
 | 
 | ||||||
| 		Expect(a.Manifest.Layers).To(HaveLen(1)) | 		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() { | 	It("podman artifact add file already exists in artifact", func() { | ||||||
|  | @ -506,7 +506,7 @@ var _ = Describe("Podman artifact", func() { | ||||||
| 		a := podmanTest.InspectArtifact(artifact1Name) | 		a := podmanTest.InspectArtifact(artifact1Name) | ||||||
| 
 | 
 | ||||||
| 		Expect(a.Manifest.Layers).To(HaveLen(1)) | 		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() { | 	It("podman artifact add with --append and --type", func() { | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ package integration | ||||||
| import ( | import ( | ||||||
| 	"bufio" | 	"bufio" | ||||||
| 	"bytes" | 	"bytes" | ||||||
|  | 	crand "crypto/rand" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | @ -1390,6 +1391,7 @@ func writeYaml(content string, fileName string) error { | ||||||
| func GetPort() int { | func GetPort() int { | ||||||
| 	portMin := 5000 | 	portMin := 5000 | ||||||
| 	portMax := 5999 | 	portMax := 5999 | ||||||
|  | 
 | ||||||
| 	rng := rand.New(rand.NewSource(time.Now().UnixNano())) | 	rng := rand.New(rand.NewSource(time.Now().UnixNano())) | ||||||
| 
 | 
 | ||||||
| 	// Avoid dup-allocation races between parallel ginkgo processes
 | 	// 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) { | func createArtifactFile(numBytes int64) (string, error) { | ||||||
|  | 	GinkgoHelper() | ||||||
| 	artifactDir := filepath.Join(podmanTest.TempDir, "artifacts") | 	artifactDir := filepath.Join(podmanTest.TempDir, "artifacts") | ||||||
| 	if err := os.MkdirAll(artifactDir, 0755); err != nil { | 	if err := os.MkdirAll(artifactDir, 0755); err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
| 	filename := RandomString(8) | 	filename := RandomString(8) | ||||||
| 	outFile := filepath.Join(artifactDir, filename) | 	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() | 	f, err := os.Create(filepath.Join(artifactDir, filename)) | ||||||
| 	if session.ExitCode() != 0 { | 	if err != nil { | ||||||
| 		return "", errors.New("unable to generate artifact file") | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	defer f.Close() | ||||||
|  | 	_, err = io.CopyN(f, crand.Reader, numBytes) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
| 	} | 	} | ||||||
| 	return outFile, nil | 	return outFile, nil | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue