diff options
| -rw-r--r-- | cmd/podmanV2/images/exists.go | 40 | ||||
| -rw-r--r-- | cmd/podmanV2/images/image.go | 6 | ||||
| -rw-r--r-- | cmd/podmanV2/images/images.go | 14 | ||||
| -rw-r--r-- | cmd/podmanV2/images/rm.go | 70 | ||||
| -rw-r--r-- | cmd/podmanV2/images/rmi.go | 30 | ||||
| -rw-r--r-- | cmd/podmanV2/main.go | 8 | ||||
| -rw-r--r-- | cmd/podmanV2/registry/registry.go | 2 | ||||
| -rw-r--r-- | cmd/podmanV2/root.go | 72 | ||||
| -rw-r--r-- | libpod/networking_linux.go | 33 | ||||
| -rw-r--r-- | pkg/api/handlers/compat/images_remove.go | 22 | ||||
| -rw-r--r-- | pkg/domain/entities/engine_image.go | 3 | ||||
| -rw-r--r-- | pkg/domain/entities/images.go | 12 | ||||
| -rw-r--r-- | pkg/domain/infra/abi/images.go | 83 | ||||
| -rw-r--r-- | pkg/domain/infra/tunnel/images.go | 33 | 
14 files changed, 354 insertions, 74 deletions
diff --git a/cmd/podmanV2/images/exists.go b/cmd/podmanV2/images/exists.go new file mode 100644 index 000000000..d35d6825e --- /dev/null +++ b/cmd/podmanV2/images/exists.go @@ -0,0 +1,40 @@ +package images + +import ( +	"os" + +	"github.com/containers/libpod/cmd/podmanV2/registry" +	"github.com/containers/libpod/pkg/domain/entities" +	"github.com/spf13/cobra" +) + +var ( +	existsCmd = &cobra.Command{ +		Use:   "exists IMAGE", +		Short: "Check if an image exists in local storage", +		Long:  `If the named image exists in local storage, podman image exists exits with 0, otherwise the exit code will be 1.`, +		Args:  cobra.ExactArgs(1), +		RunE:  exists, +		Example: `podman image exists ID +  podman image exists IMAGE && podman pull IMAGE`, +	} +) + +func init() { +	registry.Commands = append(registry.Commands, registry.CliCommand{ +		Mode:    []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, +		Command: existsCmd, +		Parent:  imageCmd, +	}) +} + +func exists(cmd *cobra.Command, args []string) error { +	found, err := registry.ImageEngine().Exists(registry.GetContext(), args[0]) +	if err != nil { +		return err +	} +	if !found.Value { +		os.Exit(1) +	} +	return nil +} diff --git a/cmd/podmanV2/images/image.go b/cmd/podmanV2/images/image.go index a15c3e826..9fc7b21d1 100644 --- a/cmd/podmanV2/images/image.go +++ b/cmd/podmanV2/images/image.go @@ -28,6 +28,8 @@ func init() {  }  func preRunE(cmd *cobra.Command, args []string) error { -	_, err := registry.NewImageEngine(cmd, args) -	return err +	if _, err := registry.NewImageEngine(cmd, args); err != nil { +		return err +	} +	return nil  } diff --git a/cmd/podmanV2/images/images.go b/cmd/podmanV2/images/images.go index 719846b4c..f248aa65f 100644 --- a/cmd/podmanV2/images/images.go +++ b/cmd/podmanV2/images/images.go @@ -11,13 +11,13 @@ import (  var (  	// podman _images_  Alias for podman image _list_  	imagesCmd = &cobra.Command{ -		Use:               strings.Replace(listCmd.Use, "list", "images", 1), -		Args:              listCmd.Args, -		Short:             listCmd.Short, -		Long:              listCmd.Long, -		PersistentPreRunE: preRunE, -		RunE:              listCmd.RunE, -		Example:           strings.Replace(listCmd.Example, "podman image list", "podman images", -1), +		Use:     strings.Replace(listCmd.Use, "list", "images", 1), +		Args:    listCmd.Args, +		Short:   listCmd.Short, +		Long:    listCmd.Long, +		PreRunE: listCmd.PreRunE, +		RunE:    listCmd.RunE, +		Example: strings.Replace(listCmd.Example, "podman image list", "podman images", -1),  	}  ) diff --git a/cmd/podmanV2/images/rm.go b/cmd/podmanV2/images/rm.go new file mode 100644 index 000000000..bb5880de3 --- /dev/null +++ b/cmd/podmanV2/images/rm.go @@ -0,0 +1,70 @@ +package images + +import ( +	"fmt" +	"os" + +	"github.com/containers/libpod/cmd/podmanV2/registry" +	"github.com/containers/libpod/pkg/domain/entities" +	"github.com/pkg/errors" +	"github.com/spf13/cobra" +) + +var ( +	rmDescription = "Removes one or more previously pulled or locally created images." +	rmCmd         = &cobra.Command{ +		Use:     "rm [flags] IMAGE [IMAGE...]", +		Short:   "Removes one or more images from local storage", +		Long:    rmDescription, +		PreRunE: preRunE, +		RunE:    rm, +		Example: `podman image rm imageID +  podman image rm --force alpine +  podman image rm c4dfb1609ee2 93fd78260bd1 c0ed59d05ff7`, +	} + +	imageOpts = entities.ImageDeleteOptions{} +) + +func init() { +	registry.Commands = append(registry.Commands, registry.CliCommand{ +		Mode:    []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, +		Command: rmCmd, +		Parent:  imageCmd, +	}) + +	flags := rmCmd.Flags() +	flags.BoolVarP(&imageOpts.All, "all", "a", false, "Remove all images") +	flags.BoolVarP(&imageOpts.Force, "force", "f", false, "Force Removal of the image") +} + +func rm(cmd *cobra.Command, args []string) error { + +	if len(args) < 1 && !imageOpts.All { +		return errors.Errorf("image name or ID must be specified") +	} +	if len(args) > 0 && imageOpts.All { +		return errors.Errorf("when using the --all switch, you may not pass any images names or IDs") +	} + +	report, err := registry.ImageEngine().Delete(registry.GetContext(), args, imageOpts) +	if err != nil { +		switch { +		case report != nil && report.ImageNotFound != nil: +			fmt.Fprintln(os.Stderr, err.Error()) +			registry.SetExitCode(2) +		case report != nil && report.ImageInUse != nil: +			fmt.Fprintln(os.Stderr, err.Error()) +		default: +			return err +		} +	} + +	for _, u := range report.Untagged { +		fmt.Println("Untagged: " + u) +	} +	for _, d := range report.Deleted { +		fmt.Println("Deleted: " + d) +	} +	return nil +} diff --git a/cmd/podmanV2/images/rmi.go b/cmd/podmanV2/images/rmi.go new file mode 100644 index 000000000..7f9297bc9 --- /dev/null +++ b/cmd/podmanV2/images/rmi.go @@ -0,0 +1,30 @@ +package images + +import ( +	"strings" + +	"github.com/containers/libpod/cmd/podmanV2/registry" +	"github.com/containers/libpod/pkg/domain/entities" +	"github.com/spf13/cobra" +) + +var ( +	rmiCmd = &cobra.Command{ +		Use:     strings.Replace(rmCmd.Use, "rm ", "rmi ", 1), +		Args:    rmCmd.Args, +		Short:   rmCmd.Short, +		Long:    rmCmd.Long, +		PreRunE: rmCmd.PreRunE, +		RunE:    rmCmd.RunE, +		Example: strings.Replace(rmCmd.Example, "podman image rm", "podman rmi", -1), +	} +) + +func init() { +	registry.Commands = append(registry.Commands, registry.CliCommand{ +		Mode:    []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, +		Command: rmiCmd, +	}) +	rmiCmd.SetHelpTemplate(registry.HelpTemplate()) +	rmiCmd.SetUsageTemplate(registry.UsageTemplate()) +} diff --git a/cmd/podmanV2/main.go b/cmd/podmanV2/main.go index dc96c26d0..bd9fbb25e 100644 --- a/cmd/podmanV2/main.go +++ b/cmd/podmanV2/main.go @@ -1,7 +1,6 @@  package main  import ( -	"fmt"  	"os"  	"reflect"  	"runtime" @@ -16,7 +15,6 @@ import (  	"github.com/containers/libpod/libpod"  	"github.com/containers/libpod/pkg/domain/entities"  	"github.com/sirupsen/logrus" -	"github.com/spf13/cobra"  )  func init() { @@ -24,10 +22,7 @@ func init() {  		logrus.Errorf(err.Error())  		os.Exit(1)  	} -	initCobra() -} -func initCobra() {  	switch runtime.GOOS {  	case "darwin":  		fallthrough @@ -46,12 +41,9 @@ func initCobra() {  			registry.EngineOptions.EngineMode = entities.TunnelMode  		}  	} - -	cobra.OnInitialize(func() {})  }  func main() { -	fmt.Fprintf(os.Stderr, "Number of commands: %d\n", len(registry.Commands))  	for _, c := range registry.Commands {  		if Contains(registry.EngineOptions.EngineMode, c.Mode) {  			parent := rootCmd diff --git a/cmd/podmanV2/registry/registry.go b/cmd/podmanV2/registry/registry.go index f0650a7cf..5cdb8a840 100644 --- a/cmd/podmanV2/registry/registry.go +++ b/cmd/podmanV2/registry/registry.go @@ -10,6 +10,8 @@ import (  	"github.com/spf13/cobra"  ) +type CobraFuncs func(cmd *cobra.Command, args []string) error +  type CliCommand struct {  	Mode    []entities.EngineMode  	Command *cobra.Command diff --git a/cmd/podmanV2/root.go b/cmd/podmanV2/root.go index 68e8b4531..cb4cb4e00 100644 --- a/cmd/podmanV2/root.go +++ b/cmd/podmanV2/root.go @@ -2,24 +2,35 @@ package main  import (  	"fmt" +	"log/syslog"  	"os"  	"path"  	"github.com/containers/libpod/cmd/podmanV2/registry"  	"github.com/containers/libpod/libpod/define" +	"github.com/containers/libpod/pkg/domain/entities"  	"github.com/containers/libpod/version" +	"github.com/sirupsen/logrus" +	logrusSyslog "github.com/sirupsen/logrus/hooks/syslog"  	"github.com/spf13/cobra"  ) -var rootCmd = &cobra.Command{ -	Use:              path.Base(os.Args[0]), -	Long:             "Manage pods, containers and images", -	SilenceUsage:     true, -	SilenceErrors:    true, -	TraverseChildren: true, -	RunE:             registry.SubCommandExists, -	Version:          version.Version, -} +var ( +	rootCmd = &cobra.Command{ +		Use:               path.Base(os.Args[0]), +		Long:              "Manage pods, containers and images", +		SilenceUsage:      true, +		SilenceErrors:     true, +		TraverseChildren:  true, +		PersistentPreRunE: preRunE, +		RunE:              registry.SubCommandExists, +		Version:           version.Version, +	} + +	logLevels = entities.NewStringSet("debug", "info", "warn", "error", "fatal", "panic") +	logLevel  = "error" +	useSyslog bool +)  func init() {  	// Override default --help information of `--version` global flag} @@ -28,6 +39,49 @@ func init() {  	rootCmd.PersistentFlags().BoolVar(&dummyVersion, "version", false, "Version of Podman")  	rootCmd.PersistentFlags().StringVarP(®istry.EngineOptions.Uri, "remote", "r", "", "URL to access Podman service")  	rootCmd.PersistentFlags().StringSliceVar(®istry.EngineOptions.Identities, "identity", []string{}, "path to SSH identity file") +	rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "error", fmt.Sprintf("Log messages above specified level (%s)", logLevels.String())) +	rootCmd.PersistentFlags().BoolVar(&useSyslog, "syslog", false, "Output logging information to syslog as well as the console (default false)") + +	cobra.OnInitialize( +		logging, +		syslogHook, +	) +} + +func preRunE(cmd *cobra.Command, args []string) error { +	cmd.SetHelpTemplate(registry.HelpTemplate()) +	cmd.SetUsageTemplate(registry.UsageTemplate()) +	return nil +} + +func logging() { +	if !logLevels.Contains(logLevel) { +		fmt.Fprintf(os.Stderr, "Log Level \"%s\" is not supported, choose from: %s\n", logLevel, logLevels.String()) +		os.Exit(1) +	} + +	level, err := logrus.ParseLevel(logLevel) +	if err != nil { +		fmt.Fprintf(os.Stderr, err.Error()) +		os.Exit(1) +	} +	logrus.SetLevel(level) + +	if logrus.IsLevelEnabled(logrus.InfoLevel) { +		logrus.Infof("%s filtering at log level %s", os.Args[0], logrus.GetLevel()) +	} +} + +func syslogHook() { +	if useSyslog { +		hook, err := logrusSyslog.NewSyslogHook("", "", syslog.LOG_INFO, "") +		if err != nil { +			logrus.WithError(err).Error("Failed to initialize syslog hook") +		} +		if err == nil { +			logrus.AddHook(hook) +		} +	}  }  func Execute() { diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go index f1bf79ce7..a7f501bfe 100644 --- a/libpod/networking_linux.go +++ b/libpod/networking_linux.go @@ -154,13 +154,25 @@ func (r *Runtime) createNetNS(ctr *Container) (n ns.NetNS, q []*cnitypes.Result,  	return ctrNS, networkStatus, err  } -func checkSlirpFlags(path string) (bool, bool, bool, error) { +type slirpFeatures struct { +	HasDisableHostLoopback bool +	HasMTU                 bool +	HasEnableSandbox       bool +	HasEnableSeccomp       bool +} + +func checkSlirpFlags(path string) (*slirpFeatures, error) {  	cmd := exec.Command(path, "--help")  	out, err := cmd.CombinedOutput()  	if err != nil { -		return false, false, false, errors.Wrapf(err, "slirp4netns %q", out) -	} -	return strings.Contains(string(out), "--disable-host-loopback"), strings.Contains(string(out), "--mtu"), strings.Contains(string(out), "--enable-sandbox"), nil +		return nil, errors.Wrapf(err, "slirp4netns %q", out) +	} +	return &slirpFeatures{ +		HasDisableHostLoopback: strings.Contains(string(out), "--disable-host-loopback"), +		HasMTU:                 strings.Contains(string(out), "--mtu"), +		HasEnableSandbox:       strings.Contains(string(out), "--enable-sandbox"), +		HasEnableSeccomp:       strings.Contains(string(out), "--enable-seccomp"), +	}, nil  }  // Configure the network namespace for a rootless container @@ -187,19 +199,22 @@ func (r *Runtime) setupRootlessNetNS(ctr *Container) (err error) {  	logPath := filepath.Join(ctr.runtime.config.TmpDir, fmt.Sprintf("slirp4netns-%s.log", ctr.config.ID))  	cmdArgs := []string{} -	dhp, mtu, sandbox, err := checkSlirpFlags(path) +	slirpFeatures, err := checkSlirpFlags(path)  	if err != nil {  		return errors.Wrapf(err, "error checking slirp4netns binary %s: %q", path, err)  	} -	if dhp { +	if slirpFeatures.HasDisableHostLoopback {  		cmdArgs = append(cmdArgs, "--disable-host-loopback")  	} -	if mtu { +	if slirpFeatures.HasMTU {  		cmdArgs = append(cmdArgs, "--mtu", "65520")  	} -	if sandbox { +	if slirpFeatures.HasEnableSandbox {  		cmdArgs = append(cmdArgs, "--enable-sandbox")  	} +	if slirpFeatures.HasEnableSeccomp { +		cmdArgs = append(cmdArgs, "--enable-seccomp") +	}  	// the slirp4netns arguments being passed are describes as follows:  	// from the slirp4netns documentation: https://github.com/rootless-containers/slirp4netns @@ -230,7 +245,7 @@ func (r *Runtime) setupRootlessNetNS(ctr *Container) (err error) {  	}  	// workaround for https://github.com/rootless-containers/slirp4netns/pull/153 -	if sandbox { +	if slirpFeatures.HasEnableSandbox {  		cmd.SysProcAttr.Cloneflags = syscall.CLONE_NEWNS  		cmd.SysProcAttr.Unshareflags = syscall.CLONE_NEWNS  	} diff --git a/pkg/api/handlers/compat/images_remove.go b/pkg/api/handlers/compat/images_remove.go index 3d346543e..ed0153529 100644 --- a/pkg/api/handlers/compat/images_remove.go +++ b/pkg/api/handlers/compat/images_remove.go @@ -36,17 +36,23 @@ func RemoveImage(w http.ResponseWriter, r *http.Request) {  		return  	} -	_, err = runtime.RemoveImage(r.Context(), newImage, query.Force) +	results, err := runtime.RemoveImage(r.Context(), newImage, query.Force)  	if err != nil {  		utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err)  		return  	} -	// TODO -	// This will need to be fixed for proper response, like Deleted: and Untagged: -	m := make(map[string]string) -	m["Deleted"] = newImage.ID() -	foo := []map[string]string{} -	foo = append(foo, m) -	utils.WriteResponse(w, http.StatusOK, foo) + +	response := make([]map[string]string, 0, len(results.Untagged)+1) +	deleted := make(map[string]string, 1) +	deleted["Deleted"] = results.Deleted +	response = append(response, deleted) + +	for _, u := range results.Untagged { +		untagged := make(map[string]string, 1) +		untagged["Untagged"] = u +		response = append(response, untagged) +	} + +	utils.WriteResponse(w, http.StatusOK, response)  } diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index d44fdaf53..d0c860a04 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -5,7 +5,8 @@ import (  )  type ImageEngine interface { -	Delete(ctx context.Context, nameOrId string, opts ImageDeleteOptions) (*ImageDeleteReport, error) +	Delete(ctx context.Context, nameOrId []string, opts ImageDeleteOptions) (*ImageDeleteReport, error) +	Exists(ctx context.Context, nameOrId string) (*BoolReport, error)  	History(ctx context.Context, nameOrId string, opts ImageHistoryOptions) (*ImageHistoryReport, error)  	List(ctx context.Context, opts ImageListOptions) ([]*ImageSummary, error)  	Prune(ctx context.Context, opts ImagePruneOptions) (*ImagePruneReport, error) diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index f04317e37..4a51b3de4 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -81,14 +81,18 @@ func (i *ImageSummary) IsDangling() bool {  }  type ImageDeleteOptions struct { +	All   bool  	Force bool  } -// ImageDeleteResponse is the response for removing an image from storage and containers -// what was untagged vs actually removed +// ImageDeleteResponse is the response for removing one or more image(s) from storage +// and containers what was untagged vs actually removed  type ImageDeleteReport struct { -	Untagged []string `json:"untagged"` -	Deleted  string   `json:"deleted"` +	Untagged      []string `json:",omitempty"` +	Deleted       []string `json:",omitempty"` +	Errors        []error +	ImageNotFound error +	ImageInUse    error  }  type ImageHistoryOptions struct{} diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 6e9d7f566..203f14987 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -4,30 +4,91 @@ package abi  import (  	"context" +	"fmt"  	libpodImage "github.com/containers/libpod/libpod/image"  	"github.com/containers/libpod/pkg/domain/entities" -	"github.com/containers/libpod/pkg/domain/utils" +	"github.com/containers/storage" +	"github.com/pkg/errors"  ) -func (ir *ImageEngine) Delete(ctx context.Context, nameOrId string, opts entities.ImageDeleteOptions) (*entities.ImageDeleteReport, error) { -	image, err := ir.Libpod.ImageRuntime().NewFromLocal(nameOrId) -	if err != nil { -		return nil, err +func (ir *ImageEngine) Exists(_ context.Context, nameOrId string) (*entities.BoolReport, error) { +	if _, err := ir.Libpod.ImageRuntime().NewFromLocal(nameOrId); err != nil { +		return &entities.BoolReport{}, nil  	} +	return &entities.BoolReport{Value: true}, nil +} -	results, err := ir.Libpod.RemoveImage(ctx, image, opts.Force) -	if err != nil { -		return nil, err +func (ir *ImageEngine) Delete(ctx context.Context, nameOrId []string, opts entities.ImageDeleteOptions) (*entities.ImageDeleteReport, error) { +	report := entities.ImageDeleteReport{} + +	if opts.All { +		var previousTargets []*libpodImage.Image +	repeatRun: +		targets, err := ir.Libpod.ImageRuntime().GetRWImages() +		if err != nil { +			return &report, errors.Wrapf(err, "unable to query local images") +		} + +		if len(targets) > 0 && len(targets) == len(previousTargets) { +			return &report, errors.New("unable to delete all images; re-run the rmi command again.") +		} +		previousTargets = targets + +		for _, img := range targets { +			isParent, err := img.IsParent(ctx) +			if err != nil { +				return &report, err +			} +			if isParent { +				continue +			} +			err = ir.deleteImage(ctx, img, opts, report) +			report.Errors = append(report.Errors, err) +		} +		if len(targets) >= 0 || len(previousTargets) != 1 { +			goto repeatRun +		} +		return &report, nil  	} -	report := entities.ImageDeleteReport{} -	if err := utils.DeepCopy(&report, results); err != nil { -		return nil, err +	for _, id := range nameOrId { +		image, err := ir.Libpod.ImageRuntime().NewFromLocal(id) +		if err != nil { +			return nil, err +		} + +		err = ir.deleteImage(ctx, image, opts, report) +		if err != nil { +			return &report, err +		}  	}  	return &report, nil  } +func (ir *ImageEngine) deleteImage(ctx context.Context, img *libpodImage.Image, opts entities.ImageDeleteOptions, report entities.ImageDeleteReport) error { +	results, err := ir.Libpod.RemoveImage(ctx, img, opts.Force) +	switch errors.Cause(err) { +	case nil: +		break +	case storage.ErrImageUsedByContainer: +		report.ImageInUse = 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())) +		return nil +	case libpodImage.ErrNoSuchImage: +		report.ImageNotFound = err +		return nil +	default: +		return err +	} + +	report.Deleted = append(report.Deleted, results.Deleted) +	for _, e := range results.Untagged { +		report.Untagged = append(report.Untagged, e) +	} +	return nil +} +  func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOptions) (*entities.ImagePruneReport, error) {  	results, err := ir.Libpod.ImageRuntime().PruneImages(ctx, opts.All, []string{})  	if err != nil { diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 60df40498..6a241641e 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -9,27 +9,30 @@ import (  	"github.com/containers/libpod/pkg/domain/utils"  ) -func (ir *ImageEngine) Delete(ctx context.Context, nameOrId string, opts entities.ImageDeleteOptions) (*entities.ImageDeleteReport, error) { -	results, err := images.Remove(ir.ClientCxt, nameOrId, &opts.Force) -	if err != nil { -		return nil, err -	} +func (ir *ImageEngine) Exists(_ context.Context, nameOrId string) (*entities.BoolReport, error) { +	found, err := images.Exists(ir.ClientCxt, nameOrId) +	return &entities.BoolReport{Value: found}, err +} -	report := entities.ImageDeleteReport{ -		Untagged: nil, -		Deleted:  "", -	} +func (ir *ImageEngine) Delete(ctx context.Context, nameOrId []string, opts entities.ImageDeleteOptions) (*entities.ImageDeleteReport, error) { +	report := entities.ImageDeleteReport{} -	for _, e := range results { -		if a, ok := e["Deleted"]; ok { -			report.Deleted = a +	for _, id := range nameOrId { +		results, err := images.Remove(ir.ClientCxt, id, &opts.Force) +		if err != nil { +			return nil, err  		} +		for _, e := range results { +			if a, ok := e["Deleted"]; ok { +				report.Deleted = append(report.Deleted, a) +			} -		if a, ok := e["Untagged"]; ok { -			report.Untagged = append(report.Untagged, a) +			if a, ok := e["Untagged"]; ok { +				report.Untagged = append(report.Untagged, a) +			}  		}  	} -	return &report, err +	return &report, nil  }  func (ir *ImageEngine) List(ctx context.Context, opts entities.ImageListOptions) ([]*entities.ImageSummary, error) {  | 
