diff options
28 files changed, 1333 insertions, 878 deletions
diff --git a/cmd/podman/common/specgen.go b/cmd/podman/common/specgen.go index 1e857ea01..10ae0bb2d 100644 --- a/cmd/podman/common/specgen.go +++ b/cmd/podman/common/specgen.go @@ -548,22 +548,31 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string } s.SeccompPolicy = c.SeccompPolicy + + // TODO: should parse out options + s.VolumesFrom = c.VolumesFrom + + // Only add read-only tmpfs mounts in case that we are read-only and the + // read-only tmpfs flag has been set. + mounts, volumes, err := parseVolumes(c.Volume, c.Mount, c.TmpFS, (c.ReadOnlyTmpFS && c.ReadOnly)) + if err != nil { + return err + } + s.Mounts = mounts + s.Volumes = volumes + // TODO any idea why this was done - // storage.go from spec/ - // grab it - //volumes := rtc.Containers.Volumes + //devices := rtc.Containers.Devices // TODO conflict on populate? - //if v := c.Volume; len(v)> 0 { - // s.Volumes = append(volumes, c.StringSlice("volume")...) + // + //if c.Changed("device") { + // devices = append(devices, c.StringSlice("device")...) //} - //s.volu - - //s.Mounts = c.Mount - s.VolumesFrom = c.VolumesFrom for _, dev := range c.Devices { s.Devices = append(s.Devices, specs.LinuxDevice{Path: dev}) } + // TODO things i cannot find in spec // we dont think these are in the spec // init - initbinary diff --git a/cmd/podman/common/volumes.go b/cmd/podman/common/volumes.go new file mode 100644 index 000000000..6b0b6e9cf --- /dev/null +++ b/cmd/podman/common/volumes.go @@ -0,0 +1,569 @@ +package common + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/containers/buildah/pkg/parse" + "github.com/containers/libpod/pkg/specgen" + "github.com/containers/libpod/pkg/util" + spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +const ( + // TypeBind is the type for mounting host dir + TypeBind = "bind" + // TypeVolume is the type for named volumes + TypeVolume = "volume" + // TypeTmpfs is the type for mounting tmpfs + TypeTmpfs = "tmpfs" +) + +var ( + errDuplicateDest = errors.Errorf("duplicate mount destination") + optionArgError = errors.Errorf("must provide an argument for option") + noDestError = errors.Errorf("must set volume destination") +) + +// Parse all volume-related options in the create config into a set of mounts +// and named volumes to add to the container. +// Handles --volumes, --mount, and --tmpfs flags. +// 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(volumeFlag, mountFlag, tmpfsFlag []string, addReadOnlyTmpfs bool) ([]spec.Mount, []*specgen.NamedVolume, error) { + // Get mounts from the --mounts flag. + unifiedMounts, unifiedVolumes, err := getMounts(mountFlag) + if err != nil { + return nil, nil, err + } + + // Next --volumes flag. + volumeMounts, volumeVolumes, err := getVolumeMounts(volumeFlag) + if err != nil { + return nil, nil, err + } + + // Next --tmpfs flag. + tmpfsMounts, err := getTmpfsMounts(tmpfsFlag) + if err != nil { + return nil, nil, err + } + + // Unify mounts from --mount, --volume, --tmpfs. + // Start with --volume. + for dest, mount := range volumeMounts { + if _, ok := unifiedMounts[dest]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, dest) + } + unifiedMounts[dest] = mount + } + for dest, volume := range volumeVolumes { + if _, ok := unifiedVolumes[dest]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, dest) + } + unifiedVolumes[dest] = volume + } + // Now --tmpfs + for dest, tmpfs := range tmpfsMounts { + if _, ok := unifiedMounts[dest]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, dest) + } + unifiedMounts[dest] = tmpfs + } + + // If requested, add tmpfs filesystems for read-only containers. + if addReadOnlyTmpfs { + readonlyTmpfs := []string{"/tmp", "/var/tmp", "/run"} + options := []string{"rw", "rprivate", "nosuid", "nodev", "tmpcopyup"} + for _, dest := range readonlyTmpfs { + if _, ok := unifiedMounts[dest]; ok { + continue + } + if _, ok := unifiedVolumes[dest]; ok { + continue + } + localOpts := options + if dest == "/run" { + localOpts = append(localOpts, "noexec", "size=65536k") + } else { + localOpts = append(localOpts, "exec") + } + unifiedMounts[dest] = spec.Mount{ + Destination: dest, + Type: TypeTmpfs, + Source: "tmpfs", + Options: localOpts, + } + } + } + + // Check for conflicts between named volumes and mounts + for dest := range unifiedMounts { + if _, ok := unifiedVolumes[dest]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, "conflict at mount destination %v", dest) + } + } + for dest := range unifiedVolumes { + if _, ok := unifiedMounts[dest]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, "conflict at mount destination %v", dest) + } + } + + // Final step: maps to arrays + finalMounts := make([]spec.Mount, 0, len(unifiedMounts)) + for _, mount := range unifiedMounts { + if mount.Type == TypeBind { + absSrc, err := filepath.Abs(mount.Source) + if err != nil { + return nil, nil, errors.Wrapf(err, "error getting absolute path of %s", mount.Source) + } + mount.Source = absSrc + } + finalMounts = append(finalMounts, mount) + } + finalVolumes := make([]*specgen.NamedVolume, 0, len(unifiedVolumes)) + for _, volume := range unifiedVolumes { + finalVolumes = append(finalVolumes, volume) + } + + return finalMounts, finalVolumes, nil +} + +// getMounts takes user-provided input from the --mount flag 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 getMounts(mountFlag []string) (map[string]spec.Mount, map[string]*specgen.NamedVolume, error) { + finalMounts := make(map[string]spec.Mount) + finalNamedVolumes := make(map[string]*specgen.NamedVolume) + + errInvalidSyntax := errors.Errorf("incorrect mount format: should be --mount type=<bind|tmpfs|volume>,[src=<host-dir|volume-name>,]target=<ctr-dir>[,options]") + + // TODO(vrothberg): the manual parsing can be replaced with a regular expression + // to allow a more robust parsing of the mount format and to give + // precise errors regarding supported format versus supported options. + for _, mount := range mountFlag { + arr := strings.SplitN(mount, ",", 2) + if len(arr) < 2 { + return nil, nil, errors.Wrapf(errInvalidSyntax, "%q", mount) + } + kv := strings.Split(arr[0], "=") + // TODO: type is not explicitly required in Docker. + // If not specified, it defaults to "volume". + if len(kv) != 2 || kv[0] != "type" { + return nil, nil, errors.Wrapf(errInvalidSyntax, "%q", mount) + } + + tokens := strings.Split(arr[1], ",") + switch kv[1] { + case TypeBind: + mount, err := getBindMount(tokens) + if err != nil { + return nil, nil, err + } + if _, ok := finalMounts[mount.Destination]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, mount.Destination) + } + finalMounts[mount.Destination] = mount + case TypeTmpfs: + mount, err := getTmpfsMount(tokens) + if err != nil { + return nil, nil, err + } + if _, ok := finalMounts[mount.Destination]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, mount.Destination) + } + finalMounts[mount.Destination] = mount + case "volume": + volume, err := getNamedVolume(tokens) + if err != nil { + return nil, nil, err + } + if _, ok := finalNamedVolumes[volume.Dest]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, volume.Dest) + } + finalNamedVolumes[volume.Dest] = volume + default: + return nil, nil, errors.Errorf("invalid filesystem type %q", kv[1]) + } + } + + return finalMounts, finalNamedVolumes, nil +} + +// Parse a single bind mount entry from the --mount flag. +func getBindMount(args []string) (spec.Mount, error) { + newMount := spec.Mount{ + Type: TypeBind, + } + + var setSource, setDest, setRORW, setSuid, setDev, setExec, setRelabel bool + + for _, val := range args { + kv := strings.Split(val, "=") + switch kv[0] { + case "bind-nonrecursive": + newMount.Options = append(newMount.Options, "bind") + case "ro", "rw": + if setRORW { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'ro' or 'rw' options more than once") + } + setRORW = true + // Can be formatted as one of: + // ro + // ro=[true|false] + // rw + // rw=[true|false] + switch len(kv) { + case 1: + newMount.Options = append(newMount.Options, kv[0]) + case 2: + switch strings.ToLower(kv[1]) { + case "true": + newMount.Options = append(newMount.Options, kv[0]) + case "false": + // Set the opposite only for rw + // ro's opposite is the default + if kv[0] == "rw" { + newMount.Options = append(newMount.Options, "ro") + } + default: + return newMount, errors.Wrapf(optionArgError, "%s must be set to true or false, instead received %q", kv[0], kv[1]) + } + default: + return newMount, errors.Wrapf(optionArgError, "badly formatted option %q", val) + } + case "nosuid", "suid": + if setSuid { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'nosuid' and 'suid' options more than once") + } + setSuid = true + newMount.Options = append(newMount.Options, kv[0]) + case "nodev", "dev": + if setDev { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'nodev' and 'dev' options more than once") + } + setDev = true + newMount.Options = append(newMount.Options, kv[0]) + case "noexec", "exec": + if setExec { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'noexec' and 'exec' options more than once") + } + setExec = true + newMount.Options = append(newMount.Options, kv[0]) + case "shared", "rshared", "private", "rprivate", "slave", "rslave", "Z", "z": + newMount.Options = append(newMount.Options, kv[0]) + case "bind-propagation": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + newMount.Options = append(newMount.Options, kv[1]) + case "src", "source": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + if err := parse.ValidateVolumeHostDir(kv[1]); err != nil { + return newMount, err + } + newMount.Source = kv[1] + setSource = true + case "target", "dst", "destination": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { + return newMount, err + } + newMount.Destination = filepath.Clean(kv[1]) + setDest = true + case "relabel": + if setRelabel { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'relabel' option more than once") + } + setRelabel = true + if len(kv) != 2 { + return newMount, errors.Wrapf(util.ErrBadMntOption, "%s mount option must be 'private' or 'shared'", kv[0]) + } + switch kv[1] { + case "private": + newMount.Options = append(newMount.Options, "z") + case "shared": + newMount.Options = append(newMount.Options, "Z") + default: + return newMount, errors.Wrapf(util.ErrBadMntOption, "%s mount option must be 'private' or 'shared'", kv[0]) + } + default: + return newMount, errors.Wrapf(util.ErrBadMntOption, kv[0]) + } + } + + if !setDest { + return newMount, noDestError + } + + if !setSource { + newMount.Source = newMount.Destination + } + + options, err := parse.ValidateVolumeOpts(newMount.Options) + if err != nil { + return newMount, err + } + newMount.Options = options + return newMount, nil +} + +// Parse a single tmpfs mount entry from the --mount flag +func getTmpfsMount(args []string) (spec.Mount, error) { + newMount := spec.Mount{ + Type: TypeTmpfs, + Source: TypeTmpfs, + } + + var setDest, setRORW, setSuid, setDev, setExec, setTmpcopyup bool + + for _, val := range args { + kv := strings.Split(val, "=") + switch kv[0] { + case "tmpcopyup", "notmpcopyup": + if setTmpcopyup { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'tmpcopyup' and 'notmpcopyup' options more than once") + } + setTmpcopyup = true + newMount.Options = append(newMount.Options, kv[0]) + case "ro", "rw": + if setRORW { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'ro' and 'rw' options more than once") + } + setRORW = true + newMount.Options = append(newMount.Options, kv[0]) + case "nosuid", "suid": + if setSuid { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'nosuid' and 'suid' options more than once") + } + setSuid = true + newMount.Options = append(newMount.Options, kv[0]) + case "nodev", "dev": + if setDev { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'nodev' and 'dev' options more than once") + } + setDev = true + newMount.Options = append(newMount.Options, kv[0]) + case "noexec", "exec": + if setExec { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'noexec' and 'exec' options more than once") + } + setExec = true + newMount.Options = append(newMount.Options, kv[0]) + case "tmpfs-mode": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + newMount.Options = append(newMount.Options, fmt.Sprintf("mode=%s", kv[1])) + case "tmpfs-size": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + newMount.Options = append(newMount.Options, fmt.Sprintf("size=%s", kv[1])) + case "src", "source": + return newMount, errors.Errorf("source is not supported with tmpfs mounts") + case "target", "dst", "destination": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { + return newMount, err + } + newMount.Destination = filepath.Clean(kv[1]) + setDest = true + default: + return newMount, errors.Wrapf(util.ErrBadMntOption, kv[0]) + } + } + + if !setDest { + return newMount, noDestError + } + + return newMount, nil +} + +// Parse a single volume mount entry from the --mount flag. +// Note that the volume-label option for named volumes is currently NOT supported. +// TODO: add support for --volume-label +func getNamedVolume(args []string) (*specgen.NamedVolume, error) { + newVolume := new(specgen.NamedVolume) + + var setSource, setDest, setRORW, setSuid, setDev, setExec bool + + for _, val := range args { + kv := strings.Split(val, "=") + switch kv[0] { + case "ro", "rw": + if setRORW { + return nil, errors.Wrapf(optionArgError, "cannot pass 'ro' and 'rw' options more than once") + } + setRORW = true + newVolume.Options = append(newVolume.Options, kv[0]) + case "nosuid", "suid": + if setSuid { + return nil, errors.Wrapf(optionArgError, "cannot pass 'nosuid' and 'suid' options more than once") + } + setSuid = true + newVolume.Options = append(newVolume.Options, kv[0]) + case "nodev", "dev": + if setDev { + return nil, errors.Wrapf(optionArgError, "cannot pass 'nodev' and 'dev' options more than once") + } + setDev = true + newVolume.Options = append(newVolume.Options, kv[0]) + case "noexec", "exec": + if setExec { + return nil, errors.Wrapf(optionArgError, "cannot pass 'noexec' and 'exec' options more than once") + } + setExec = true + newVolume.Options = append(newVolume.Options, kv[0]) + case "volume-label": + return nil, errors.Errorf("the --volume-label option is not presently implemented") + case "src", "source": + if len(kv) == 1 { + return nil, errors.Wrapf(optionArgError, kv[0]) + } + newVolume.Name = kv[1] + setSource = true + case "target", "dst", "destination": + if len(kv) == 1 { + return nil, errors.Wrapf(optionArgError, kv[0]) + } + if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { + return nil, err + } + newVolume.Dest = filepath.Clean(kv[1]) + setDest = true + default: + return nil, errors.Wrapf(util.ErrBadMntOption, kv[0]) + } + } + + if !setSource { + return nil, errors.Errorf("must set source volume") + } + if !setDest { + return nil, noDestError + } + + return newVolume, nil +} + +func getVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*specgen.NamedVolume, error) { + mounts := make(map[string]spec.Mount) + volumes := make(map[string]*specgen.NamedVolume) + + volumeFormatErr := errors.Errorf("incorrect volume format, should be [host-dir:]ctr-dir[:option]") + + for _, vol := range volumeFlag { + var ( + options []string + src string + dest string + err error + ) + + splitVol := strings.Split(vol, ":") + if len(splitVol) > 3 { + return nil, nil, errors.Wrapf(volumeFormatErr, vol) + } + + src = splitVol[0] + if len(splitVol) == 1 { + // This is an anonymous named volume. Only thing given + // is destination. + // Name/source will be blank, and populated by libpod. + src = "" + dest = splitVol[0] + } else if len(splitVol) > 1 { + dest = splitVol[1] + } + if len(splitVol) > 2 { + if options, err = parse.ValidateVolumeOpts(strings.Split(splitVol[2], ",")); err != nil { + return nil, nil, err + } + } + + // Do not check source dir for anonymous volumes + if len(splitVol) > 1 { + if err := parse.ValidateVolumeHostDir(src); err != nil { + return nil, nil, err + } + } + if err := parse.ValidateVolumeCtrDir(dest); err != nil { + return nil, nil, err + } + + cleanDest := filepath.Clean(dest) + + if strings.HasPrefix(src, "/") || strings.HasPrefix(src, ".") { + // This is not a named volume + newMount := spec.Mount{ + Destination: cleanDest, + Type: string(TypeBind), + Source: src, + Options: options, + } + if _, ok := mounts[newMount.Destination]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, newMount.Destination) + } + mounts[newMount.Destination] = newMount + } else { + // This is a named volume + newNamedVol := new(specgen.NamedVolume) + newNamedVol.Name = src + newNamedVol.Dest = cleanDest + newNamedVol.Options = options + + if _, ok := volumes[newNamedVol.Dest]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, newNamedVol.Dest) + } + volumes[newNamedVol.Dest] = newNamedVol + } + + logrus.Debugf("User mount %s:%s options %v", src, dest, options) + } + + return mounts, volumes, 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) + for _, i := range tmpfsFlag { + // Default options if nothing passed + var options []string + spliti := strings.Split(i, ":") + destPath := spliti[0] + if err := parse.ValidateVolumeCtrDir(spliti[0]); err != nil { + return nil, err + } + if len(spliti) > 1 { + options = strings.Split(spliti[1], ",") + } + + if _, ok := m[destPath]; ok { + return nil, errors.Wrapf(errDuplicateDest, destPath) + } + + mount := spec.Mount{ + Destination: filepath.Clean(destPath), + Type: string(TypeTmpfs), + Options: options, + Source: string(TypeTmpfs), + } + m[destPath] = mount + } + return m, nil +} diff --git a/cmd/podman/main.go b/cmd/podman/main.go index 2d9e45177..8109eca2f 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -6,6 +6,7 @@ import ( _ "github.com/containers/libpod/cmd/podman/containers" _ "github.com/containers/libpod/cmd/podman/healthcheck" _ "github.com/containers/libpod/cmd/podman/images" + _ "github.com/containers/libpod/cmd/podman/manifest" _ "github.com/containers/libpod/cmd/podman/networks" _ "github.com/containers/libpod/cmd/podman/pods" "github.com/containers/libpod/cmd/podman/registry" diff --git a/cmd/podman/manifest/add.go b/cmd/podman/manifest/add.go new file mode 100644 index 000000000..20251ca87 --- /dev/null +++ b/cmd/podman/manifest/add.go @@ -0,0 +1,49 @@ +package manifest + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + manifestAddOpts = entities.ManifestAddOptions{} + addCmd = &cobra.Command{ + Use: "add", + Short: "Add images to a manifest list or image index", + Long: "Adds an image to a manifest list or image index.", + RunE: add, + Example: `podman manifest add mylist:v1.11 image:v1.11-amd64 + podman manifest add mylist:v1.11 transport:imageName`, + Args: cobra.ExactArgs(2), + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: addCmd, + Parent: manifestCmd, + }) + flags := addCmd.Flags() + flags.BoolVar(&manifestAddOpts.All, "all", false, "add all of the list's images if the image is a list") + flags.StringSliceVar(&manifestAddOpts.Annotation, "annotation", nil, "set an `annotation` for the specified image") + flags.StringVar(&manifestAddOpts.Arch, "arch", "", "override the `architecture` of the specified image") + flags.StringSliceVar(&manifestAddOpts.Features, "features", nil, "override the `features` of the specified image") + flags.StringVar(&manifestAddOpts.OSVersion, "os-version", "", "override the OS `version` of the specified image") + flags.StringVar(&manifestAddOpts.Variant, "variant", "", "override the `Variant` of the specified image") +} + +func add(cmd *cobra.Command, args []string) error { + manifestAddOpts.Images = []string{args[1], args[0]} + listID, err := registry.ImageEngine().ManifestAdd(context.Background(), manifestAddOpts) + if err != nil { + return errors.Wrapf(err, "error adding to manifest list %s", args[0]) + } + fmt.Printf("%s\n", listID) + return nil +} diff --git a/cmd/podman/manifest/create.go b/cmd/podman/manifest/create.go new file mode 100644 index 000000000..4f3e27774 --- /dev/null +++ b/cmd/podman/manifest/create.go @@ -0,0 +1,44 @@ +package manifest + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + manifestCreateOpts = entities.ManifestCreateOptions{} + createCmd = &cobra.Command{ + Use: "create", + Short: "Create manifest list or image index", + Long: "Creates manifest lists or image indexes.", + RunE: create, + Example: `podman manifest create mylist:v1.11 + podman manifest create mylist:v1.11 arch-specific-image-to-add + podman manifest create --all mylist:v1.11 transport:tagged-image-to-add`, + Args: cobra.MinimumNArgs(1), + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: createCmd, + Parent: manifestCmd, + }) + flags := createCmd.Flags() + flags.BoolVar(&manifestCreateOpts.All, "all", false, "add all of the lists' images if the images to add are lists") +} + +func create(cmd *cobra.Command, args []string) error { + imageID, err := registry.ImageEngine().ManifestCreate(context.Background(), args[:1], args[1:], manifestCreateOpts) + if err != nil { + return errors.Wrapf(err, "error creating manifest %s", args[0]) + } + fmt.Printf("%s\n", imageID) + return nil +} diff --git a/cmd/podman/manifest/inspect.go b/cmd/podman/manifest/inspect.go new file mode 100644 index 000000000..36ecdc87b --- /dev/null +++ b/cmd/podman/manifest/inspect.go @@ -0,0 +1,39 @@ +package manifest + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + inspectCmd = &cobra.Command{ + Use: "inspect IMAGE", + Short: "Display the contents of a manifest list or image index", + Long: "Display the contents of a manifest list or image index.", + RunE: inspect, + Example: "podman manifest inspect localhost/list", + Args: cobra.ExactArgs(1), + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: inspectCmd, + Parent: manifestCmd, + }) +} + +func inspect(cmd *cobra.Command, args []string) error { + buf, err := registry.ImageEngine().ManifestInspect(context.Background(), args[0]) + if err != nil { + return errors.Wrapf(err, "error inspect manifest %s", args[0]) + } + fmt.Printf("%s\n", buf) + return nil +} diff --git a/cmd/podman/manifest/manifest.go b/cmd/podman/manifest/manifest.go new file mode 100644 index 000000000..b9ac7ea68 --- /dev/null +++ b/cmd/podman/manifest/manifest.go @@ -0,0 +1,27 @@ +package manifest + +import ( + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + manifestDescription = "Creates, modifies, and pushes manifest lists and image indexes." + manifestCmd = &cobra.Command{ + Use: "manifest", + Short: "Manipulate manifest lists and image indexes", + Long: manifestDescription, + TraverseChildren: true, + RunE: registry.SubCommandExists, + Example: `podman manifest create localhost/list + podman manifest inspect localhost/list`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: manifestCmd, + }) +} diff --git a/completions/bash/podman b/completions/bash/podman index 6997db3b5..41a76a967 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -1733,6 +1733,83 @@ _podman_logs() { esac } +_podman_manifest() { + local boolean_options=" + --help + -h + " + subcommands=" + add + create + inspect + " + __podman_subcommands "$subcommands" && return + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + COMPREPLY=( $( compgen -W "$subcommands" -- "$cur" ) ) + ;; + esac +} + +_podman_manifest_add() { + local options_with_args=" + --annotation + --arch + --features + --os-version + --variant + " + + local boolean_options=" + --all + --help + -h + " + + _complete_ "$options_with_args" "$boolean_options" + case "$cur" in + -*) + COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) + ;; + *) + __podman_complete_images --id + ;; + esac +} + +_podman_manifest_create() { + local boolean_options=" + --all + --help + -h + " + + _complete_ "$boolean_options" +} + +_podman_manifest_inspect() { + local options_with_args=" + " + + local boolean_options=" + " + + _complete_ "$options_with_args" "$boolean_options" + case "$cur" in + -*) + COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) + ;; + *) + __podman_complete_images --id + + ;; + esac +} + _podman_pull() { local options_with_args=" --authfile @@ -3356,6 +3433,7 @@ _podman_podman() { login logout logs + manifest mount pause pod diff --git a/docs/source/markdown/podman-manifest-add.1.md b/docs/source/markdown/podman-manifest-add.1.md new file mode 100644 index 000000000..4ecf03900 --- /dev/null +++ b/docs/source/markdown/podman-manifest-add.1.md @@ -0,0 +1,69 @@ +% podman-manifest-add(1) + +## NAME +podman\-manifest\-add - Add an image to a manifest list or image index + +## SYNOPSIS +**podman manifest add** *listnameorindexname* *imagename* + +## DESCRIPTION + +Adds the specified image to the specified manifest list or image index. + +## RETURN VALUE +The list image's ID. + +## OPTIONS + +**--all** + +If the image which should be added to the list or index is itself a list or +index, add all of the contents to the local list. By default, only one image +from such a list or index will be added to the list or index. Combining +*--all* with any of the other options described below is NOT recommended. + +**--annotation** *annotation=value* + +Set an annotation on the entry for the newly-added image. + +**--arch** + +Override the architecture which the list or index records as a requirement for +the image. If *imageName* refers to a manifest list or image index, the +architecture information will be retrieved from it. Otherwise, it will be +retrieved from the image's configuration information. + +**--features** + +Specify the features list which the list or index records as requirements for +the image. This option is rarely used. + +**--os-version** + +Specify the OS version which the list or index records as a requirement for the +image. This option is rarely used. + +**--variant** + +Specify the variant which the list or index records for the image. This option +is typically used to distinguish between multiple entries which share the same +architecture value, but which expect different versions of its instruction set. + +## EXAMPLE + +``` +podman manifest add mylist:v1.11 docker://fedora +71c201d10fffdcac52968a000d85a0a016ca1c7d5473948000d3131c1773d965 +``` + +``` +podman manifest add --all mylist:v1.11 docker://fedora +71c201d10fffdcac52968a000d85a0a016ca1c7d5473948000d3131c1773d965 +``` + +``` +podman manifest add --arch arm64 --variant v8 mylist:v1.11 docker://71c201d10fffdcac52968a000d85a0a016ca1c7d5473948000d3131c1773d965 +``` + +## SEE ALSO +podman(1), podman-manifest(1), podman-manifest-create(1), podman-manifest-inspect(1), podman-rmi(1) diff --git a/docs/source/markdown/podman-manifest-create.1.md b/docs/source/markdown/podman-manifest-create.1.md new file mode 100644 index 000000000..941e70c32 --- /dev/null +++ b/docs/source/markdown/podman-manifest-create.1.md @@ -0,0 +1,43 @@ +% podman-manifest-create(1) + +## NAME +podman\-manifest\-create - Create a manifest list or image index + +## SYNOPSIS +**podman manifest create** [*options*] *listnameorindexname* [*imagename* ...] + +## DESCRIPTION + +Creates a new manifest list and stores it as an image in local storage using +the specified name. + +If additional images are specified, they are added to the newly-created list or +index. + +## OPTIONS + +**--all** + +If any of the images which should be added to the new list or index are +themselves lists or indexes, add all of their contents. By default, only one +image from such a list will be added to the newly-created list or index. + +## EXAMPLES + +``` +podman manifest create mylist:v1.11 +9cfd24048d5fc80903f088f1531a21bff01172abe66effa8941a4c2308dc745f +``` + +``` +podman manifest create mylist:v1.11 docker://fedora +5c2bc76bfb4ba6665a7973f7e1c05ee0536b4580637f27adc9fa5a4b2bc03cf1 +``` + +``` +podman manifest create --all mylist:v1.11 docker://fedora +30330571e79c65288a4fca421d9aed29b0210d57294d9c2056743fdcf6e3967b +``` + +## SEE ALSO +podman(1), podman-manifest(1), podman-manifest-add(1), podman-manifest-inspect(1), podman-rmi(1) diff --git a/docs/source/markdown/podman-manifest-inspect.1.md b/docs/source/markdown/podman-manifest-inspect.1.md new file mode 100644 index 000000000..efde02643 --- /dev/null +++ b/docs/source/markdown/podman-manifest-inspect.1.md @@ -0,0 +1,24 @@ +% podman-manifest-inspect(1) + +## NAME +podman\-manifest\-inspect - Display a manifest list or image index + +## SYNOPSIS +**podman manifest inspect** *listnameorindexname* + +## DESCRIPTION + +Displays the manifest list or image index stored using the specified image name. + +## RETURN VALUE + +A formatted JSON representation of the manifest list or image index. + +## EXAMPLES + +``` +podman manifest inspect mylist:v1.11 +``` + +## SEE ALSO +podman(1), podman-manifest(1), podman-manifest-create(1), podman-manifest-add(1), podman-rmi(1) diff --git a/docs/source/markdown/podman-manifest.1.md b/docs/source/markdown/podman-manifest.1.md new file mode 100644 index 000000000..70d695883 --- /dev/null +++ b/docs/source/markdown/podman-manifest.1.md @@ -0,0 +1,23 @@ +% podman-manifest(1) + +## NAME +podman\-manifest - Create and manipulate manifest lists and image indexes + +## SYNOPSIS +**podman manifest** *subcommand* + +## DESCRIPTION +The `podman manifest` command provides subcommands which can be used to: + + * Create a working Docker manifest list or OCI image index. + +## SUBCOMMANDS + +| Command | Man Page | Description | +| ------- | ---------------------------------------------------------- | --------------------------------------------------------------------------- | +| add | [podman-manifest-add(1)](podman-manifest-add.1.md) | Add an image to a manifest list or image index. | +| create | [podman-manifest-create(1)](podman-manifest-create.1.md) | Create a manifest list or image index. | +| inspect | [podman-manifest-inspect(1)](podman-manifest-inspect.1.md) | Display a manifest list or image index. | + +## SEE ALSO +podman(1), podman-manifest-add(1), podman-manifest-create(1), podman-manifest-inspect(1) diff --git a/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md index cd4148c95..6bac0cc9d 100644 --- a/docs/source/markdown/podman.1.md +++ b/docs/source/markdown/podman.1.md @@ -169,6 +169,7 @@ the exit codes follow the `chroot` standard, see below: | [podman-login(1)](podman-login.1.md) | Login to a container registry. | | [podman-logout(1)](podman-logout.1.md) | Logout of a container registry. | | [podman-logs(1)](podman-logs.1.md) | Display the logs of one or more containers. | +| [podman-manifest(1)](podman-manifest.1.md) | Create and manipulate manifest lists and image indexes. | | [podman-mount(1)](podman-mount.1.md) | Mount a working container's root filesystem. | | [podman-network(1)](podman-network.1.md) | Manage Podman CNI networks. | | [podman-pause(1)](podman-pause.1.md) | Pause one or more containers. | @@ -131,6 +131,7 @@ github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1 github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= diff --git a/libpod/define/errors.go b/libpod/define/errors.go index 3ba343789..16df2a1cc 100644 --- a/libpod/define/errors.go +++ b/libpod/define/errors.go @@ -141,4 +141,7 @@ var ( // ErrConmonOutdated indicates the version of conmon found (whether via the configuration or $PATH) // is out of date for the current podman version ErrConmonOutdated = errors.New("outdated conmon version") + + // ErrImageInUse indicates the requested operation failed because the image was in use + ErrImageInUse = errors.New("image is being used") ) diff --git a/libpod/image/pull.go b/libpod/image/pull.go index fd359d593..6b4c40ba2 100644 --- a/libpod/image/pull.go +++ b/libpod/image/pull.go @@ -334,11 +334,11 @@ func (ir *Runtime) doPullImage(ctx context.Context, sc *types.SystemContext, goa // If the image passed in was fully-qualified, we will have 1 refpair. Bc the image is fq'd, we don't need to yap about registries. if !goal.usedSearchRegistries { if pullErrors != nil && len(pullErrors.Errors) > 0 { // this should always be true - return nil, errors.Wrap(pullErrors.Errors[0], "unable to pull image") + return nil, pullErrors.Errors[0] } return nil, errors.Errorf("unable to pull image, or you do not have pull access") } - return nil, pullErrors + return nil, errors.Cause(pullErrors) } if len(images) > 0 { ir.newImageEvent(events.Pull, images[0]) diff --git a/libpod/runtime.go b/libpod/runtime.go index 3b8f9e057..e71483ef9 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -733,6 +733,11 @@ func (r *Runtime) StorageConfig() storage.StoreOptions { return r.storageConfig } +// GetStore returns the runtime stores +func (r *Runtime) GetStore() storage.Store { + return r.store +} + // DBConfig is a set of Libpod runtime configuration settings that are saved in // a State when it is first created, and can subsequently be retrieved. type DBConfig struct { diff --git a/libpod/runtime_img.go b/libpod/runtime_img.go index 6ac32878b..919080c42 100644 --- a/libpod/runtime_img.go +++ b/libpod/runtime_img.go @@ -71,7 +71,8 @@ func (r *Runtime) RemoveImage(ctx context.Context, img *image.Image, force bool) // to and untag it. repoName, err := img.MatchRepoTag(img.InputName) if hasChildren && errors.Cause(err) == image.ErrRepoTagNotFound { - return nil, errors.Errorf("unable to delete %q (cannot be forced) - image has dependent child images", img.ID()) + return nil, errors.Wrapf(define.ErrImageInUse, + "unable to delete %q (cannot be forced) - image has dependent child images", img.ID()) } if err != nil { return nil, err @@ -84,7 +85,8 @@ func (r *Runtime) RemoveImage(ctx context.Context, img *image.Image, force bool) } else if len(img.Names()) > 1 && img.InputIsID() && !force { // If the user requests to delete an image by ID and the image has multiple // reponames and no force is applied, we error out. - return nil, fmt.Errorf("unable to delete %s (must force) - image is referred to in multiple tags", img.ID()) + return nil, errors.Wrapf(define.ErrImageInUse, + "unable to delete %s (must force) - image is referred to in multiple tags", img.ID()) } err = img.Remove(ctx, force) if err != nil && errors.Cause(err) == storage.ErrImageUsedByContainer { diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 46401e4f2..760ab1b7c 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -443,7 +443,7 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) { nil, util.PullImageAlways) if err != nil { - utils.InternalServerError(w, errors.Wrapf(err, "error pulling image %q", query.Reference)) + utils.InternalServerError(w, err) return } res = append(res, handlers.LibpodImagesPullReport{ID: newImage.ID()}) diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index 9ba733162..b118a4104 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -26,4 +26,7 @@ type ImageEngine interface { Tag(ctx context.Context, nameOrId string, tags []string, options ImageTagOptions) error Tree(ctx context.Context, nameOrId string, options ImageTreeOptions) (*ImageTreeReport, error) Untag(ctx context.Context, nameOrId string, tags []string, options ImageUntagOptions) error + ManifestCreate(ctx context.Context, names, images []string, opts ManifestCreateOptions) (string, error) + ManifestInspect(ctx context.Context, name string) ([]byte, error) + ManifestAdd(ctx context.Context, opts ManifestAddOptions) (string, error) } diff --git a/pkg/domain/entities/manifest.go b/pkg/domain/entities/manifest.go new file mode 100644 index 000000000..a9c961f9d --- /dev/null +++ b/pkg/domain/entities/manifest.go @@ -0,0 +1,15 @@ +package entities + +type ManifestCreateOptions struct { + All bool `schema:"all"` +} + +type ManifestAddOptions struct { + All bool `json:"all" schema:"all"` + Annotation []string `json:"annotation" schema:"annotation"` + Arch string `json:"arch" schema:"arch"` + Features []string `json:"features" schema:"features"` + Images []string `json:"images" schema:"images"` + OSVersion string `json:"os_version" schema:"os_version"` + Variant string `json:"variant" schema:"variant"` +} diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index bed59b379..64d9c9f12 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -101,7 +101,7 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, options entiti if imageRef.Transport().Name() == dockerarchive.Transport.Name() { newImage, err := ir.Libpod.ImageRuntime().LoadFromArchiveReference(ctx, imageRef, options.SignaturePolicy, writer) if err != nil { - return nil, errors.Wrapf(err, "error pulling image %q", rawImage) + return nil, err } return &entities.ImagePullReport{Images: []string{newImage[0].ID()}}, nil } @@ -125,7 +125,7 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, options entiti if !options.AllTags { newImage, err := ir.Libpod.ImageRuntime().New(ctx, rawImage, options.SignaturePolicy, options.Authfile, writer, &dockerRegistryOptions, image.SigningOptions{}, nil, util.PullImageAlways) if err != nil { - return nil, errors.Wrapf(err, "error pulling image %q", rawImage) + return nil, err } return &entities.ImagePullReport{Images: []string{newImage.ID()}}, nil } @@ -166,7 +166,7 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, options entiti } if len(tags) != len(foundIDs) { - return nil, errors.Errorf("error pulling image %q", rawImage) + return nil, err } return &entities.ImagePullReport{Images: foundIDs}, nil } @@ -466,7 +466,7 @@ func (ir *ImageEngine) Remove(ctx context.Context, images []string, opts entitie }() // deleteImage is an anonymous function to conveniently delete an image - // withouth having to pass all local data around. + // without having to pass all local data around. deleteImage := func(img *image.Image) error { results, err := ir.Libpod.RemoveImage(ctx, img, opts.Force) switch errors.Cause(err) { @@ -476,6 +476,9 @@ func (ir *ImageEngine) Remove(ctx context.Context, images []string, opts entitie inUseErrors = true // Important for exit codes in Podman. return errors.New( fmt.Sprintf("A container associated with containers/storage, i.e. via Buildah, CRI-O, etc., may be associated with this image: %-12.12s\n", img.ID())) + case define.ErrImageInUse: + inUseErrors = true + return err default: otherErrors = true // Important for exit codes in Podman. return err diff --git a/pkg/domain/infra/abi/manifest.go b/pkg/domain/infra/abi/manifest.go new file mode 100644 index 000000000..27d4bf9a5 --- /dev/null +++ b/pkg/domain/infra/abi/manifest.go @@ -0,0 +1,101 @@ +// +build ABISupport + +package abi + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + buildahUtil "github.com/containers/buildah/util" + "github.com/containers/image/v5/docker" + "github.com/containers/image/v5/transports/alltransports" + libpodImage "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/util" + + "github.com/pkg/errors" +) + +// ManifestCreate implements logic for creating manifest lists via ImageEngine +func (ir *ImageEngine) ManifestCreate(ctx context.Context, names, images []string, opts entities.ManifestCreateOptions) (string, error) { + fullNames, err := buildahUtil.ExpandNames(names, "", ir.Libpod.SystemContext(), ir.Libpod.GetStore()) + if err != nil { + return "", errors.Wrapf(err, "error encountered while expanding image name %q", names) + } + imageID, err := libpodImage.CreateManifestList(ir.Libpod.ImageRuntime(), *ir.Libpod.SystemContext(), fullNames, images, opts.All) + if err != nil { + return imageID, err + } + return imageID, err +} + +// ManifestInspect returns the content of a manifest list or image +func (ir *ImageEngine) ManifestInspect(ctx context.Context, name string) ([]byte, error) { + dockerPrefix := fmt.Sprintf("%s://", docker.Transport.Name()) + _, err := alltransports.ParseImageName(name) + if err != nil { + _, err = alltransports.ParseImageName(dockerPrefix + name) + if err != nil { + return nil, errors.Errorf("invalid image reference %q", name) + } + } + image, err := ir.Libpod.ImageRuntime().New(ctx, name, "", "", nil, nil, libpodImage.SigningOptions{}, nil, util.PullImageMissing) + if err != nil { + return nil, errors.Wrapf(err, "reading image %q", name) + } + + list, err := image.InspectManifest() + if err != nil { + return nil, errors.Wrapf(err, "loading manifest %q", name) + } + buf, err := json.MarshalIndent(list, "", " ") + if err != nil { + return buf, errors.Wrapf(err, "error rendering manifest for display") + } + return buf, nil +} + +// ManifestAdd adds images to the manifest list +func (ir *ImageEngine) ManifestAdd(ctx context.Context, opts entities.ManifestAddOptions) (string, error) { + imageSpec := opts.Images[0] + listImageSpec := opts.Images[1] + dockerPrefix := fmt.Sprintf("%s://", docker.Transport.Name()) + _, err := alltransports.ParseImageName(imageSpec) + if err != nil { + _, err = alltransports.ParseImageName(fmt.Sprintf("%s%s", dockerPrefix, imageSpec)) + if err != nil { + return "", errors.Errorf("invalid image reference %q", imageSpec) + } + } + listImage, err := ir.Libpod.ImageRuntime().NewFromLocal(listImageSpec) + if err != nil { + return "", errors.Wrapf(err, "error retriving local image from image name %s", listImageSpec) + } + + manifestAddOpts := libpodImage.ManifestAddOpts{ + All: opts.All, + Arch: opts.Arch, + Features: opts.Features, + Images: opts.Images, + OSVersion: opts.OSVersion, + Variant: opts.Variant, + } + if len(opts.Annotation) != 0 { + annotations := make(map[string]string) + for _, annotationSpec := range opts.Annotation { + spec := strings.SplitN(annotationSpec, "=", 2) + if len(spec) != 2 { + return "", errors.Errorf("no value given for annotation %q", spec[0]) + } + annotations[spec[0]] = spec[1] + } + manifestAddOpts.Annotation = annotations + } + listID, err := listImage.AddManifest(*ir.Libpod.SystemContext(), manifestAddOpts) + if err != nil { + return listID, err + } + return listID, nil +} diff --git a/pkg/domain/infra/tunnel/manifest.go b/pkg/domain/infra/tunnel/manifest.go new file mode 100644 index 000000000..338256530 --- /dev/null +++ b/pkg/domain/infra/tunnel/manifest.go @@ -0,0 +1,63 @@ +package tunnel + +import ( + "context" + "encoding/json" + "strings" + + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/bindings/manifests" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" +) + +// ManifestCreate implements manifest create via ImageEngine +func (ir *ImageEngine) ManifestCreate(ctx context.Context, names, images []string, opts entities.ManifestCreateOptions) (string, error) { + imageID, err := manifests.Create(ir.ClientCxt, names, images, &opts.All) + if err != nil { + return imageID, errors.Wrapf(err, "error creating manifest") + } + return imageID, err +} + +// ManifestInspect returns contents of manifest list with given name +func (ir *ImageEngine) ManifestInspect(ctx context.Context, name string) ([]byte, error) { + list, err := manifests.Inspect(ir.ClientCxt, name) + if err != nil { + return nil, errors.Wrapf(err, "error getting content of manifest list or image %s", name) + } + + buf, err := json.MarshalIndent(list, "", " ") + if err != nil { + return buf, errors.Wrapf(err, "error rendering manifest for display") + } + return buf, err +} + +// ManifestAdd adds images to the manifest list +func (ir *ImageEngine) ManifestAdd(ctx context.Context, opts entities.ManifestAddOptions) (string, error) { + manifestAddOpts := image.ManifestAddOpts{ + All: opts.All, + Arch: opts.Arch, + Features: opts.Features, + Images: opts.Images, + OSVersion: opts.OSVersion, + Variant: opts.Variant, + } + if len(opts.Annotation) != 0 { + annotations := make(map[string]string) + for _, annotationSpec := range opts.Annotation { + spec := strings.SplitN(annotationSpec, "=", 2) + if len(spec) != 2 { + return "", errors.Errorf("no value given for annotation %q", spec[0]) + } + annotations[spec[0]] = spec[1] + } + manifestAddOpts.Annotation = annotations + } + listID, err := manifests.Add(ctx, opts.Images[1], manifestAddOpts) + if err != nil { + return listID, errors.Wrapf(err, "error adding to manifest list %s", opts.Images[1]) + } + return listID, nil +} diff --git a/pkg/specgen/generate/storage.go b/pkg/specgen/generate/storage.go index c9a36ed46..7650e4e9a 100644 --- a/pkg/specgen/generate/storage.go +++ b/pkg/specgen/generate/storage.go @@ -1,22 +1,16 @@ package generate -//nolint - import ( - "fmt" "path" "path/filepath" "strings" - "github.com/containers/buildah/pkg/parse" - "github.com/containers/libpod/libpod" - "github.com/containers/libpod/pkg/specgen" "github.com/containers/libpod/pkg/util" spec "github.com/opencontainers/runtime-spec/specs-go" - "github.com/pkg/errors" "github.com/sirupsen/logrus" ) +// TODO unify this in one place - maybe libpod/define const ( // TypeBind is the type for mounting host dir TypeBind = "bind" @@ -26,812 +20,6 @@ const ( TypeTmpfs = "tmpfs" ) -var ( - errDuplicateDest = errors.Errorf("duplicate mount destination") //nolint - optionArgError = errors.Errorf("must provide an argument for option") //nolint - noDestError = errors.Errorf("must set volume destination") //nolint -) - -// Parse all volume-related options in the create config into a set of mounts -// and named volumes to add to the container. -// Handles --volumes-from, --volumes, --tmpfs, --init, and --init-path flags. -// TODO: Named volume options - should we default to rprivate? It bakes into a -// bind mount under the hood... -// TODO: handle options parsing/processing via containers/storage/pkg/mount -func parseVolumes(s *specgen.SpecGenerator, mounts, volMounts, tmpMounts []string) error { //nolint - - // TODO this needs to come from the image and erquires a runtime - - // Add image volumes. - //baseMounts, baseVolumes, err := config.getImageVolumes() - //if err != nil { - // return nil, nil, err - //} - - // Add --volumes-from. - // Overrides image volumes unconditionally. - //vFromMounts, vFromVolumes, err := config.getVolumesFrom(runtime) - //if err != nil { - // return nil, nil, err - //} - //for dest, mount := range vFromMounts { - // baseMounts[dest] = mount - //} - //for dest, volume := range vFromVolumes { - // baseVolumes[dest] = volume - //} - - // Next mounts from the --mounts flag. - // Do not override yet. - //unifiedMounts, _, err := getMounts(mounts) - //if err != nil { - // return err - //} - // - //// Next --volumes flag. - //// Do not override yet. - //volumeMounts, _ , err := getVolumeMounts(volMounts) - //if err != nil { - // return err - //} - // - //// Next --tmpfs flag. - //// Do not override yet. - //tmpfsMounts, err := getTmpfsMounts(tmpMounts) - //if err != nil { - // return err - //} - - //// Unify mounts from --mount, --volume, --tmpfs. - //// Also add mounts + volumes directly from createconfig. - //// Start with --volume. - //for dest, mount := range volumeMounts { - // if _, ok := unifiedMounts[dest]; ok { - // return nil, nil, errors.Wrapf(errDuplicateDest, dest) - // } - // unifiedMounts[dest] = mount - //} - //for dest, volume := range volumeVolumes { - // if _, ok := unifiedVolumes[dest]; ok { - // return nil, nil, errors.Wrapf(errDuplicateDest, dest) - // } - // unifiedVolumes[dest] = volume - //} - //// Now --tmpfs - //for dest, tmpfs := range tmpfsMounts { - // if _, ok := unifiedMounts[dest]; ok { - // return nil, nil, errors.Wrapf(errDuplicateDest, dest) - // } - // unifiedMounts[dest] = tmpfs - //} - //// Now spec mounts and volumes - //for _, mount := range config.Mounts { - // dest := mount.Destination - // if _, ok := unifiedMounts[dest]; ok { - // return nil, nil, errors.Wrapf(errDuplicateDest, dest) - // } - // unifiedMounts[dest] = mount - //} - //for _, volume := range config.NamedVolumes { - // dest := volume.Dest - // if _, ok := unifiedVolumes[dest]; ok { - // return nil, nil, errors.Wrapf(errDuplicateDest, dest) - // } - // unifiedVolumes[dest] = volume - //} - // - //// If requested, add container init binary - //if config.Init { - // initPath := config.InitPath - // if initPath == "" { - // rtc, err := runtime.GetConfig() - // if err != nil { - // return nil, nil, err - // } - // initPath = rtc.Engine.InitPath - // } - // initMount, err := config.addContainerInitBinary(initPath) - // if err != nil { - // return nil, nil, err - // } - // if _, ok := unifiedMounts[initMount.Destination]; ok { - // return nil, nil, errors.Wrapf(errDuplicateDest, "conflict with mount added by --init to %q", initMount.Destination) - // } - // unifiedMounts[initMount.Destination] = initMount - //} - // - //// Before superseding, we need to find volume mounts which conflict with - //// named volumes, and vice versa. - //// We'll delete the conflicts here as we supersede. - //for dest := range unifiedMounts { - // if _, ok := baseVolumes[dest]; ok { - // delete(baseVolumes, dest) - // } - //} - //for dest := range unifiedVolumes { - // if _, ok := baseMounts[dest]; ok { - // delete(baseMounts, dest) - // } - //} - // - //// Supersede volumes-from/image volumes with unified volumes from above. - //// This is an unconditional replacement. - //for dest, mount := range unifiedMounts { - // baseMounts[dest] = mount - //} - //for dest, volume := range unifiedVolumes { - // baseVolumes[dest] = volume - //} - // - //// If requested, add tmpfs filesystems for read-only containers. - //if config.Security.ReadOnlyRootfs && config.Security.ReadOnlyTmpfs { - // readonlyTmpfs := []string{"/tmp", "/var/tmp", "/run"} - // options := []string{"rw", "rprivate", "nosuid", "nodev", "tmpcopyup"} - // for _, dest := range readonlyTmpfs { - // if _, ok := baseMounts[dest]; ok { - // continue - // } - // if _, ok := baseVolumes[dest]; ok { - // continue - // } - // localOpts := options - // if dest == "/run" { - // localOpts = append(localOpts, "noexec", "size=65536k") - // } else { - // localOpts = append(localOpts, "exec") - // } - // baseMounts[dest] = spec.Mount{ - // Destination: dest, - // Type: "tmpfs", - // Source: "tmpfs", - // Options: localOpts, - // } - // } - //} - // - //// Check for conflicts between named volumes and mounts - //for dest := range baseMounts { - // if _, ok := baseVolumes[dest]; ok { - // return nil, nil, errors.Wrapf(errDuplicateDest, "conflict at mount destination %v", dest) - // } - //} - //for dest := range baseVolumes { - // if _, ok := baseMounts[dest]; ok { - // return nil, nil, errors.Wrapf(errDuplicateDest, "conflict at mount destination %v", dest) - // } - //} - // - //// Final step: maps to arrays - //finalMounts := make([]spec.Mount, 0, len(baseMounts)) - //for _, mount := range baseMounts { - // if mount.Type == TypeBind { - // absSrc, err := filepath.Abs(mount.Source) - // if err != nil { - // return nil, nil, errors.Wrapf(err, "error getting absolute path of %s", mount.Source) - // } - // mount.Source = absSrc - // } - // finalMounts = append(finalMounts, mount) - //} - //finalVolumes := make([]*define.ContainerNamedVolume, 0, len(baseVolumes)) - //for _, volume := range baseVolumes { - // finalVolumes = append(finalVolumes, volume) - //} - - //return finalMounts, finalVolumes, nil - return nil -} - -// Parse volumes from - a set of containers whose volumes we will mount in. -// Grab the containers, retrieve any user-created spec mounts and all named -// volumes, and return a list of them. -// Conflicts are resolved simply - the last container specified wins. -// Container names may be suffixed by mount options after a colon. -// TODO: We should clean these paths if possible -// TODO deferred baude -func getVolumesFrom() (map[string]spec.Mount, map[string]*libpod.ContainerNamedVolume, error) { //nolint - // Both of these are maps of mount destination to mount type. - // We ensure that each destination is only mounted to once in this way. - //finalMounts := make(map[string]spec.Mount) - //finalNamedVolumes := make(map[string]*define.ContainerNamedVolume) - // - //for _, vol := range config.VolumesFrom { - // var ( - // options = []string{} - // err error - // splitVol = strings.SplitN(vol, ":", 2) - // ) - // if len(splitVol) == 2 { - // splitOpts := strings.Split(splitVol[1], ",") - // for _, checkOpt := range splitOpts { - // switch checkOpt { - // case "z", "ro", "rw": - // // Do nothing, these are valid options - // default: - // return nil, nil, errors.Errorf("invalid options %q, can only specify 'ro', 'rw', and 'z'", splitVol[1]) - // } - // } - // - // if options, err = parse.ValidateVolumeOpts(splitOpts); err != nil { - // return nil, nil, err - // } - // } - // ctr, err := runtime.LookupContainer(splitVol[0]) - // if err != nil { - // return nil, nil, errors.Wrapf(err, "error looking up container %q for volumes-from", splitVol[0]) - // } - // - // logrus.Debugf("Adding volumes from container %s", ctr.ID()) - // - // // Look up the container's user volumes. This gets us the - // // destinations of all mounts the user added to the container. - // userVolumesArr := ctr.UserVolumes() - // - // // We're going to need to access them a lot, so convert to a map - // // to reduce looping. - // // We'll also use the map to indicate if we missed any volumes along the way. - // userVolumes := make(map[string]bool) - // for _, dest := range userVolumesArr { - // userVolumes[dest] = false - // } - // - // // Now we get the container's spec and loop through its volumes - // // and append them in if we can find them. - // spec := ctr.Spec() - // if spec == nil { - // return nil, nil, errors.Errorf("error retrieving container %s spec for volumes-from", ctr.ID()) - // } - // for _, mnt := range spec.Mounts { - // if mnt.Type != TypeBind { - // continue - // } - // if _, exists := userVolumes[mnt.Destination]; exists { - // userVolumes[mnt.Destination] = true - // - // if len(options) != 0 { - // mnt.Options = options - // } - // - // if _, ok := finalMounts[mnt.Destination]; ok { - // logrus.Debugf("Overriding mount to %s with new mount from container %s", mnt.Destination, ctr.ID()) - // } - // finalMounts[mnt.Destination] = mnt - // } - // } - // - // // We're done with the spec mounts. Add named volumes. - // // Add these unconditionally - none of them are automatically - // // part of the container, as some spec mounts are. - // namedVolumes := ctr.NamedVolumes() - // for _, namedVol := range namedVolumes { - // if _, exists := userVolumes[namedVol.Dest]; exists { - // userVolumes[namedVol.Dest] = true - // } - // - // if len(options) != 0 { - // namedVol.Options = options - // } - // - // if _, ok := finalMounts[namedVol.Dest]; ok { - // logrus.Debugf("Overriding named volume mount to %s with new named volume from container %s", namedVol.Dest, ctr.ID()) - // } - // finalNamedVolumes[namedVol.Dest] = namedVol - // } - // - // // Check if we missed any volumes - // for volDest, found := range userVolumes { - // if !found { - // logrus.Warnf("Unable to match volume %s from container %s for volumes-from", volDest, ctr.ID()) - // } - // } - //} - // - //return finalMounts, finalNamedVolumes, nil - return nil, nil, nil -} - -// getMounts takes user-provided input from the --mount flag 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 getMounts(mounts []string) (map[string]spec.Mount, map[string]*libpod.ContainerNamedVolume, error) { //nolint - finalMounts := make(map[string]spec.Mount) - finalNamedVolumes := make(map[string]*libpod.ContainerNamedVolume) - - errInvalidSyntax := errors.Errorf("incorrect mount format: should be --mount type=<bind|tmpfs|volume>,[src=<host-dir|volume-name>,]target=<ctr-dir>[,options]") - - // TODO(vrothberg): the manual parsing can be replaced with a regular expression - // to allow a more robust parsing of the mount format and to give - // precise errors regarding supported format versus supported options. - for _, mount := range mounts { - arr := strings.SplitN(mount, ",", 2) - if len(arr) < 2 { - return nil, nil, errors.Wrapf(errInvalidSyntax, "%q", mount) - } - kv := strings.Split(arr[0], "=") - // TODO: type is not explicitly required in Docker. - // If not specified, it defaults to "volume". - if len(kv) != 2 || kv[0] != "type" { - return nil, nil, errors.Wrapf(errInvalidSyntax, "%q", mount) - } - - tokens := strings.Split(arr[1], ",") - switch kv[1] { - case TypeBind: - mount, err := getBindMount(tokens) - if err != nil { - return nil, nil, err - } - if _, ok := finalMounts[mount.Destination]; ok { - return nil, nil, errors.Wrapf(errDuplicateDest, mount.Destination) - } - finalMounts[mount.Destination] = mount - case TypeTmpfs: - mount, err := getTmpfsMount(tokens) - if err != nil { - return nil, nil, err - } - if _, ok := finalMounts[mount.Destination]; ok { - return nil, nil, errors.Wrapf(errDuplicateDest, mount.Destination) - } - finalMounts[mount.Destination] = mount - case "volume": - volume, err := getNamedVolume(tokens) - if err != nil { - return nil, nil, err - } - if _, ok := finalNamedVolumes[volume.Dest]; ok { - return nil, nil, errors.Wrapf(errDuplicateDest, volume.Dest) - } - finalNamedVolumes[volume.Dest] = volume - default: - return nil, nil, errors.Errorf("invalid filesystem type %q", kv[1]) - } - } - - return finalMounts, finalNamedVolumes, nil -} - -// Parse a single bind mount entry from the --mount flag. -func getBindMount(args []string) (spec.Mount, error) { //nolint - newMount := spec.Mount{ - Type: TypeBind, - } - - var setSource, setDest, setRORW, setSuid, setDev, setExec, setRelabel bool - - for _, val := range args { - kv := strings.Split(val, "=") - switch kv[0] { - case "bind-nonrecursive": - newMount.Options = append(newMount.Options, "bind") - case "ro", "rw": - if setRORW { - return newMount, errors.Wrapf(optionArgError, "cannot pass 'ro' or 'rw' options more than once") - } - setRORW = true - // Can be formatted as one of: - // ro - // ro=[true|false] - // rw - // rw=[true|false] - switch len(kv) { - case 1: - newMount.Options = append(newMount.Options, kv[0]) - case 2: - switch strings.ToLower(kv[1]) { - case "true": - newMount.Options = append(newMount.Options, kv[0]) - case "false": - // Set the opposite only for rw - // ro's opposite is the default - if kv[0] == "rw" { - newMount.Options = append(newMount.Options, "ro") - } - default: - return newMount, errors.Wrapf(optionArgError, "%s must be set to true or false, instead received %q", kv[0], kv[1]) - } - default: - return newMount, errors.Wrapf(optionArgError, "badly formatted option %q", val) - } - case "nosuid", "suid": - if setSuid { - return newMount, errors.Wrapf(optionArgError, "cannot pass 'nosuid' and 'suid' options more than once") - } - setSuid = true - newMount.Options = append(newMount.Options, kv[0]) - case "nodev", "dev": - if setDev { - return newMount, errors.Wrapf(optionArgError, "cannot pass 'nodev' and 'dev' options more than once") - } - setDev = true - newMount.Options = append(newMount.Options, kv[0]) - case "noexec", "exec": - if setExec { - return newMount, errors.Wrapf(optionArgError, "cannot pass 'noexec' and 'exec' options more than once") - } - setExec = true - newMount.Options = append(newMount.Options, kv[0]) - case "shared", "rshared", "private", "rprivate", "slave", "rslave", "Z", "z": - newMount.Options = append(newMount.Options, kv[0]) - case "bind-propagation": - if len(kv) == 1 { - return newMount, errors.Wrapf(optionArgError, kv[0]) - } - newMount.Options = append(newMount.Options, kv[1]) - case "src", "source": - if len(kv) == 1 { - return newMount, errors.Wrapf(optionArgError, kv[0]) - } - if err := parse.ValidateVolumeHostDir(kv[1]); err != nil { - return newMount, err - } - newMount.Source = kv[1] - setSource = true - case "target", "dst", "destination": - if len(kv) == 1 { - return newMount, errors.Wrapf(optionArgError, kv[0]) - } - if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { - return newMount, err - } - newMount.Destination = filepath.Clean(kv[1]) - setDest = true - case "relabel": - if setRelabel { - return newMount, errors.Wrapf(optionArgError, "cannot pass 'relabel' option more than once") - } - setRelabel = true - if len(kv) != 2 { - return newMount, errors.Wrapf(util.ErrBadMntOption, "%s mount option must be 'private' or 'shared'", kv[0]) - } - switch kv[1] { - case "private": - newMount.Options = append(newMount.Options, "z") - case "shared": - newMount.Options = append(newMount.Options, "Z") - default: - return newMount, errors.Wrapf(util.ErrBadMntOption, "%s mount option must be 'private' or 'shared'", kv[0]) - } - default: - return newMount, errors.Wrapf(util.ErrBadMntOption, kv[0]) - } - } - - if !setDest { - return newMount, noDestError - } - - if !setSource { - newMount.Source = newMount.Destination - } - - options, err := parse.ValidateVolumeOpts(newMount.Options) - if err != nil { - return newMount, err - } - newMount.Options = options - return newMount, nil -} - -// Parse a single tmpfs mount entry from the --mount flag -func getTmpfsMount(args []string) (spec.Mount, error) { //nolint - newMount := spec.Mount{ - Type: TypeTmpfs, - Source: TypeTmpfs, - } - - var setDest, setRORW, setSuid, setDev, setExec, setTmpcopyup bool - - for _, val := range args { - kv := strings.Split(val, "=") - switch kv[0] { - case "tmpcopyup", "notmpcopyup": - if setTmpcopyup { - return newMount, errors.Wrapf(optionArgError, "cannot pass 'tmpcopyup' and 'notmpcopyup' options more than once") - } - setTmpcopyup = true - newMount.Options = append(newMount.Options, kv[0]) - case "ro", "rw": - if setRORW { - return newMount, errors.Wrapf(optionArgError, "cannot pass 'ro' and 'rw' options more than once") - } - setRORW = true - newMount.Options = append(newMount.Options, kv[0]) - case "nosuid", "suid": - if setSuid { - return newMount, errors.Wrapf(optionArgError, "cannot pass 'nosuid' and 'suid' options more than once") - } - setSuid = true - newMount.Options = append(newMount.Options, kv[0]) - case "nodev", "dev": - if setDev { - return newMount, errors.Wrapf(optionArgError, "cannot pass 'nodev' and 'dev' options more than once") - } - setDev = true - newMount.Options = append(newMount.Options, kv[0]) - case "noexec", "exec": - if setExec { - return newMount, errors.Wrapf(optionArgError, "cannot pass 'noexec' and 'exec' options more than once") - } - setExec = true - newMount.Options = append(newMount.Options, kv[0]) - case "tmpfs-mode": - if len(kv) == 1 { - return newMount, errors.Wrapf(optionArgError, kv[0]) - } - newMount.Options = append(newMount.Options, fmt.Sprintf("mode=%s", kv[1])) - case "tmpfs-size": - if len(kv) == 1 { - return newMount, errors.Wrapf(optionArgError, kv[0]) - } - newMount.Options = append(newMount.Options, fmt.Sprintf("size=%s", kv[1])) - case "src", "source": - return newMount, errors.Errorf("source is not supported with tmpfs mounts") - case "target", "dst", "destination": - if len(kv) == 1 { - return newMount, errors.Wrapf(optionArgError, kv[0]) - } - if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { - return newMount, err - } - newMount.Destination = filepath.Clean(kv[1]) - setDest = true - default: - return newMount, errors.Wrapf(util.ErrBadMntOption, kv[0]) - } - } - - if !setDest { - return newMount, noDestError - } - - return newMount, nil -} - -// Parse a single volume mount entry from the --mount flag. -// Note that the volume-label option for named volumes is currently NOT supported. -// TODO: add support for --volume-label -func getNamedVolume(args []string) (*libpod.ContainerNamedVolume, error) { //nolint - newVolume := new(libpod.ContainerNamedVolume) - - var setSource, setDest, setRORW, setSuid, setDev, setExec bool - - for _, val := range args { - kv := strings.Split(val, "=") - switch kv[0] { - case "ro", "rw": - if setRORW { - return nil, errors.Wrapf(optionArgError, "cannot pass 'ro' and 'rw' options more than once") - } - setRORW = true - newVolume.Options = append(newVolume.Options, kv[0]) - case "nosuid", "suid": - if setSuid { - return nil, errors.Wrapf(optionArgError, "cannot pass 'nosuid' and 'suid' options more than once") - } - setSuid = true - newVolume.Options = append(newVolume.Options, kv[0]) - case "nodev", "dev": - if setDev { - return nil, errors.Wrapf(optionArgError, "cannot pass 'nodev' and 'dev' options more than once") - } - setDev = true - newVolume.Options = append(newVolume.Options, kv[0]) - case "noexec", "exec": - if setExec { - return nil, errors.Wrapf(optionArgError, "cannot pass 'noexec' and 'exec' options more than once") - } - setExec = true - newVolume.Options = append(newVolume.Options, kv[0]) - case "volume-label": - return nil, errors.Errorf("the --volume-label option is not presently implemented") - case "src", "source": - if len(kv) == 1 { - return nil, errors.Wrapf(optionArgError, kv[0]) - } - newVolume.Name = kv[1] - setSource = true - case "target", "dst", "destination": - if len(kv) == 1 { - return nil, errors.Wrapf(optionArgError, kv[0]) - } - if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { - return nil, err - } - newVolume.Dest = filepath.Clean(kv[1]) - setDest = true - default: - return nil, errors.Wrapf(util.ErrBadMntOption, kv[0]) - } - } - - if !setSource { - return nil, errors.Errorf("must set source volume") - } - if !setDest { - return nil, noDestError - } - - return newVolume, nil -} - -func getVolumeMounts(vols []string) (map[string]spec.Mount, map[string]*libpod.ContainerNamedVolume, error) { //nolint - mounts := make(map[string]spec.Mount) - volumes := make(map[string]*libpod.ContainerNamedVolume) - - volumeFormatErr := errors.Errorf("incorrect volume format, should be [host-dir:]ctr-dir[:option]") - - for _, vol := range vols { - var ( - options []string - src string - dest string - err error - ) - - splitVol := strings.Split(vol, ":") - if len(splitVol) > 3 { - return nil, nil, errors.Wrapf(volumeFormatErr, vol) - } - - src = splitVol[0] - if len(splitVol) == 1 { - // This is an anonymous named volume. Only thing given - // is destination. - // Name/source will be blank, and populated by libpod. - src = "" - dest = splitVol[0] - } else if len(splitVol) > 1 { - dest = splitVol[1] - } - if len(splitVol) > 2 { - if options, err = parse.ValidateVolumeOpts(strings.Split(splitVol[2], ",")); err != nil { - return nil, nil, err - } - } - - // Do not check source dir for anonymous volumes - if len(splitVol) > 1 { - if err := parse.ValidateVolumeHostDir(src); err != nil { - return nil, nil, err - } - } - if err := parse.ValidateVolumeCtrDir(dest); err != nil { - return nil, nil, err - } - - cleanDest := filepath.Clean(dest) - - if strings.HasPrefix(src, "/") || strings.HasPrefix(src, ".") { - // This is not a named volume - newMount := spec.Mount{ - Destination: cleanDest, - Type: string(TypeBind), - Source: src, - Options: options, - } - if _, ok := mounts[newMount.Destination]; ok { - return nil, nil, errors.Wrapf(errDuplicateDest, newMount.Destination) - } - mounts[newMount.Destination] = newMount - } else { - // This is a named volume - newNamedVol := new(libpod.ContainerNamedVolume) - newNamedVol.Name = src - newNamedVol.Dest = cleanDest - newNamedVol.Options = options - - if _, ok := volumes[newNamedVol.Dest]; ok { - return nil, nil, errors.Wrapf(errDuplicateDest, newNamedVol.Dest) - } - volumes[newNamedVol.Dest] = newNamedVol - } - - logrus.Debugf("User mount %s:%s options %v", src, dest, options) - } - - return mounts, volumes, nil -} - -// Get mounts for container's image volumes -// TODO deferred baude -func getImageVolumes() (map[string]spec.Mount, map[string]*libpod.ContainerNamedVolume, error) { //nolint - //mounts := make(map[string]spec.Mount) - //volumes := make(map[string]*define.ContainerNamedVolume) - // - //if config.ImageVolumeType == "ignore" { - // return mounts, volumes, nil - //} - // - //for vol := range config.BuiltinImgVolumes { - // cleanDest := filepath.Clean(vol) - // logrus.Debugf("Adding image volume at %s", cleanDest) - // if config.ImageVolumeType == "tmpfs" { - // // Tmpfs image volumes are handled as mounts - // mount := spec.Mount{ - // Destination: cleanDest, - // Source: TypeTmpfs, - // Type: TypeTmpfs, - // Options: []string{"rprivate", "rw", "nodev", "exec"}, - // } - // mounts[cleanDest] = mount - // } else { - // // Anonymous volumes have no name. - // namedVolume := new(define.ContainerNamedVolume) - // namedVolume.Options = []string{"rprivate", "rw", "nodev", "exec"} - // namedVolume.Dest = cleanDest - // volumes[cleanDest] = namedVolume - // } - //} - // - //return mounts, volumes, nil - return nil, nil, nil -} - -// GetTmpfsMounts creates spec.Mount structs for user-requested tmpfs mounts -func getTmpfsMounts(mounts []string) (map[string]spec.Mount, error) { //nolint - m := make(map[string]spec.Mount) - for _, i := range mounts { - // Default options if nothing passed - var options []string - spliti := strings.Split(i, ":") - destPath := spliti[0] - if err := parse.ValidateVolumeCtrDir(spliti[0]); err != nil { - return nil, err - } - if len(spliti) > 1 { - options = strings.Split(spliti[1], ",") - } - - if _, ok := m[destPath]; ok { - return nil, errors.Wrapf(errDuplicateDest, destPath) - } - - mount := spec.Mount{ - Destination: filepath.Clean(destPath), - Type: string(TypeTmpfs), - Options: options, - Source: string(TypeTmpfs), - } - m[destPath] = mount - } - return m, nil -} - -// AddContainerInitBinary adds the init binary specified by path iff the -// container will run in a private PID namespace that is not shared with the -// host or another pre-existing container, where an init-like process is -// already running. -// -// Note that AddContainerInitBinary prepends "/dev/init" "--" to the command -// to execute the bind-mounted binary as PID 1. -// TODO this needs to be worked on to work in new env -func addContainerInitBinary(path string) (spec.Mount, error) { //nolint - mount := spec.Mount{ - Destination: "/dev/init", - Type: TypeBind, - Source: path, - Options: []string{TypeBind, "ro"}, - } - - //if path == "" { - // return mount, fmt.Errorf("please specify a path to the container-init binary") - //} - //if !config.Pid.PidMode.IsPrivate() { - // return mount, fmt.Errorf("cannot add init binary as PID 1 (PID namespace isn't private)") - //} - //if config.Systemd { - // return mount, fmt.Errorf("cannot use container-init binary with systemd") - //} - //if _, err := os.Stat(path); os.IsNotExist(err) { - // return mount, errors.Wrap(err, "container-init binary not found on the host") - //} - //config.Command = append([]string{"/dev/init", "--"}, config.Command...) - return mount, nil -} - // Supersede existing mounts in the spec with new, user-specified mounts. // TODO: Should we unmount subtree mounts? E.g., if /tmp/ is mounted by // one mount, and we already have /tmp/a and /tmp/b, should we remove diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index e102a3234..37f2b3190 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -171,7 +171,7 @@ type ContainerStorageConfig struct { // These will supersede Image Volumes and VolumesFrom volumes where // there are conflicts. // Optional. - Volumes []*Volumes `json:"volumes,omitempty"` + Volumes []*NamedVolume `json:"volumes,omitempty"` // Devices are devices that will be added to the container. // Optional. Devices []spec.LinuxDevice `json:"devices,omitempty"` @@ -387,10 +387,17 @@ type SpecGenerator struct { ContainerHealthCheckConfig } -// Volumes is a temporary struct to hold input from the User -type Volumes struct { - Name string - Dest string +// NamedVolume holds information about a named volume that will be mounted into +// the container. +type NamedVolume struct { + // Name is the name of the named volume to be mounted. May be empty. + // If empty, a new named volume with a pseudorandomly generated name + // will be mounted at the given destination. + Name string + // Destination to mount the named volume within the container. Must be + // an absolute path. Path will be created if it does not exist. + Dest string + // Options are options that the named volume will be mounted with. Options []string } diff --git a/test/e2e/manifest_test.go b/test/e2e/manifest_test.go new file mode 100644 index 000000000..a52916e87 --- /dev/null +++ b/test/e2e/manifest_test.go @@ -0,0 +1,88 @@ +package integration + +import ( + "os" + + . "github.com/containers/libpod/test/utils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Podman manifest", func() { + var ( + tempdir string + err error + podmanTest *PodmanTestIntegration + ) + + const ( + imageList = "docker://k8s.gcr.io/pause:3.1" + imageListInstance = "docker://k8s.gcr.io/pause@sha256:f365626a556e58189fc21d099fc64603db0f440bff07f77c740989515c544a39" + imageListARM64InstanceDigest = "sha256:f365626a556e58189fc21d099fc64603db0f440bff07f77c740989515c544a39" + imageListAMD64InstanceDigest = "sha256:59eec8837a4d942cc19a52b8c09ea75121acc38114a2c68b98983ce9356b8610" + imageListARMInstanceDigest = "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53" + imageListPPC64LEInstanceDigest = "sha256:bcf9771c0b505e68c65440474179592ffdfa98790eb54ffbf129969c5e429990" + imageListS390XInstanceDigest = "sha256:882a20ee0df7399a445285361d38b711c299ca093af978217112c73803546d5e" + ) + + BeforeEach(func() { + tempdir, err = CreateTempDirInTempDir() + if err != nil { + os.Exit(1) + } + podmanTest = PodmanTestCreate(tempdir) + podmanTest.Setup() + podmanTest.SeedImages() + }) + + AfterEach(func() { + podmanTest.Cleanup() + f := CurrentGinkgoTestDescription() + processTestResult(f) + + }) + It("podman manifest create", func() { + session := podmanTest.Podman([]string{"manifest", "create", "foo"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + }) + + It("podman manifest add", func() { + session := podmanTest.Podman([]string{"manifest", "create", "foo"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + session = podmanTest.Podman([]string{"manifest", "add", "--arch=arm64", "foo", imageListInstance}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + }) + + It("podman manifest add one", func() { + session := podmanTest.Podman([]string{"manifest", "create", "foo"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + session = podmanTest.Podman([]string{"manifest", "add", "--arch=arm64", "foo", imageListInstance}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + session = podmanTest.Podman([]string{"manifest", "inspect", "foo"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring(imageListARM64InstanceDigest)) + }) + + It("podman manifest add --all", func() { + session := podmanTest.Podman([]string{"manifest", "create", "foo"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + session = podmanTest.Podman([]string{"manifest", "add", "--all", "foo", imageList}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + session = podmanTest.Podman([]string{"manifest", "inspect", "foo"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring(imageListAMD64InstanceDigest)) + Expect(session.OutputToString()).To(ContainSubstring(imageListARMInstanceDigest)) + Expect(session.OutputToString()).To(ContainSubstring(imageListARM64InstanceDigest)) + Expect(session.OutputToString()).To(ContainSubstring(imageListPPC64LEInstanceDigest)) + Expect(session.OutputToString()).To(ContainSubstring(imageListS390XInstanceDigest)) + }) +}) diff --git a/test/e2e/rmi_test.go b/test/e2e/rmi_test.go index 765d2b19e..d556cbc72 100644 --- a/test/e2e/rmi_test.go +++ b/test/e2e/rmi_test.go @@ -7,6 +7,7 @@ import ( . "github.com/containers/libpod/test/utils" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" ) var _ = Describe("Podman rmi", func() { @@ -17,7 +18,6 @@ var _ = Describe("Podman rmi", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) @@ -37,21 +37,21 @@ var _ = Describe("Podman rmi", func() { It("podman rmi bogus image", func() { session := podmanTest.Podman([]string{"rmi", "debian:6.0.10"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(1)) + Expect(session).Should(Exit(1)) }) It("podman rmi with fq name", func() { session := podmanTest.PodmanNoCache([]string{"rmi", ALPINE}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) }) It("podman rmi with short name", func() { session := podmanTest.PodmanNoCache([]string{"rmi", "alpine"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) }) @@ -62,7 +62,7 @@ var _ = Describe("Podman rmi", func() { images := podmanTest.PodmanNoCache([]string{"images"}) images.WaitWithDefaultTimeout() fmt.Println(images.OutputToStringArray()) - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) }) @@ -70,22 +70,22 @@ var _ = Describe("Podman rmi", func() { podmanTest.RestoreArtifact(nginx) session := podmanTest.PodmanNoCache([]string{"rmi", "-fa"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) }) It("podman rmi tagged image", func() { setup := podmanTest.PodmanNoCache([]string{"images", "-q", ALPINE}) setup.WaitWithDefaultTimeout() - Expect(setup.ExitCode()).To(Equal(0)) + Expect(setup).Should(Exit(0)) session := podmanTest.PodmanNoCache([]string{"tag", "alpine", "foo:bar", "foo"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) result := podmanTest.PodmanNoCache([]string{"images", "-q", "foo"}) result.WaitWithDefaultTimeout() - Expect(result.ExitCode()).To(Equal(0)) + Expect(result).Should(Exit(0)) Expect(result.LineInOutputContains(setup.OutputToString())).To(BeTrue()) }) @@ -93,12 +93,12 @@ var _ = Describe("Podman rmi", func() { It("podman rmi image with tags by ID cannot be done without force", func() { setup := podmanTest.PodmanNoCache([]string{"images", "-q", ALPINE}) setup.WaitWithDefaultTimeout() - Expect(setup.ExitCode()).To(Equal(0)) + Expect(setup).Should(Exit(0)) alpineId := setup.OutputToString() session := podmanTest.PodmanNoCache([]string{"tag", "alpine", "foo:bar", "foo"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) // Trying without --force should fail result := podmanTest.PodmanNoCache([]string{"rmi", alpineId}) @@ -108,80 +108,80 @@ var _ = Describe("Podman rmi", func() { // With --force it should work resultForce := podmanTest.PodmanNoCache([]string{"rmi", "-f", alpineId}) resultForce.WaitWithDefaultTimeout() - Expect(resultForce.ExitCode()).To(Equal(0)) + Expect(resultForce).Should(Exit(0)) }) It("podman rmi image that is a parent of another image", func() { SkipIfRemote() session := podmanTest.PodmanNoCache([]string{"rmi", "-fa"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session = podmanTest.PodmanNoCache([]string{"run", "--name", "c_test", ALPINE, "true"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session = podmanTest.PodmanNoCache([]string{"commit", "-q", "c_test", "test"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session = podmanTest.PodmanNoCache([]string{"rm", "c_test"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session = podmanTest.PodmanNoCache([]string{"rmi", ALPINE}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session = podmanTest.PodmanNoCache([]string{"images", "-q"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) Expect(len(session.OutputToStringArray())).To(Equal(1)) session = podmanTest.PodmanNoCache([]string{"images", "-q", "-a"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) Expect(len(session.OutputToStringArray())).To(Equal(2)) untaggedImg := session.OutputToStringArray()[1] session = podmanTest.PodmanNoCache([]string{"rmi", "-f", untaggedImg}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(2)) + Expect(session).Should(Exit(2)) }) It("podman rmi image that is created from another named imaged", func() { SkipIfRemote() session := podmanTest.PodmanNoCache([]string{"rmi", "-fa"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session = podmanTest.PodmanNoCache([]string{"create", "--name", "c_test1", ALPINE, "true"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session = podmanTest.PodmanNoCache([]string{"commit", "-q", "c_test1", "test1"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session = podmanTest.PodmanNoCache([]string{"create", "--name", "c_test2", "test1", "true"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session = podmanTest.PodmanNoCache([]string{"commit", "-q", "c_test2", "test2"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session = podmanTest.PodmanNoCache([]string{"rm", "-a"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session = podmanTest.PodmanNoCache([]string{"rmi", "test2"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session = podmanTest.PodmanNoCache([]string{"images", "-q"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) Expect(len(session.OutputToStringArray())).To(Equal(2)) }) @@ -189,7 +189,7 @@ var _ = Describe("Podman rmi", func() { SkipIfRemote() session := podmanTest.PodmanNoCache([]string{"rmi", "-fa"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) dockerfile := `FROM docker.io/library/alpine:latest RUN mkdir hello @@ -208,51 +208,51 @@ var _ = Describe("Podman rmi", func() { session = podmanTest.PodmanNoCache([]string{"images", "-q", "-a"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) numOfImages := len(session.OutputToStringArray()) session = podmanTest.PodmanNoCache([]string{"rmi", "test2"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session = podmanTest.PodmanNoCache([]string{"images", "-q", "-a"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) Expect(numOfImages - len(session.OutputToStringArray())).To(Equal(2)) session = podmanTest.PodmanNoCache([]string{"rmi", "test"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session = podmanTest.PodmanNoCache([]string{"images", "-q", "-a"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) Expect(len(session.OutputToStringArray())).To(Equal(1)) podmanTest.BuildImage(dockerfile, "test3", "true") session = podmanTest.PodmanNoCache([]string{"rmi", ALPINE}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session = podmanTest.PodmanNoCache([]string{"rmi", "test3"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session = podmanTest.PodmanNoCache([]string{"images", "-q", "-a"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) Expect(len(session.OutputToString())).To(Equal(0)) }) It("podman rmi -a with no images should be exit 0", func() { session := podmanTest.PodmanNoCache([]string{"rmi", "-fa"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) session2 := podmanTest.PodmanNoCache([]string{"rmi", "-fa"}) session2.WaitWithDefaultTimeout() - Expect(session2.ExitCode()).To(Equal(0)) + Expect(session2).Should(Exit(0)) }) It("podman rmi -a with parent|child images", func() { @@ -269,11 +269,11 @@ RUN find $LOCAL session := podmanTest.PodmanNoCache([]string{"rmi", "-a"}) session.WaitWithDefaultTimeout() fmt.Println(session.OutputToString()) - Expect(session.ExitCode()).To(Equal(0)) + Expect(session).Should(Exit(0)) images := podmanTest.PodmanNoCache([]string{"images", "-aq"}) images.WaitWithDefaultTimeout() - Expect(images.ExitCode()).To(Equal(0)) + Expect(images).Should(Exit(0)) Expect(len(images.OutputToStringArray())).To(Equal(0)) }) @@ -282,7 +282,7 @@ RUN find $LOCAL It("podman image rm is the same as rmi", func() { session := podmanTest.PodmanNoCache([]string{"image", "rm"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(125)) + Expect(session).Should(Exit(125)) match, _ := session.ErrorGrepString("image name or ID must be specified") Expect(match).To(BeTrue()) }) |