diff options
53 files changed, 1677 insertions, 163 deletions
diff --git a/.cirrus.yml b/.cirrus.yml index 0770e0702..a0340dbbd 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -39,7 +39,7 @@ env: UBUNTU_NAME: "ubuntu-19" PRIOR_UBUNTU_NAME: "ubuntu-18" - _BUILT_IMAGE_SUFFIX: "libpod-5633729662025728" + _BUILT_IMAGE_SUFFIX: "libpod-6465271544152064" FEDORA_CACHE_IMAGE_NAME: "${FEDORA_NAME}-${_BUILT_IMAGE_SUFFIX}" PRIOR_FEDORA_CACHE_IMAGE_NAME: "${PRIOR_FEDORA_NAME}-${_BUILT_IMAGE_SUFFIX}" UBUNTU_CACHE_IMAGE_NAME: "${UBUNTU_NAME}-${_BUILT_IMAGE_SUFFIX}" diff --git a/cmd/podmanV2/Makefile b/cmd/podmanV2/Makefile index 147a78d9c..f2f7bd73c 100644 --- a/cmd/podmanV2/Makefile +++ b/cmd/podmanV2/Makefile @@ -1,2 +1,2 @@ all: - GO111MODULE=off go build -tags 'ABISupport' + GO111MODULE=off go build -tags 'ABISupport systemd' diff --git a/cmd/podmanV2/common/inspect.go b/cmd/podmanV2/common/inspect.go new file mode 100644 index 000000000..dfc6fe679 --- /dev/null +++ b/cmd/podmanV2/common/inspect.go @@ -0,0 +1,18 @@ +package common + +import ( + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +// AddInspectFlagSet takes a command and adds the inspect flags and returns an InspectOptions object +// Since this cannot live in `package main` it lives here until a better home is found +func AddInspectFlagSet(cmd *cobra.Command) *entities.InspectOptions { + opts := entities.InspectOptions{} + + flags := cmd.Flags() + flags.BoolVarP(&opts.Size, "size", "s", false, "Display total file size") + flags.StringVarP(&opts.Format, "format", "f", "", "Change the output format to a Go template") + + return &opts +} diff --git a/cmd/podmanV2/containers/export.go b/cmd/podmanV2/containers/export.go new file mode 100644 index 000000000..b93b60878 --- /dev/null +++ b/cmd/podmanV2/containers/export.go @@ -0,0 +1,57 @@ +package containers + +import ( + "context" + "os" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" +) + +var ( + exportDescription = "Exports container's filesystem contents as a tar archive" + + " and saves it on the local machine." + + exportCommand = &cobra.Command{ + Use: "export [flags] CONTAINER", + Short: "Export container's filesystem contents as a tar archive", + Long: exportDescription, + PersistentPreRunE: preRunE, + RunE: export, + Args: cobra.ExactArgs(1), + Example: `podman export ctrID > myCtr.tar + podman export --output="myCtr.tar" ctrID`, + } +) + +var ( + exportOpts entities.ContainerExportOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: exportCommand, + }) + exportCommand.SetHelpTemplate(registry.HelpTemplate()) + exportCommand.SetUsageTemplate(registry.UsageTemplate()) + flags := exportCommand.Flags() + flags.StringVarP(&exportOpts.Output, "output", "o", "", "Write to a specified file (default: stdout, which must be redirected)") +} + +func export(cmd *cobra.Command, args []string) error { + if len(exportOpts.Output) == 0 { + file := os.Stdout + if terminal.IsTerminal(int(file.Fd())) { + return errors.Errorf("refusing to export to terminal. Use -o flag or redirect") + } + exportOpts.Output = "/dev/stdout" + } else if err := parse.ValidateFileName(exportOpts.Output); err != nil { + return err + } + return registry.ContainerEngine().ContainerExport(context.Background(), args[0], exportOpts) +} diff --git a/cmd/podmanV2/containers/inspect.go b/cmd/podmanV2/containers/inspect.go index 648289f0b..3147426cb 100644 --- a/cmd/podmanV2/containers/inspect.go +++ b/cmd/podmanV2/containers/inspect.go @@ -7,9 +7,11 @@ import ( "strings" "text/template" + "github.com/containers/libpod/cmd/podmanV2/common" "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" - jsoniter "github.com/json-iterator/go" + json "github.com/json-iterator/go" "github.com/spf13/cobra" ) @@ -24,10 +26,7 @@ var ( Example: `podman container inspect myCtr podman container inspect -l --format '{{.Id}} {{.Config.Labels}}'`, } -) - -var ( - inspectOptions entities.ContainerInspectOptions + inspectOpts *entities.InspectOptions ) func init() { @@ -36,29 +35,29 @@ func init() { Command: inspectCmd, Parent: containerCmd, }) + inspectOpts = common.AddInspectFlagSet(inspectCmd) flags := inspectCmd.Flags() - flags.StringVarP(&inspectOptions.Format, "format", "f", "", "Change the output format to a Go template") - flags.BoolVarP(&inspectOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") - flags.BoolVarP(&inspectOptions.Size, "size", "s", false, "Display total file size") - if registry.IsRemote() { - _ = flags.MarkHidden("latest") + + if !registry.IsRemote() { + flags.BoolVarP(&inspectOpts.Latest, "latest", "l", false, "Act on the latest container podman is aware of") } + } func inspect(cmd *cobra.Command, args []string) error { - responses, err := registry.ContainerEngine().ContainerInspect(context.Background(), args, inspectOptions) + responses, err := registry.ContainerEngine().ContainerInspect(context.Background(), args, *inspectOpts) if err != nil { return err } - if inspectOptions.Format == "" { - b, err := jsoniter.MarshalIndent(responses, "", " ") + if inspectOpts.Format == "" { + b, err := json.MarshalIndent(responses, "", " ") if err != nil { return err } fmt.Println(string(b)) return nil } - format := inspectOptions.Format + format := inspectOpts.Format if !strings.HasSuffix(format, "\n") { format += "\n" } @@ -73,3 +72,8 @@ func inspect(cmd *cobra.Command, args []string) error { } return nil } + +func Inspect(cmd *cobra.Command, args []string, options *entities.InspectOptions) error { + inspectOpts = options + return inspect(cmd, args) +} diff --git a/cmd/podmanV2/healthcheck/healthcheck.go b/cmd/podmanV2/healthcheck/healthcheck.go new file mode 100644 index 000000000..2af398ff0 --- /dev/null +++ b/cmd/podmanV2/healthcheck/healthcheck.go @@ -0,0 +1,33 @@ +package healthcheck + +import ( + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // Command: healthcheck + healthCmd = &cobra.Command{ + Use: "healthcheck", + Short: "Manage Healthcheck", + Long: "Manage Healthcheck", + TraverseChildren: true, + PersistentPreRunE: preRunE, + RunE: registry.SubCommandExists, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: healthCmd, + }) + healthCmd.SetHelpTemplate(registry.HelpTemplate()) + healthCmd.SetUsageTemplate(registry.UsageTemplate()) +} + +func preRunE(cmd *cobra.Command, args []string) error { + _, err := registry.NewContainerEngine(cmd, args) + return err +} diff --git a/cmd/podmanV2/healthcheck/run.go b/cmd/podmanV2/healthcheck/run.go new file mode 100644 index 000000000..bb2962eaf --- /dev/null +++ b/cmd/podmanV2/healthcheck/run.go @@ -0,0 +1,42 @@ +package healthcheck + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + healthcheckRunDescription = "run the health check of a container" + healthcheckrunCommand = &cobra.Command{ + Use: "run [flags] CONTAINER", + Short: "run the health check of a container", + Long: healthcheckRunDescription, + Example: `podman healthcheck run mywebapp`, + RunE: run, + Args: cobra.ExactArgs(1), + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: healthcheckrunCommand, + Parent: healthCmd, + }) +} + +func run(cmd *cobra.Command, args []string) error { + response, err := registry.ContainerEngine().HealthCheckRun(context.Background(), args[0], entities.HealthCheckOptions{}) + if err != nil { + return err + } + if response.Status == "unhealthy" { + registry.SetExitCode(1) + } + fmt.Println(response.Status) + return err +} diff --git a/cmd/podmanV2/images/inspect.go b/cmd/podmanV2/images/inspect.go index f8fd44571..d7f6b0ee1 100644 --- a/cmd/podmanV2/images/inspect.go +++ b/cmd/podmanV2/images/inspect.go @@ -1,71 +1,44 @@ package images import ( + "context" + "encoding/json" + "fmt" + "os" "strings" + "text/tabwriter" + "text/template" "github.com/containers/buildah/pkg/formats" + "github.com/containers/libpod/cmd/podmanV2/common" "github.com/containers/libpod/cmd/podmanV2/registry" "github.com/containers/libpod/pkg/domain/entities" - "github.com/containers/libpod/pkg/util" "github.com/pkg/errors" "github.com/spf13/cobra" ) var ( - inspectOpts = entities.ImageInspectOptions{} - // Command: podman image _inspect_ inspectCmd = &cobra.Command{ Use: "inspect [flags] IMAGE", Short: "Display the configuration of an image", Long: `Displays the low-level information on an image identified by name or ID.`, - PreRunE: populateEngines, - RunE: imageInspect, + RunE: inspect, Example: `podman image inspect alpine`, } - - containerEngine entities.ContainerEngine + inspectOpts *entities.InspectOptions ) -// Inspect is unique in that it needs both an ImageEngine and a ContainerEngine -func populateEngines(cmd *cobra.Command, args []string) (err error) { - // Populate registry.ImageEngine - err = preRunE(cmd, args) - if err != nil { - return - } - - // Populate registry.ContainerEngine - containerEngine, err = registry.NewContainerEngine(cmd, args) - return -} - func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: inspectCmd, Parent: imageCmd, }) - - flags := inspectCmd.Flags() - flags.BoolVarP(&inspectOpts.Latest, "latest", "l", false, "Act on the latest container podman is aware of") - flags.BoolVarP(&inspectOpts.Size, "size", "s", false, "Display total file size") - flags.StringVarP(&inspectOpts.Format, "format", "f", "", "Change the output format to a Go template") - - if registry.EngineOptions.EngineMode == entities.ABIMode { - // TODO: This is the same as V1. We could skip creating the flag altogether in V2... - _ = flags.MarkHidden("latest") - } + inspectOpts = common.AddInspectFlagSet(inspectCmd) } -const ( - inspectTypeContainer = "container" - inspectTypeImage = "image" - inspectAll = "all" -) - -func imageInspect(cmd *cobra.Command, args []string) error { - inspectType := inspectTypeImage +func inspect(cmd *cobra.Command, args []string) error { latestContainer := inspectOpts.Latest if len(args) == 0 && !latestContainer { @@ -76,49 +49,61 @@ func imageInspect(cmd *cobra.Command, args []string) error { return errors.Errorf("you cannot provide additional arguments with --latest") } - if !util.StringInSlice(inspectType, []string{inspectTypeContainer, inspectTypeImage, inspectAll}) { - return errors.Errorf("the only recognized types are %q, %q, and %q", inspectTypeContainer, inspectTypeImage, inspectAll) + results, err := registry.ImageEngine().Inspect(context.Background(), args, *inspectOpts) + if err != nil { + return err } - outputFormat := inspectOpts.Format - if strings.Contains(outputFormat, "{{.Id}}") { - outputFormat = strings.Replace(outputFormat, "{{.Id}}", formats.IDString, -1) - } - // These fields were renamed, so we need to provide backward compat for - // the old names. - if strings.Contains(outputFormat, ".Src") { - outputFormat = strings.Replace(outputFormat, ".Src", ".Source", -1) + if len(results.Images) > 0 { + if inspectOpts.Format == "" { + buf, err := json.MarshalIndent(results.Images, "", " ") + if err != nil { + return err + } + fmt.Println(string(buf)) + + for id, e := range results.Errors { + fmt.Fprintf(os.Stderr, "%s: %s\n", id, e.Error()) + } + return nil + } + + row := inspectFormat(inspectOpts.Format) + format := "{{range . }}" + row + "{{end}}" + tmpl, err := template.New("inspect").Parse(format) + if err != nil { + return err + } + + w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) + defer func() { _ = w.Flush() }() + err = tmpl.Execute(w, results) + if err != nil { + return err + } } - if strings.Contains(outputFormat, ".Dst") { - outputFormat = strings.Replace(outputFormat, ".Dst", ".Destination", -1) - } - if strings.Contains(outputFormat, ".ImageID") { - outputFormat = strings.Replace(outputFormat, ".ImageID", ".Image", -1) + + for id, e := range results.Errors { + fmt.Fprintf(os.Stderr, "%s: %s\n", id, e.Error()) } - _ = outputFormat - // if latestContainer { - // lc, err := ctnrRuntime.GetLatestContainer() - // if err != nil { - // return err - // } - // args = append(args, lc.ID()) - // inspectType = inspectTypeContainer - // } - - // inspectedObjects, iterateErr := iterateInput(getContext(), c.Size, args, runtime, inspectType) - // if iterateErr != nil { - // return iterateErr - // } - // - // var out formats.Writer - // if outputFormat != "" && outputFormat != formats.JSONString { - // // template - // out = formats.StdoutTemplateArray{Output: inspectedObjects, Template: outputFormat} - // } else { - // // default is json output - // out = formats.JSONStructArray{Output: inspectedObjects} - // } - // - // return out.Out() return nil } + +func inspectFormat(row string) string { + r := strings.NewReplacer("{{.Id}}", formats.IDString, + ".Src", ".Source", + ".Dst", ".Destination", + ".ImageID", ".Image", + ) + row = r.Replace(row) + + if !strings.HasSuffix(row, "\n") { + row += "\n" + } + return row +} + +func Inspect(cmd *cobra.Command, args []string, options *entities.InspectOptions) error { + inspectOpts = options + return inspect(cmd, args) +} diff --git a/cmd/podmanV2/images/list.go b/cmd/podmanV2/images/list.go index 9a5b47299..2d6cb3596 100644 --- a/cmd/podmanV2/images/list.go +++ b/cmd/podmanV2/images/list.go @@ -152,7 +152,7 @@ func writeTemplate(imageS []*entities.ImageSummary, err error) error { hdr, row := imageListFormat(listFlag) format := hdr + "{{range . }}" + row + "{{end}}" - tmpl := template.Must(template.New("report").Funcs(report.PodmanTemplateFuncs()).Parse(format)) + tmpl := template.Must(template.New("list").Funcs(report.PodmanTemplateFuncs()).Parse(format)) w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) defer w.Flush() return tmpl.Execute(w, imgs) diff --git a/cmd/podmanV2/images/load.go b/cmd/podmanV2/images/load.go new file mode 100644 index 000000000..f60dc4908 --- /dev/null +++ b/cmd/podmanV2/images/load.go @@ -0,0 +1,61 @@ +package images + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + loadDescription = "Loads an image from a locally stored archive (tar file) into container storage." + loadCommand = &cobra.Command{ + Use: "load [flags] [NAME[:TAG]]", + Short: "Load an image from container archive", + Long: loadDescription, + RunE: load, + Args: cobra.MaximumNArgs(1), + PersistentPreRunE: preRunE, + } +) + +var ( + loadOpts entities.ImageLoadOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: loadCommand, + }) + + loadCommand.SetHelpTemplate(registry.HelpTemplate()) + loadCommand.SetUsageTemplate(registry.UsageTemplate()) + flags := loadCommand.Flags() + flags.StringVarP(&loadOpts.Input, "input", "i", "", "Read from specified archive file (default: stdin)") + flags.BoolVarP(&loadOpts.Quiet, "quiet", "q", false, "Suppress the output") + flags.StringVar(&loadOpts.SignaturePolicy, "signature-policy", "", "Pathname of signature policy file") + if registry.IsRemote() { + _ = flags.MarkHidden("signature-policy") + } + +} + +func load(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + repo, err := image.NormalizedTag(args[0]) + if err != nil { + return err + } + loadOpts.Name = repo.Name() + } + response, err := registry.ImageEngine().Load(context.Background(), loadOpts) + if err != nil { + return err + } + fmt.Println("Loaded image: " + response.Name) + return nil +} diff --git a/cmd/podmanV2/images/pull.go b/cmd/podmanV2/images/pull.go new file mode 100644 index 000000000..c7e325409 --- /dev/null +++ b/cmd/podmanV2/images/pull.go @@ -0,0 +1,140 @@ +package images + +import ( + "fmt" + + buildahcli "github.com/containers/buildah/pkg/cli" + "github.com/containers/image/v5/types" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/opentracing/opentracing-go" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// pullOptionsWrapper wraps entities.ImagePullOptions and prevents leaking +// CLI-only fields into the API types. +type pullOptionsWrapper struct { + entities.ImagePullOptions + TLSVerifyCLI bool // CLI only +} + +var ( + pullOptions = pullOptionsWrapper{} + pullDescription = `Pulls an image from a registry and stores it locally. + + An image can be pulled by tag or digest. If a tag is not specified, the image with the 'latest' tag is pulled.` + + // Command: podman pull + pullCmd = &cobra.Command{ + Use: "pull [flags] IMAGE", + Short: "Pull an image from a registry", + Long: pullDescription, + PreRunE: preRunE, + RunE: imagePull, + Example: `podman pull imageName + podman pull fedora:latest`, + } + + // Command: podman image pull + // It's basically a clone of `pullCmd` with the exception of being a + // child of the images command. + imagesPullCmd = &cobra.Command{ + Use: pullCmd.Use, + Short: pullCmd.Short, + Long: pullCmd.Long, + PreRunE: pullCmd.PreRunE, + RunE: pullCmd.RunE, + Example: `podman image pull imageName + podman image pull fedora:latest`, + } +) + +func init() { + // pull + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: pullCmd, + }) + + pullCmd.SetHelpTemplate(registry.HelpTemplate()) + pullCmd.SetUsageTemplate(registry.UsageTemplate()) + + flags := pullCmd.Flags() + pullFlags(flags) + + // images pull + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imagesPullCmd, + Parent: imageCmd, + }) + + imagesPullCmd.SetHelpTemplate(registry.HelpTemplate()) + imagesPullCmd.SetUsageTemplate(registry.UsageTemplate()) + imagesPullFlags := imagesPullCmd.Flags() + pullFlags(imagesPullFlags) +} + +// pullFlags set the flags for the pull command. +func pullFlags(flags *pflag.FlagSet) { + flags.BoolVar(&pullOptions.AllTags, "all-tags", false, "All tagged images in the repository will be pulled") + flags.StringVar(&pullOptions.Authfile, "authfile", buildahcli.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") + flags.StringVar(&pullOptions.CertDir, "cert-dir", "", "`Pathname` of a directory containing TLS certificates and keys") + flags.StringVar(&pullOptions.Credentials, "creds", "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry") + flags.StringVar(&pullOptions.OverrideArch, "override-arch", "", "Use `ARCH` instead of the architecture of the machine for choosing images") + flags.StringVar(&pullOptions.OverrideOS, "override-os", "", "Use `OS` instead of the running OS for choosing images") + flags.BoolVarP(&pullOptions.Quiet, "quiet", "q", false, "Suppress output information when pulling images") + flags.StringVar(&pullOptions.SignaturePolicy, "signature-policy", "", "`Pathname` of signature policy file (not usually used)") + flags.BoolVar(&pullOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries") + + if registry.IsRemote() { + _ = flags.MarkHidden("authfile") + _ = flags.MarkHidden("cert-dir") + _ = flags.MarkHidden("signature-policy") + _ = flags.MarkHidden("tls-verify") + } +} + +// imagePull is implement the command for pulling images. +func imagePull(cmd *cobra.Command, args []string) error { + // Sanity check input. + if len(args) == 0 { + return errors.Errorf("an image name must be specified") + } + if len(args) > 1 { + return errors.Errorf("too many arguments. Requires exactly 1") + } + + // Start tracing if requested. + if cmd.Flags().Changed("trace") { + span, _ := opentracing.StartSpanFromContext(registry.GetContext(), "pullCmd") + defer span.Finish() + } + + pullOptsAPI := pullOptions.ImagePullOptions + // TLS verification in c/image is controlled via a `types.OptionalBool` + // which allows for distinguishing among set-true, set-false, unspecified + // which is important to implement a sane way of dealing with defaults of + // boolean CLI flags. + if cmd.Flags().Changed("tls-verify") { + pullOptsAPI.TLSVerify = types.NewOptionalBool(pullOptions.TLSVerifyCLI) + } + + // Let's do all the remaining Yoga in the API to prevent us from + // scattering logic across (too) many parts of the code. + pullReport, err := registry.ImageEngine().Pull(registry.GetContext(), args[0], pullOptsAPI) + if err != nil { + return err + } + + if len(pullReport.Images) > 1 { + fmt.Println("Pulled Images:") + } + for _, img := range pullReport.Images { + fmt.Println(img) + } + + return nil +} diff --git a/cmd/podmanV2/images/tag.go b/cmd/podmanV2/images/tag.go new file mode 100644 index 000000000..f66fe7857 --- /dev/null +++ b/cmd/podmanV2/images/tag.go @@ -0,0 +1,34 @@ +package images + +import ( + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + tagDescription = "Adds one or more additional names to locally-stored image." + tagCommand = &cobra.Command{ + Use: "tag [flags] IMAGE TARGET_NAME [TARGET_NAME...]", + Short: "Add an additional name to a local image", + Long: tagDescription, + RunE: tag, + Args: cobra.MinimumNArgs(2), + Example: `podman tag 0e3bbc2 fedora:latest + podman tag imageID:latest myNewImage:newTag + podman tag httpd myregistryhost:5000/fedora/httpd:v2`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: tagCommand, + }) + tagCommand.SetHelpTemplate(registry.HelpTemplate()) + tagCommand.SetUsageTemplate(registry.UsageTemplate()) +} + +func tag(cmd *cobra.Command, args []string) error { + return registry.ImageEngine().Tag(registry.GetContext(), args[0], args[1:], entities.ImageTagOptions{}) +} diff --git a/cmd/podmanV2/images/untag.go b/cmd/podmanV2/images/untag.go new file mode 100644 index 000000000..c84827bb3 --- /dev/null +++ b/cmd/podmanV2/images/untag.go @@ -0,0 +1,33 @@ +package images + +import ( + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + untagCommand = &cobra.Command{ + Use: "untag [flags] IMAGE [NAME...]", + Short: "Remove a name from a local image", + Long: "Removes one or more names from a locally-stored image.", + RunE: untag, + Args: cobra.MinimumNArgs(1), + Example: `podman untag 0e3bbc2 + podman untag imageID:latest otherImageName:latest + podman untag httpd myregistryhost:5000/fedora/httpd:v2`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: untagCommand, + }) + untagCommand.SetHelpTemplate(registry.HelpTemplate()) + untagCommand.SetUsageTemplate(registry.UsageTemplate()) +} + +func untag(cmd *cobra.Command, args []string) error { + return registry.ImageEngine().Untag(registry.GetContext(), args[0], args[1:], entities.ImageUntagOptions{}) +} diff --git a/cmd/podmanV2/inspect.go b/cmd/podmanV2/inspect.go new file mode 100644 index 000000000..4975cf632 --- /dev/null +++ b/cmd/podmanV2/inspect.go @@ -0,0 +1,62 @@ +package main + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/common" + "github.com/containers/libpod/cmd/podmanV2/containers" + "github.com/containers/libpod/cmd/podmanV2/images" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +// Inspect is one of the out layer commands in that it operates on images/containers/... + +var ( + inspectOpts *entities.InspectOptions + + // Command: podman _inspect_ Object_ID + inspectCmd = &cobra.Command{ + Use: "inspect [flags] {CONTAINER_ID | IMAGE_ID}", + Args: cobra.ExactArgs(1), + Short: "Display the configuration of object denoted by ID", + Long: "Displays the low-level information on an object identified by name or ID", + TraverseChildren: true, + RunE: inspect, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: inspectCmd, + }) + inspectOpts = common.AddInspectFlagSet(inspectCmd) +} + +func inspect(cmd *cobra.Command, args []string) error { + ie, err := registry.NewImageEngine(cmd, args) + if err != nil { + return err + } + + if found, err := ie.Exists(context.Background(), args[0]); err != nil { + return err + } else if found.Value { + return images.Inspect(cmd, args, inspectOpts) + } + + ce, err := registry.NewContainerEngine(cmd, args) + if err != nil { + return err + } + + if found, err := ce.ContainerExists(context.Background(), args[0]); err != nil { + return err + } else if found.Value { + return containers.Inspect(cmd, args, inspectOpts) + } + return fmt.Errorf("%s not found on system", args[0]) +} diff --git a/cmd/podmanV2/main.go b/cmd/podmanV2/main.go index bd9fbb25e..6781a7f06 100644 --- a/cmd/podmanV2/main.go +++ b/cmd/podmanV2/main.go @@ -7,6 +7,7 @@ import ( "strings" _ "github.com/containers/libpod/cmd/podmanV2/containers" + _ "github.com/containers/libpod/cmd/podmanV2/healthcheck" _ "github.com/containers/libpod/cmd/podmanV2/images" _ "github.com/containers/libpod/cmd/podmanV2/networks" _ "github.com/containers/libpod/cmd/podmanV2/pods" @@ -14,6 +15,7 @@ import ( _ "github.com/containers/libpod/cmd/podmanV2/volumes" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/storage/pkg/reexec" "github.com/sirupsen/logrus" ) @@ -44,6 +46,11 @@ func init() { } func main() { + if reexec.Init() { + // We were invoked with a different argv[0] indicating that we + // had a specific job to do as a subprocess, and it's done. + return + } for _, c := range registry.Commands { if Contains(registry.EngineOptions.EngineMode, c.Mode) { parent := rootCmd diff --git a/cmd/podmanV2/pods/pod.go b/cmd/podmanV2/pods/pod.go index 81c0d33e1..3766893bb 100644 --- a/cmd/podmanV2/pods/pod.go +++ b/cmd/podmanV2/pods/pod.go @@ -1,6 +1,9 @@ package pods import ( + "strings" + "text/template" + "github.com/containers/libpod/cmd/podmanV2/registry" "github.com/containers/libpod/pkg/domain/entities" "github.com/spf13/cobra" @@ -18,6 +21,33 @@ var ( } ) +var podFuncMap = template.FuncMap{ + "numCons": func(cons []*entities.ListPodContainer) int { + return len(cons) + }, + "podcids": func(cons []*entities.ListPodContainer) string { + var ctrids []string + for _, c := range cons { + ctrids = append(ctrids, c.Id[:12]) + } + return strings.Join(ctrids, ",") + }, + "podconnames": func(cons []*entities.ListPodContainer) string { + var ctrNames []string + for _, c := range cons { + ctrNames = append(ctrNames, c.Names[:12]) + } + return strings.Join(ctrNames, ",") + }, + "podconstatuses": func(cons []*entities.ListPodContainer) string { + var statuses []string + for _, c := range cons { + statuses = append(statuses, c.Status) + } + return strings.Join(statuses, ",") + }, +} + func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, diff --git a/cmd/podmanV2/pods/ps.go b/cmd/podmanV2/pods/ps.go index d4c625b2e..9546dff9e 100644 --- a/cmd/podmanV2/pods/ps.go +++ b/cmd/podmanV2/pods/ps.go @@ -1,8 +1,19 @@ package pods import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "strings" + "text/tabwriter" + "text/template" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/report" "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -19,14 +30,137 @@ var ( } ) +var ( + defaultHeaders string = "POD ID\tNAME\tSTATUS\tCREATED" + inputFilters string + noTrunc bool + psInput entities.PodPSOptions +) + func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: psCmd, Parent: podCmd, }) + flags := psCmd.Flags() + flags.BoolVar(&psInput.CtrNames, "ctr-names", false, "Display the container names") + flags.BoolVar(&psInput.CtrIds, "ctr-ids", false, "Display the container UUIDs. If no-trunc is not set they will be truncated") + flags.BoolVar(&psInput.CtrStatus, "ctr-status", false, "Display the container status") + // TODO should we make this a [] ? + flags.StringVarP(&inputFilters, "filter", "f", "", "Filter output based on conditions given") + flags.StringVar(&psInput.Format, "format", "", "Pretty-print pods to JSON or using a Go template") + flags.BoolVarP(&psInput.Latest, "latest", "l", false, "Act on the latest pod podman is aware of") + flags.BoolVar(&psInput.Namespace, "namespace", false, "Display namespace information of the pod") + flags.BoolVar(&psInput.Namespace, "ns", false, "Display namespace information of the pod") + flags.BoolVar(&noTrunc, "no-trunc", false, "Do not truncate pod and container IDs") + flags.BoolVarP(&psInput.Quiet, "quiet", "q", false, "Print the numeric IDs of the pods only") + flags.StringVar(&psInput.Sort, "sort", "created", "Sort output by created, id, name, or number") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } } func pods(cmd *cobra.Command, args []string) error { + var ( + w io.Writer = os.Stdout + row string + ) + if cmd.Flag("filter").Changed { + for _, f := range strings.Split(inputFilters, ",") { + split := strings.Split(f, "=") + if len(split) < 2 { + return errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f) + } + psInput.Filters[split[0]] = append(psInput.Filters[split[0]], split[1]) + } + } + responses, err := registry.ContainerEngine().PodPs(context.Background(), psInput) + if err != nil { + return err + } + + if psInput.Format == "json" { + b, err := json.MarshalIndent(responses, "", " ") + if err != nil { + return err + } + fmt.Println(string(b)) + return nil + } + headers, row := createPodPsOut(cmd) + if psInput.Quiet { + if noTrunc { + row = "{{.Id}}\n" + } else { + row = "{{slice .Id 0 12}}\n" + } + } + if cmd.Flag("format").Changed { + row = psInput.Format + if !strings.HasPrefix(row, "\n") { + row += "\n" + } + } + format := "{{range . }}" + row + "{{end}}" + if !psInput.Quiet && !cmd.Flag("format").Changed { + format = headers + format + } + funcs := report.AppendFuncMap(podFuncMap) + tmpl, err := template.New("listPods").Funcs(funcs).Parse(format) + if err != nil { + return err + } + if !psInput.Quiet { + w = tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) + } + if err := tmpl.Execute(w, responses); err != nil { + return err + } + if flusher, ok := w.(interface{ Flush() error }); ok { + return flusher.Flush() + } return nil } + +func createPodPsOut(cmd *cobra.Command) (string, string) { + var row string + headers := defaultHeaders + if noTrunc { + row += "{{.Id}}" + } else { + row += "{{slice .Id 0 12}}" + } + + row += "\t{{.Name}}\t{{.Status}}\t{{humanDurationFromTime .Created}}" + + //rowFormat string = "{{slice .Id 0 12}}\t{{.Name}}\t{{.Status}}\t{{humanDurationFromTime .Created}}" + if psInput.CtrIds { + headers += "\tIDS" + row += "\t{{podcids .Containers}}" + } + if psInput.CtrNames { + headers += "\tNAMES" + row += "\t{{podconnames .Containers}}" + } + if psInput.CtrStatus { + headers += "\tSTATUS" + row += "\t{{podconstatuses .Containers}}" + } + if psInput.Namespace { + headers += "\tCGROUP\tNAMESPACES" + row += "\t{{.Cgroup}}\t{{.Namespace}}" + } + if !psInput.CtrStatus && !psInput.CtrNames && !psInput.CtrIds { + headers += "\t# OF CONTAINERS" + row += "\t{{numCons .Containers}}" + + } + headers += "\tINFRA ID\n" + if noTrunc { + row += "\t{{.InfraId}}\n" + } else { + row += "\t{{slice .InfraId 0 12}}\n" + } + return headers, row +} diff --git a/cmd/podmanV2/registry/registry.go b/cmd/podmanV2/registry/registry.go index 5cdb8a840..401f82718 100644 --- a/cmd/podmanV2/registry/registry.go +++ b/cmd/podmanV2/registry/registry.go @@ -3,7 +3,6 @@ package registry import ( "context" - "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/infra" "github.com/pkg/errors" @@ -18,24 +17,24 @@ type CliCommand struct { Parent *cobra.Command } -var ( - Commands []CliCommand +const ExecErrorCodeGeneric = 125 - imageEngine entities.ImageEngine - containerEngine entities.ContainerEngine +var ( cliCtx context.Context + containerEngine entities.ContainerEngine + exitCode = ExecErrorCodeGeneric + imageEngine entities.ImageEngine + Commands []CliCommand EngineOptions entities.EngineOptions - - ExitCode = define.ExecErrorCodeGeneric ) func SetExitCode(code int) { - ExitCode = code + exitCode = code } func GetExitCode() int { - return ExitCode + return exitCode } // HelpTemplate returns the help template for podman commands diff --git a/cmd/podmanV2/report/templates.go b/cmd/podmanV2/report/templates.go index f3bc06405..e46048e97 100644 --- a/cmd/podmanV2/report/templates.go +++ b/cmd/podmanV2/report/templates.go @@ -19,6 +19,9 @@ var defaultFuncMap = template.FuncMap{ "humanDuration": func(t int64) string { return units.HumanDuration(time.Since(time.Unix(t, 0))) + " ago" }, + "humanDurationFromTime": func(t time.Time) string { + return units.HumanDuration(time.Since(t)) + " ago" + }, "humanSize": func(sz int64) string { s := units.HumanSizeWithPrecision(float64(sz), 3) i := strings.LastIndexFunc(s, unicode.IsNumber) diff --git a/cmd/podmanV2/root.go b/cmd/podmanV2/root.go index cb4cb4e00..6fc12f57e 100644 --- a/cmd/podmanV2/root.go +++ b/cmd/podmanV2/root.go @@ -7,7 +7,6 @@ import ( "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" @@ -88,8 +87,8 @@ func Execute() { o := registry.NewOptions(rootCmd.Context(), ®istry.EngineOptions) if err := rootCmd.ExecuteContext(o); err != nil { fmt.Fprintln(os.Stderr, "Error:", err.Error()) - } else if registry.GetExitCode() == define.ExecErrorCodeGeneric { - // The exitCode modified from define.ExecErrorCodeGeneric, + } else if registry.GetExitCode() == registry.ExecErrorCodeGeneric { + // The exitCode modified from registry.ExecErrorCodeGeneric, // indicates an application // running inside of a container failed, as opposed to the // podman command failed. Must exit with that exit code diff --git a/cmd/podmanV2/system/system.go b/cmd/podmanV2/system/system.go index 30ed328e8..4e805c7bd 100644 --- a/cmd/podmanV2/system/system.go +++ b/cmd/podmanV2/system/system.go @@ -1,4 +1,4 @@ -package images +package system import ( "github.com/containers/libpod/cmd/podmanV2/registry" diff --git a/contrib/build_rpm.sh b/contrib/build_rpm.sh index de6941199..a9db029df 100755 --- a/contrib/build_rpm.sh +++ b/contrib/build_rpm.sh @@ -7,11 +7,18 @@ echo "Package manager binary: $pkg_manager" if [[ $pkg_manager == *yum ]]; then - echo "[virt7-container-common-candidate] -name=virt7-container-common-candidate -baseurl=https://cbs.centos.org/repos/virt7-container-common-candidate/x86_64/os/ + echo "[virt7-container] +name=virt7-container +baseurl=https://buildlogs.centos.org/centos/7/virt/x86_64/container/ enabled=1 gpgcheck=0" > /etc/yum.repos.d/container_virt.repo + echo "[paas_openshift_origin311] +name=paas_openshift_origin311 +# Use this if you need the newest *-testing packages +# baseurl=https://buildlogs.centos.org/centos/7/paas/x86_64/openshift-origin311/ +baseurl=http://mirror.centos.org/centos/7/paas/x86_64/openshift-origin311/ +enabled=1 +gpgcheck=0" > /etc/yum.repos.d/paas_openshift_origin311.repo fi declare -a PKGS=(\ diff --git a/contrib/cirrus/README.md b/contrib/cirrus/README.md index 3789965d6..709985b5b 100644 --- a/contrib/cirrus/README.md +++ b/contrib/cirrus/README.md @@ -80,7 +80,7 @@ having `SPECIALMODE` set to 'cgroupv2` Modifying the contents of cache-images is tested by making changes to one or more of the ``./contrib/cirrus/packer/*_setup.sh`` files. Then -in the PR description, add the magic string: ``***CIRRUS: TEST IMAGES***`` +in the PR description, add the magic string: ``[CI:IMG]`` ***N/B: Steps below are performed by automation*** @@ -153,7 +153,7 @@ env: ***NOTES:*** * If re-using the same PR with new images in `.cirrus.yml`, take care to also *update the PR description* to remove - the magic ``***CIRRUS: TEST IMAGES***`` string. Keeping it and + the magic ``[CI:IMG]`` string. Keeping it and `--force` pushing would needlessly cause Cirrus-CI to build and test images again. * In the future, if you need to review the log from the build that produced diff --git a/contrib/spec/podman.spec.in b/contrib/spec/podman.spec.in index 17adf9672..9641a52e6 100644 --- a/contrib/spec/podman.spec.in +++ b/contrib/spec/podman.spec.in @@ -392,7 +392,7 @@ popd ln -s vendor src export GO111MODULE=off export GOPATH=$(pwd)/_build:$(pwd):$(pwd):%{gopath} -export BUILDTAGS="varlink selinux seccomp $(%{hackdir}/hack/btrfs_installed_tag.sh) $(%{hackdir}/hack/btrfs_tag.sh) $(%{hackdir}/hack/libdm_tag.sh) exclude_graphdriver_devicemapper" +export BUILDTAGS="varlink selinux seccomp systemd $(%{hackdir}/hack/btrfs_installed_tag.sh) $(%{hackdir}/hack/btrfs_tag.sh) $(%{hackdir}/hack/libdm_tag.sh) exclude_graphdriver_devicemapper" GOPATH=$GOPATH go generate ./pkg/varlink/... @@ -410,7 +410,7 @@ mkdir -p src/%{provider}.%{provider_tld}/{containers,opencontainers} ln -s $(dirs +1 -l) src/%{import_path_conmon} popd -export BUILDTAGS="selinux seccomp $(%{hackdir}/hack/btrfs_installed_tag.sh) $(%{hackdir}/hack/btrfs_tag.sh)" +export BUILDTAGS="selinux seccomp systemd $(%{hackdir}/hack/btrfs_installed_tag.sh) $(%{hackdir}/hack/btrfs_tag.sh)" BUILDTAGS=$BUILDTAGS make popd @@ -301,6 +301,7 @@ github.com/mtrmac/gpgme v0.1.2/go.mod h1:GYYHnGSuS7HK3zVS2n3y73y0okK/BeKzwnn5jgi github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -596,6 +597,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= diff --git a/hack/podmanv2-retry b/hack/podmanv2-retry new file mode 100755 index 000000000..ea77486ff --- /dev/null +++ b/hack/podmanv2-retry @@ -0,0 +1,37 @@ +#!/bin/bash +# +# podman-try - try running a command via PODMAN1; use PODMAN2 as fallback +# +# Intended for use with a podmanv2 client. If a command isn't yet +# implemented, fall back to regular podman: +# +# Set PODMAN_V2 to the path to a podman v2 client +# Set PODMAN_FALLBACK to the path to regular podman +# +# THIS IS IMPERFECT. In particular, it will not work if stdin is redirected +# (e.g. 'podman ... < file' or 'something | podman'); nor for anything +# that generates continuous output ('podman logs -f'); and probably more +# situations. +# + +die() { + echo "$(basename $0): $*" >&2 + exit 1 +} + +test -n "$PODMAN_V2" || die "Please set \$PODMAN_V2 (path to podman v2)" +test -n "$PODMAN_FALLBACK" || die "Please set \$PODMAN_FALLBACK (path to podman)" + + +result=$(${PODMAN_V2} "$@" 2>&1) +rc=$? + +if [ $rc == 125 ]; then + if [[ "$result" =~ unrecognized\ command|unknown\ flag|unknown\ shorthand ]]; then + result=$(${PODMAN_FALLBACK} "$@") + rc=$? + fi +fi + +echo -n "$result" +exit $rc diff --git a/libpod/image/image.go b/libpod/image/image.go index 5f914ed79..80cc6f15a 100644 --- a/libpod/image/image.go +++ b/libpod/image/image.go @@ -512,8 +512,8 @@ func getImageDigest(ctx context.Context, src types.ImageReference, sc *types.Sys return "@" + imageDigest.Hex(), nil } -// normalizedTag returns the canonical version of tag for use in Image.Names() -func normalizedTag(tag string) (reference.Named, error) { +// NormalizedTag returns the canonical version of tag for use in Image.Names() +func NormalizedTag(tag string) (reference.Named, error) { decomposedTag, err := decompose(tag) if err != nil { return nil, err @@ -541,7 +541,7 @@ func (i *Image) TagImage(tag string) error { if err := i.reloadImage(); err != nil { return err } - ref, err := normalizedTag(tag) + ref, err := NormalizedTag(tag) if err != nil { return err } diff --git a/libpod/image/image_test.go b/libpod/image/image_test.go index 19f7eee1e..3cd368cdc 100644 --- a/libpod/image/image_test.go +++ b/libpod/image/image_test.go @@ -292,7 +292,7 @@ func TestNormalizedTag(t *testing.T) { {"ns/busybox:latest", "localhost/ns/busybox:latest"}, // Unqualified with a dot-less namespace {"docker.io/busybox:latest", "docker.io/library/busybox:latest"}, // docker.io without /library/ } { - res, err := normalizedTag(c.input) + res, err := NormalizedTag(c.input) if c.expected == "" { assert.Error(t, err, c.input) } else { diff --git a/libpod/podfilters/pods.go b/libpod/podfilters/pods.go new file mode 100644 index 000000000..54fa85edc --- /dev/null +++ b/libpod/podfilters/pods.go @@ -0,0 +1,115 @@ +package podfilters + +import ( + "strconv" + "strings" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" +) + +// GeneratePodFilterFunc takes a filter and filtervalue (key, value) +// and generates a libpod function that can be used to filter +// pods +func GeneratePodFilterFunc(filter, filterValue string) ( + func(pod *libpod.Pod) bool, error) { + switch filter { + case "ctr-ids": + return func(p *libpod.Pod) bool { + ctrIds, err := p.AllContainersByID() + if err != nil { + return false + } + return util.StringInSlice(filterValue, ctrIds) + }, nil + case "ctr-names": + return func(p *libpod.Pod) bool { + ctrs, err := p.AllContainers() + if err != nil { + return false + } + for _, ctr := range ctrs { + if filterValue == ctr.Name() { + return true + } + } + return false + }, nil + case "ctr-number": + return func(p *libpod.Pod) bool { + ctrIds, err := p.AllContainersByID() + if err != nil { + return false + } + + fVint, err2 := strconv.Atoi(filterValue) + if err2 != nil { + return false + } + return len(ctrIds) == fVint + }, nil + case "ctr-status": + if !util.StringInSlice(filterValue, + []string{"created", "restarting", "running", "paused", + "exited", "unknown"}) { + return nil, errors.Errorf("%s is not a valid status", filterValue) + } + return func(p *libpod.Pod) bool { + ctr_statuses, err := p.Status() + if err != nil { + return false + } + for _, ctr_status := range ctr_statuses { + state := ctr_status.String() + if ctr_status == define.ContainerStateConfigured { + state = "created" + } + if state == filterValue { + return true + } + } + return false + }, nil + case "id": + return func(p *libpod.Pod) bool { + return strings.Contains(p.ID(), filterValue) + }, nil + case "name": + return func(p *libpod.Pod) bool { + return strings.Contains(p.Name(), filterValue) + }, nil + case "status": + if !util.StringInSlice(filterValue, []string{"stopped", "running", "paused", "exited", "dead", "created"}) { + return nil, errors.Errorf("%s is not a valid pod status", filterValue) + } + return func(p *libpod.Pod) bool { + status, err := p.GetPodStatus() + if err != nil { + return false + } + if strings.ToLower(status) == filterValue { + return true + } + return false + }, nil + case "label": + var filterArray = strings.SplitN(filterValue, "=", 2) + var filterKey = filterArray[0] + if len(filterArray) > 1 { + filterValue = filterArray[1] + } else { + filterValue = "" + } + return func(p *libpod.Pod) bool { + for labelKey, labelValue := range p.Labels() { + if labelKey == filterKey && ("" == filterValue || labelValue == filterValue) { + return true + } + } + return false + }, nil + } + return nil, errors.Errorf("%s is an invalid filter", filter) +} diff --git a/pkg/api/handlers/compat/containers_export.go b/pkg/api/handlers/compat/containers_export.go new file mode 100644 index 000000000..37b9fbf2b --- /dev/null +++ b/pkg/api/handlers/compat/containers_export.go @@ -0,0 +1,42 @@ +package compat + +import ( + "io/ioutil" + "net/http" + "os" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/pkg/errors" +) + +func ExportContainer(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := utils.GetName(r) + con, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + tmpfile, err := ioutil.TempFile("", "api.tar") + if err != nil { + utils.Error(w, "unable to create tarball tempfile", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) + return + } + defer os.Remove(tmpfile.Name()) + if err := tmpfile.Close(); err != nil { + utils.Error(w, "unable to close tempfile", http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile")) + return + } + if err := con.Export(tmpfile.Name()); err != nil { + utils.Error(w, "failed to save the image", http.StatusInternalServerError, errors.Wrap(err, "failed to save image")) + return + } + rdr, err := os.Open(tmpfile.Name()) + if err != nil { + utils.Error(w, "failed to read temp tarball", http.StatusInternalServerError, errors.Wrap(err, "failed to read the exported tarfile")) + return + } + defer rdr.Close() + utils.WriteResponse(w, http.StatusOK, rdr) +} diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 4b24d7d9f..19c6102be 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -254,7 +254,7 @@ func ImagesLoad(w http.ResponseWriter, r *http.Request) { return } } - utils.WriteResponse(w, http.StatusOK, handlers.LibpodImagesLoadReport{ID: loadedImage}) + utils.WriteResponse(w, http.StatusOK, entities.ImageLoadReport{Name: loadedImage}) } func ImagesImport(w http.ResponseWriter, r *http.Request) { @@ -303,6 +303,10 @@ func ImagesImport(w http.ResponseWriter, r *http.Request) { utils.WriteResponse(w, http.StatusOK, handlers.LibpodImagesImportReport{ID: importedImage}) } +// ImagesPull is the v2 libpod endpoint for pulling images. Note that the +// mandatory `reference` must be a reference to a registry (i.e., of docker +// transport or be normalized to one). Other transports are rejected as they +// do not make sense in a remote context. func ImagesPull(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) @@ -328,10 +332,11 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) { return } // Enforce the docker transport. This is just a precaution as some callers - // might accustomed to using the "transport:reference" notation. Using + // might be accustomed to using the "transport:reference" notation. Using // another than the "docker://" transport does not really make sense for a // remote case. For loading tarballs, the load and import endpoints should // be used. + dockerPrefix := fmt.Sprintf("%s://", docker.Transport.Name()) imageRef, err := alltransports.ParseImageName(query.Reference) if err == nil && imageRef.Transport().Name() != docker.Transport.Name() { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, @@ -339,7 +344,7 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) { return } else if err != nil { origErr := err - imageRef, err = alltransports.ParseImageName(fmt.Sprintf("%s://%s", docker.Transport.Name(), query.Reference)) + imageRef, err = alltransports.ParseImageName(fmt.Sprintf("%s%s", dockerPrefix, query.Reference)) if err != nil { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, errors.Wrapf(origErr, "reference %q must be a docker reference", query.Reference)) @@ -347,16 +352,19 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) { } } + // Trim the docker-transport prefix. + rawImage := strings.TrimPrefix(query.Reference, dockerPrefix) + // all-tags doesn't work with a tagged reference, so let's check early - namedRef, err := reference.Parse(query.Reference) + namedRef, err := reference.Parse(rawImage) if err != nil { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, - errors.Wrapf(err, "error parsing reference %q", query.Reference)) + errors.Wrapf(err, "error parsing reference %q", rawImage)) return } if _, isTagged := namedRef.(reference.Tagged); isTagged && query.AllTags { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, - errors.Errorf("reference %q must not have a tag for all-tags", query.Reference)) + errors.Errorf("reference %q must not have a tag for all-tags", rawImage)) return } @@ -502,3 +510,29 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusOK, handlers.IDResponse{ID: commitImage.ID()}) // nolint } + +func UntagImage(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + name := utils.GetName(r) + newImage, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.ImageNotFound(w, name, errors.Wrapf(err, "Failed to find image %s", name)) + return + } + tag := "latest" + if len(r.Form.Get("tag")) > 0 { + tag = r.Form.Get("tag") + } + if len(r.Form.Get("repo")) < 1 { + utils.Error(w, "repo tag is required", http.StatusBadRequest, errors.New("repo parameter is required to tag an image")) + return + } + repo := r.Form.Get("repo") + tagName := fmt.Sprintf("%s:%s", repo, tag) + if err := newImage.UntagImage(tagName); err != nil { + utils.Error(w, "failed to untag", http.StatusInternalServerError, err) + return + } + utils.WriteResponse(w, http.StatusCreated, "") +} diff --git a/pkg/api/handlers/swagger.go b/pkg/api/handlers/swagger.go index 52763a050..12de3a3bd 100644 --- a/pkg/api/handlers/swagger.go +++ b/pkg/api/handlers/swagger.go @@ -31,7 +31,7 @@ type swagImageInspect struct { // swagger:response DocsLibpodImagesLoadResponse type swagLibpodImagesLoadResponse struct { // in:body - Body []LibpodImagesLoadReport + Body entities.ImageLoadReport } // Import response diff --git a/pkg/api/handlers/utils/pods.go b/pkg/api/handlers/utils/pods.go index 79d1a5090..d47053eda 100644 --- a/pkg/api/handlers/utils/pods.go +++ b/pkg/api/handlers/utils/pods.go @@ -59,6 +59,10 @@ func GetPods(w http.ResponseWriter, r *http.Request) ([]*entities.ListPodsReport if err != nil { return nil, err } + infraId, err := pod.InfraContainerID() + if err != nil { + return nil, err + } lp := entities.ListPodsReport{ Cgroup: pod.CgroupParent(), Created: pod.CreatedTime(), @@ -66,6 +70,7 @@ func GetPods(w http.ResponseWriter, r *http.Request) ([]*entities.ListPodsReport Name: pod.Name(), Namespace: pod.Namespace(), Status: status, + InfraId: infraId, } for _, ctr := range ctrs { state, err := ctr.State() diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index 08834ff01..145c054c0 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -587,6 +587,29 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { r.HandleFunc(VersionedPath("/containers/{name}/resize"), s.APIHandler(compat.ResizeContainer)).Methods(http.MethodPost) // Added non version path to URI to support docker non versioned paths r.HandleFunc("/containers/{name}/resize", s.APIHandler(compat.ResizeContainer)).Methods(http.MethodPost) + // swagger:operation GET /containers/{name}/export compat exportContainer + // --- + // tags: + // - containers (compat) + // summary: Export a container + // description: Export the contents of a container as a tarball. + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // 200: + // description: tarball is returned in body + // 404: + // $ref: "#/responses/NoSuchContainer" + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/{name}/export"), s.APIHandler(compat.ExportContainer)).Methods(http.MethodGet) + r.HandleFunc("/containers/{name}/export", s.APIHandler(compat.ExportContainer)).Methods(http.MethodGet) /* libpod endpoints @@ -1237,5 +1260,27 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/resize"), s.APIHandler(compat.ResizeContainer)).Methods(http.MethodPost) + // swagger:operation GET /libpod/containers/{name}/export libpod libpodExportContainer + // --- + // tags: + // - containers + // summary: Export a container + // description: Export the contents of a container as a tarball. + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // 200: + // description: tarball is returned in body + // 404: + // $ref: "#/responses/NoSuchContainer" + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name}/export"), s.APIHandler(compat.ExportContainer)).Methods(http.MethodGet) return nil } diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index e8dfe2fa8..74b245a77 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -1019,5 +1019,39 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/commit"), s.APIHandler(libpod.CommitContainer)).Methods(http.MethodPost) + // swagger:operation POST /libpod/images/{name:.*}/untag libpod libpodUntagImage + // --- + // tags: + // - images + // summary: Untag an image + // description: Untag an image + // parameters: + // - in: path + // name: name:.* + // type: string + // required: true + // description: the name or ID of the container + // - in: query + // name: repo + // type: string + // description: the repository to untag + // - in: query + // name: tag + // type: string + // description: the name of the tag to untag + // produces: + // - application/json + // responses: + // 201: + // description: no error + // 400: + // $ref: '#/responses/BadParamError' + // 404: + // $ref: '#/responses/NoSuchImage' + // 409: + // $ref: '#/responses/ConflictError' + // 500: + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/libpod/images/{name:.*}/untag"), s.APIHandler(libpod.UntagImage)).Methods(http.MethodPost) return nil } diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go index bad1294f4..49a2dfd58 100644 --- a/pkg/bindings/containers/containers.go +++ b/pkg/bindings/containers/containers.go @@ -2,6 +2,7 @@ package containers import ( "context" + "io" "net/http" "net/url" "strconv" @@ -296,3 +297,22 @@ func Stop(ctx context.Context, nameOrID string, timeout *uint) error { } return response.Process(nil) } + +// Export creates a tarball of the given name or ID of a container. It +// requires an io.Writer be provided to write the tarball. +func Export(ctx context.Context, nameOrID string, w io.Writer) error { + params := url.Values{} + conn, err := bindings.GetClient(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodGet, "/containers/%s/export", params, nameOrID) + if err != nil { + return err + } + if response.StatusCode/100 == 2 { + _, err = io.Copy(w, response.Body) + return err + } + return response.Process(nil) +} diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index 5e3af7a60..6c739494b 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -8,10 +8,10 @@ import ( "net/url" "strconv" + "github.com/containers/image/v5/types" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/bindings" "github.com/containers/libpod/pkg/domain/entities" - "github.com/containers/libpod/pkg/inspect" ) // Exists a lightweight way to determine if an image exists in local storage. It returns a @@ -56,7 +56,7 @@ func List(ctx context.Context, all *bool, filters map[string][]string) ([]*entit // Get performs an image inspect. To have the on-disk size of the image calculated, you can // use the optional size parameter. -func GetImage(ctx context.Context, nameOrID string, size *bool) (*inspect.ImageData, error) { +func GetImage(ctx context.Context, nameOrID string, size *bool) (*entities.ImageData, error) { conn, err := bindings.GetClient(ctx) if err != nil { return nil, err @@ -65,7 +65,7 @@ func GetImage(ctx context.Context, nameOrID string, size *bool) (*inspect.ImageD if size != nil { params.Set("size", strconv.FormatBool(*size)) } - inspectedData := inspect.ImageData{} + inspectedData := entities.ImageData{} response, err := conn.DoRequest(nil, http.MethodGet, "/images/%s/json", params, nameOrID) if err != nil { return &inspectedData, err @@ -91,11 +91,11 @@ func History(ctx context.Context, nameOrID string) ([]*handlers.HistoryResponse, return history, response.Process(&history) } -func Load(ctx context.Context, r io.Reader, name *string) (string, error) { - var id handlers.IDResponse +func Load(ctx context.Context, r io.Reader, name *string) (*entities.ImageLoadReport, error) { + var report entities.ImageLoadReport conn, err := bindings.GetClient(ctx) if err != nil { - return "", err + return nil, err } params := url.Values{} if name != nil { @@ -103,9 +103,9 @@ func Load(ctx context.Context, r io.Reader, name *string) (string, error) { } response, err := conn.DoRequest(r, http.MethodPost, "/images/load", params) if err != nil { - return "", err + return nil, err } - return id.ID, response.Process(&id) + return &report, response.Process(&report) } // Remove deletes an image from local storage. The optional force parameter will forcibly remove @@ -196,6 +196,22 @@ func Tag(ctx context.Context, nameOrID, tag, repo string) error { return response.Process(nil) } +// Untag removes a name from locally-stored image. Both the tag and repo parameters are required. +func Untag(ctx context.Context, nameOrID, tag, repo string) error { + conn, err := bindings.GetClient(ctx) + if err != nil { + return err + } + params := url.Values{} + params.Set("tag", tag) + params.Set("repo", repo) + response, err := conn.DoRequest(nil, http.MethodPost, "/images/%s/untag", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + func Build(nameOrId string) {} // Imports adds the given image to the local image store. This can be done by file and the given reader @@ -229,3 +245,41 @@ func Import(ctx context.Context, changes []string, message, reference, u *string } return id.ID, response.Process(&id) } + +// Pull is the binding for libpod's v2 endpoints for pulling images. Note that +// `rawImage` must be a reference to a registry (i.e., of docker transport or be +// normalized to one). Other transports are rejected as they do not make sense +// in a remote context. +func Pull(ctx context.Context, rawImage string, options entities.ImagePullOptions) ([]string, error) { + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + params.Set("reference", rawImage) + params.Set("credentials", options.Credentials) + params.Set("overrideArch", options.OverrideArch) + params.Set("overrideOS", options.OverrideOS) + if options.TLSVerify != types.OptionalBoolUndefined { + val := bool(options.TLSVerify == types.OptionalBoolTrue) + params.Set("tlsVerify", strconv.FormatBool(val)) + } + params.Set("allTags", strconv.FormatBool(options.AllTags)) + + response, err := conn.DoRequest(nil, http.MethodPost, "/images/pull", params) + if err != nil { + return nil, err + } + + reports := []handlers.LibpodImagesPullReport{} + if err := response.Process(&reports); err != nil { + return nil, err + } + + pulledImages := []string{} + for _, r := range reports { + pulledImages = append(pulledImages, r.ID) + } + + return pulledImages, nil +} diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go index 13b6086c3..992720196 100644 --- a/pkg/bindings/test/images_test.go +++ b/pkg/bindings/test/images_test.go @@ -9,6 +9,7 @@ import ( "github.com/containers/libpod/pkg/bindings" "github.com/containers/libpod/pkg/bindings/containers" "github.com/containers/libpod/pkg/bindings/images" + "github.com/containers/libpod/pkg/domain/entities" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gexec" @@ -218,7 +219,7 @@ var _ = Describe("Podman images", func() { Expect(err).To(BeNil()) names, err := images.Load(bt.conn, f, nil) Expect(err).To(BeNil()) - Expect(names).To(Equal(alpine.name)) + Expect(names.Name).To(Equal(alpine.name)) exists, err = images.Exists(bt.conn, alpine.name) Expect(err).To(BeNil()) Expect(exists).To(BeTrue()) @@ -234,7 +235,7 @@ var _ = Describe("Podman images", func() { newName := "quay.io/newname:fizzle" names, err = images.Load(bt.conn, f, &newName) Expect(err).To(BeNil()) - Expect(names).To(Equal(alpine.name)) + Expect(names.Name).To(Equal(alpine.name)) exists, err = images.Exists(bt.conn, newName) Expect(err).To(BeNil()) Expect(exists).To(BeTrue()) @@ -353,4 +354,24 @@ var _ = Describe("Podman images", func() { Expect(results).To(ContainElement("docker.io/library/alpine:latest")) }) + // TODO: we really need to extent to pull tests once we have a more sophisticated CI. + It("Image Pull", func() { + rawImage := "docker.io/library/busybox:latest" + + pulledImages, err := images.Pull(bt.conn, rawImage, entities.ImagePullOptions{}) + Expect(err).To(BeNil()) + Expect(len(pulledImages)).To(Equal(1)) + + exists, err := images.Exists(bt.conn, rawImage) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + + // Make sure the normalization AND the full-transport reference works. + _, err = images.Pull(bt.conn, "docker://"+rawImage, entities.ImagePullOptions{}) + Expect(err).To(BeNil()) + + // The v2 endpoint only supports the docker transport. Let's see if that's really true. + _, err = images.Pull(bt.conn, "bogus-transport:bogus.com/image:reference", entities.ImagePullOptions{}) + Expect(err).To(Not(BeNil())) + }) }) diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 3389e4db5..d51124f55 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -98,12 +98,6 @@ type RmReport struct { Id string } -type ContainerInspectOptions struct { - Format string - Latest bool - Size bool -} - type ContainerInspectReport struct { *define.InspectContainerData } @@ -123,3 +117,7 @@ type CommitOptions struct { type CommitReport struct { Id string } + +type ContainerExportOptions struct { + Output string +} diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index dddaa6013..a122857cd 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -2,34 +2,38 @@ package entities import ( "context" + + "github.com/containers/libpod/libpod/define" ) type ContainerEngine interface { ContainerCommit(ctx context.Context, nameOrId string, options CommitOptions) (*CommitReport, error) ContainerExists(ctx context.Context, nameOrId string) (*BoolReport, error) - ContainerInspect(ctx context.Context, namesOrIds []string, options ContainerInspectOptions) ([]*ContainerInspectReport, error) + ContainerInspect(ctx context.Context, namesOrIds []string, options InspectOptions) ([]*ContainerInspectReport, error) + ContainerExport(ctx context.Context, nameOrId string, options ContainerExportOptions) error ContainerKill(ctx context.Context, namesOrIds []string, options KillOptions) ([]*KillReport, error) ContainerPause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error) ContainerRestart(ctx context.Context, namesOrIds []string, options RestartOptions) ([]*RestartReport, error) ContainerRm(ctx context.Context, namesOrIds []string, options RmOptions) ([]*RmReport, error) - ContainerUnpause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error) ContainerStop(ctx context.Context, namesOrIds []string, options StopOptions) ([]*StopReport, error) - ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error) ContainerTop(ctx context.Context, options TopOptions) (*StringSliceReport, error) + ContainerUnpause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error) + ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error) + HealthCheckRun(ctx context.Context, nameOrId string, options HealthCheckOptions) (*define.HealthCheckResults, error) PodCreate(ctx context.Context, opts PodCreateOptions) (*PodCreateReport, error) PodExists(ctx context.Context, nameOrId string) (*BoolReport, error) PodKill(ctx context.Context, namesOrIds []string, options PodKillOptions) ([]*PodKillReport, error) PodPause(ctx context.Context, namesOrIds []string, options PodPauseOptions) ([]*PodPauseReport, error) + PodPs(ctx context.Context, options PodPSOptions) ([]*ListPodsReport, error) PodRestart(ctx context.Context, namesOrIds []string, options PodRestartOptions) ([]*PodRestartReport, error) + PodRm(ctx context.Context, namesOrIds []string, options PodRmOptions) ([]*PodRmReport, error) PodStart(ctx context.Context, namesOrIds []string, options PodStartOptions) ([]*PodStartReport, error) PodStop(ctx context.Context, namesOrIds []string, options PodStopOptions) ([]*PodStopReport, error) - PodRm(ctx context.Context, namesOrIds []string, options PodRmOptions) ([]*PodRmReport, error) - PodUnpause(ctx context.Context, namesOrIds []string, options PodunpauseOptions) ([]*PodUnpauseReport, error) PodTop(ctx context.Context, options PodTopOptions) (*StringSliceReport, error) - + PodUnpause(ctx context.Context, namesOrIds []string, options PodunpauseOptions) ([]*PodUnpauseReport, error) VolumeCreate(ctx context.Context, opts VolumeCreateOptions) (*IdOrNameResponse, error) VolumeInspect(ctx context.Context, namesOrIds []string, opts VolumeInspectOptions) ([]*VolumeInspectReport, error) - VolumeRm(ctx context.Context, namesOrIds []string, opts VolumeRmOptions) ([]*VolumeRmReport, error) - VolumePrune(ctx context.Context, opts VolumePruneOptions) ([]*VolumePruneReport, error) VolumeList(ctx context.Context, opts VolumeListOptions) ([]*VolumeListReport, error) + VolumePrune(ctx context.Context, opts VolumePruneOptions) ([]*VolumePruneReport, error) + VolumeRm(ctx context.Context, namesOrIds []string, opts VolumeRmOptions) ([]*VolumeRmReport, error) } diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index d0c860a04..4cc2a67b9 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -8,6 +8,11 @@ type ImageEngine interface { 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) + Inspect(ctx context.Context, names []string, opts InspectOptions) (*ImageInspectReport, error) List(ctx context.Context, opts ImageListOptions) ([]*ImageSummary, error) Prune(ctx context.Context, opts ImagePruneOptions) (*ImagePruneReport, error) + Pull(ctx context.Context, rawImage string, opts ImagePullOptions) (*ImagePullReport, error) + Tag(ctx context.Context, nameOrId string, tags []string, options ImageTagOptions) error + Untag(ctx context.Context, nameOrId string, tags []string, options ImageUntagOptions) error + Load(ctx context.Context, opts ImageLoadOptions) (*ImageLoadReport, error) } diff --git a/pkg/domain/entities/healthcheck.go b/pkg/domain/entities/healthcheck.go new file mode 100644 index 000000000..a880805f9 --- /dev/null +++ b/pkg/domain/entities/healthcheck.go @@ -0,0 +1,3 @@ +package entities + +type HealthCheckOptions struct{} diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index 20af0356f..a6c7baebd 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -4,6 +4,8 @@ import ( "net/url" "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/types" + "github.com/containers/libpod/pkg/inspect" docker "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/opencontainers/go-digest" @@ -11,7 +13,6 @@ import ( ) type Image struct { - IdOrNamed ID string `json:"Id"` RepoTags []string `json:",omitempty"` RepoDigests []string `json:",omitempty"` @@ -110,11 +111,37 @@ type ImageHistoryReport struct { Layers []ImageHistoryLayer } -type ImageInspectOptions struct { - TypeObject string `json:",omitempty"` - Format string `json:",omitempty"` - Size bool `json:",omitempty"` - Latest bool `json:",omitempty"` +// ImagePullOptions are the arguments for pulling images. +type ImagePullOptions struct { + // AllTags can be specified to pull all tags of the spiecifed image. Note + // that this only works if the specified image does not include a tag. + AllTags bool + // Authfile is the path to the authentication file. Ignored for remote + // calls. + Authfile string + // CertDir is the path to certificate directories. Ignored for remote + // calls. + CertDir string + // Credentials for authenticating against the registry in the format + // USERNAME:PASSWORD. + Credentials string + // OverrideArch will overwrite the local architecture for image pulls. + OverrideArch string + // OverrideOS will overwrite the local operating system (OS) for image + // pulls. + OverrideOS string + // Quiet can be specified to suppress pull progress when pulling. Ignored + // for remote calls. + Quiet bool + // SignaturePolicy to use when pulling. Ignored for remote calls. + SignaturePolicy string + // TLSVerify to enable/disable HTTPS and certificate verification. + TLSVerify types.OptionalBool +} + +// ImagePullReport is the response from pulling one or more images. +type ImagePullReport struct { + Images []string } type ImageListOptions struct { @@ -123,10 +150,6 @@ type ImageListOptions struct { Filters url.Values `json:"filters" schema:"filters"` } -// type ImageListReport struct { -// Images []ImageSummary -// } - type ImagePruneOptions struct { All bool `json:"all" schema:"all"` Filter []string `json:"filter" schema:"filter"` @@ -137,3 +160,27 @@ type ImagePruneReport struct { Report Report Size int64 } + +type ImageTagOptions struct{} +type ImageUntagOptions struct{} + +type ImageData struct { + *inspect.ImageData +} + +type ImageInspectReport struct { + Images []*ImageData + Errors map[string]error +} + +type ImageLoadOptions struct { + Name string + Tag string + Input string + Quiet bool + SignaturePolicy string +} + +type ImageLoadReport struct { + Name string +} diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index d92d1bc7a..a0b2c6cec 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -22,6 +22,7 @@ type ListPodsReport struct { Containers []*ListPodContainer Created time.Time Id string + InfraId string Name string Namespace string Status string @@ -151,3 +152,15 @@ type PodTopOptions struct { Descriptors []string NameOrID string } + +type PodPSOptions struct { + CtrNames bool + CtrIds bool + CtrStatus bool + Filters map[string][]string + Format string + Latest bool + Namespace bool + Quiet bool + Sort string +} diff --git a/pkg/domain/entities/types.go b/pkg/domain/entities/types.go index a1a729584..dd7aaa07f 100644 --- a/pkg/domain/entities/types.go +++ b/pkg/domain/entities/types.go @@ -42,3 +42,10 @@ type NetOptions struct { StaticIP *net.IP StaticMAC *net.HardwareAddr } + +// All CLI inspect commands and inspect sub-commands use the same options +type InspectOptions struct { + Format string `json:",omitempty"` + Latest bool `json:",omitempty"` + Size bool `json:",omitempty"` +} diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index d25af24c5..d4c5ac311 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -243,7 +243,7 @@ func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, return reports, nil } -func (ic *ContainerEngine) ContainerInspect(ctx context.Context, namesOrIds []string, options entities.ContainerInspectOptions) ([]*entities.ContainerInspectReport, error) { +func (ic *ContainerEngine) ContainerInspect(ctx context.Context, namesOrIds []string, options entities.InspectOptions) ([]*entities.ContainerInspectReport, error) { var reports []*entities.ContainerInspectReport ctrs, err := shortcuts.GetContainersByContext(false, options.Latest, namesOrIds, ic.Libpod) if err != nil { @@ -325,3 +325,11 @@ func (ic *ContainerEngine) ContainerCommit(ctx context.Context, nameOrId string, } return &entities.CommitReport{Id: newImage.ID()}, nil } + +func (ic *ContainerEngine) ContainerExport(ctx context.Context, nameOrId string, options entities.ContainerExportOptions) error { + ctr, err := ic.Libpod.LookupContainer(nameOrId) + if err != nil { + return err + } + return ctr.Export(options.Output) +} diff --git a/pkg/domain/infra/abi/healthcheck.go b/pkg/domain/infra/abi/healthcheck.go new file mode 100644 index 000000000..699483243 --- /dev/null +++ b/pkg/domain/infra/abi/healthcheck.go @@ -0,0 +1,26 @@ +// +build ABISupport + +package abi + +import ( + "context" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" +) + +func (ic *ContainerEngine) HealthCheckRun(ctx context.Context, nameOrId string, options entities.HealthCheckOptions) (*define.HealthCheckResults, error) { + status, err := ic.Libpod.HealthCheck(nameOrId) + if err != nil { + return nil, err + } + hcStatus := "unhealthy" + if status == libpod.HealthCheckSuccess { + hcStatus = "healthy" + } + report := define.HealthCheckResults{ + Status: hcStatus, + } + return &report, nil +} diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 44420c1e1..c3a2bb288 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -5,11 +5,23 @@ package abi import ( "context" "fmt" + "io" + "os" + "strings" + "github.com/containers/image/v5/docker" + dockerarchive "github.com/containers/image/v5/docker/archive" + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/transports/alltransports" + "github.com/containers/image/v5/types" + "github.com/containers/libpod/libpod/image" libpodImage "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/domain/entities" + domainUtils "github.com/containers/libpod/pkg/domain/utils" + "github.com/containers/libpod/pkg/util" "github.com/containers/storage" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) func (ir *ImageEngine) Exists(_ context.Context, nameOrId string) (*entities.BoolReport, error) { @@ -134,6 +146,120 @@ func ToDomainHistoryLayer(layer *libpodImage.History) entities.ImageHistoryLayer return l } +func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, options entities.ImagePullOptions) (*entities.ImagePullReport, error) { + var writer io.Writer + if !options.Quiet { + writer = os.Stderr + } + + dockerPrefix := fmt.Sprintf("%s://", docker.Transport.Name()) + imageRef, err := alltransports.ParseImageName(rawImage) + if err != nil { + imageRef, err = alltransports.ParseImageName(fmt.Sprintf("%s%s", dockerPrefix, rawImage)) + if err != nil { + return nil, errors.Errorf("invalid image reference %q", rawImage) + } + } + + // Special-case for docker-archive which allows multiple tags. + 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 &entities.ImagePullReport{Images: []string{newImage[0].ID()}}, nil + } + + var registryCreds *types.DockerAuthConfig + if options.Credentials != "" { + creds, err := util.ParseRegistryCreds(options.Credentials) + if err != nil { + return nil, err + } + registryCreds = creds + } + dockerRegistryOptions := image.DockerRegistryOptions{ + DockerRegistryCreds: registryCreds, + DockerCertPath: options.CertDir, + OSChoice: options.OverrideOS, + ArchitectureChoice: options.OverrideArch, + DockerInsecureSkipTLSVerify: options.TLSVerify, + } + + 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 &entities.ImagePullReport{Images: []string{newImage.ID()}}, nil + } + + // --all-tags requires the docker transport + if imageRef.Transport().Name() != docker.Transport.Name() { + return nil, errors.New("--all-tags requires docker transport") + } + + // Trim the docker-transport prefix. + rawImage = strings.TrimPrefix(rawImage, docker.Transport.Name()) + + // all-tags doesn't work with a tagged reference, so let's check early + namedRef, err := reference.Parse(rawImage) + if err != nil { + return nil, errors.Wrapf(err, "error parsing %q", rawImage) + } + if _, isTagged := namedRef.(reference.Tagged); isTagged { + return nil, errors.New("--all-tags requires a reference without a tag") + + } + + systemContext := image.GetSystemContext("", options.Authfile, false) + tags, err := docker.GetRepositoryTags(ctx, systemContext, imageRef) + if err != nil { + return nil, errors.Wrapf(err, "error getting repository tags") + } + + var foundIDs []string + for _, tag := range tags { + name := rawImage + ":" + tag + newImage, err := ir.Libpod.ImageRuntime().New(ctx, name, options.SignaturePolicy, options.Authfile, writer, &dockerRegistryOptions, image.SigningOptions{}, nil, util.PullImageAlways) + if err != nil { + logrus.Errorf("error pulling image %q", name) + continue + } + foundIDs = append(foundIDs, newImage.ID()) + } + + if len(tags) != len(foundIDs) { + return nil, errors.Errorf("error pulling image %q", rawImage) + } + return &entities.ImagePullReport{Images: foundIDs}, nil +} + +func (ir *ImageEngine) Inspect(ctx context.Context, names []string, opts entities.InspectOptions) (*entities.ImageInspectReport, error) { + report := entities.ImageInspectReport{ + Errors: make(map[string]error), + } + + for _, id := range names { + img, err := ir.Libpod.ImageRuntime().NewFromLocal(id) + if err != nil { + report.Errors[id] = err + continue + } + + results, err := img.Inspect(ctx) + if err != nil { + report.Errors[id] = err + continue + } + + cookedResults := entities.ImageData{} + _ = domainUtils.DeepCopy(&cookedResults, results) + report.Images = append(report.Images, &cookedResults) + } + return &report, nil +} + // func (r *imageRuntime) Delete(ctx context.Context, nameOrId string, opts entities.ImageDeleteOptions) (*entities.ImageDeleteReport, error) { // image, err := r.libpod.ImageEngine().NewFromLocal(nameOrId) // if err != nil { @@ -146,7 +272,7 @@ func ToDomainHistoryLayer(layer *libpodImage.History) entities.ImageHistoryLayer // } // // report := entities.ImageDeleteReport{} -// if err := utils.DeepCopy(&report, results); err != nil { +// if err := domainUtils.DeepCopy(&report, results); err != nil { // return nil, err // } // return &report, nil @@ -164,3 +290,49 @@ func ToDomainHistoryLayer(layer *libpodImage.History) entities.ImageHistoryLayer // copy(report.Report.Id, id) // return &report, nil // } + +func (ir *ImageEngine) Tag(ctx context.Context, nameOrId string, tags []string, options entities.ImageTagOptions) error { + newImage, err := ir.Libpod.ImageRuntime().NewFromLocal(nameOrId) + if err != nil { + return err + } + for _, tag := range tags { + if err := newImage.TagImage(tag); err != nil { + return err + } + } + return nil +} +func (ir *ImageEngine) Untag(ctx context.Context, nameOrId string, tags []string, options entities.ImageUntagOptions) error { + newImage, err := ir.Libpod.ImageRuntime().NewFromLocal(nameOrId) + if err != nil { + return err + } + for _, tag := range tags { + if err := newImage.UntagImage(tag); err != nil { + return err + } + } + return nil +} + +func (ir *ImageEngine) Load(ctx context.Context, opts entities.ImageLoadOptions) (*entities.ImageLoadReport, error) { + var ( + writer io.Writer + ) + if !opts.Quiet { + writer = os.Stderr + } + name, err := ir.Libpod.LoadImage(ctx, opts.Name, opts.Input, writer, opts.SignaturePolicy) + if err != nil { + return nil, err + } + newImage, err := ir.Libpod.ImageRuntime().NewFromLocal(name) + if err != nil { + return nil, errors.Wrap(err, "image loaded but no additional tags were created") + } + if err := newImage.TagImage(opts.Name); err != nil { + return nil, errors.Wrapf(err, "error adding %q to image %q", opts.Name, newImage.InputName) + } + return &entities.ImageLoadReport{Name: name}, nil +} diff --git a/pkg/domain/infra/abi/pods.go b/pkg/domain/infra/abi/pods.go index 8abcc6e4b..494a048ec 100644 --- a/pkg/domain/infra/abi/pods.go +++ b/pkg/domain/infra/abi/pods.go @@ -7,6 +7,7 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/libpod/podfilters" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/signal" "github.com/containers/libpod/pkg/specgen" @@ -272,3 +273,61 @@ func (ic *ContainerEngine) PodTop(ctx context.Context, options entities.PodTopOp report.Value, err = pod.GetPodPidInformation(options.Descriptors) return report, err } + +func (ic *ContainerEngine) PodPs(ctx context.Context, options entities.PodPSOptions) ([]*entities.ListPodsReport, error) { + var ( + filters []libpod.PodFilter + reports []*entities.ListPodsReport + ) + for k, v := range options.Filters { + for _, filter := range v { + f, err := podfilters.GeneratePodFilterFunc(k, filter) + if err != nil { + return nil, err + } + filters = append(filters, f) + + } + } + pds, err := ic.Libpod.Pods(filters...) + if err != nil { + return nil, err + } + for _, p := range pds { + var lpcs []*entities.ListPodContainer + status, err := p.GetPodStatus() + if err != nil { + return nil, err + } + cons, err := p.AllContainers() + if err != nil { + return nil, err + } + for _, c := range cons { + state, err := c.State() + if err != nil { + return nil, err + } + lpcs = append(lpcs, &entities.ListPodContainer{ + Id: c.ID(), + Names: c.Name(), + Status: state.String(), + }) + } + infraId, err := p.InfraContainerID() + if err != nil { + return nil, err + } + reports = append(reports, &entities.ListPodsReport{ + Cgroup: p.CgroupParent(), + Containers: lpcs, + Created: p.CreatedTime(), + Id: p.ID(), + InfraId: infraId, + Name: p.Name(), + Namespace: p.Namespace(), + Status: status, + }) + } + return reports, nil +} diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 3c8be90dc..8885ae7c7 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -2,6 +2,8 @@ package tunnel import ( "context" + "io" + "os" "github.com/containers/image/v5/docker/reference" @@ -142,7 +144,7 @@ func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, return reports, nil } -func (ic *ContainerEngine) ContainerInspect(ctx context.Context, namesOrIds []string, options entities.ContainerInspectOptions) ([]*entities.ContainerInspectReport, error) { +func (ic *ContainerEngine) ContainerInspect(ctx context.Context, namesOrIds []string, options entities.InspectOptions) ([]*entities.ContainerInspectReport, error) { var ( reports []*entities.ContainerInspectReport ) @@ -210,3 +212,17 @@ func (ic *ContainerEngine) ContainerCommit(ctx context.Context, nameOrId string, } return &entities.CommitReport{Id: response.ID}, nil } + +func (ic *ContainerEngine) ContainerExport(ctx context.Context, nameOrId string, options entities.ContainerExportOptions) error { + var ( + err error + w io.Writer + ) + if len(options.Output) > 0 { + w, err = os.Create(options.Output) + if err != nil { + return err + } + } + return containers.Export(ic.ClientCxt, nameOrId, w) +} diff --git a/pkg/domain/infra/tunnel/healthcheck.go b/pkg/domain/infra/tunnel/healthcheck.go new file mode 100644 index 000000000..e589489b3 --- /dev/null +++ b/pkg/domain/infra/tunnel/healthcheck.go @@ -0,0 +1,13 @@ +package tunnel + +import ( + "context" + + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/bindings/containers" + "github.com/containers/libpod/pkg/domain/entities" +) + +func (ic *ContainerEngine) HealthCheckRun(ctx context.Context, nameOrId string, options entities.HealthCheckOptions) (*define.HealthCheckResults, error) { + return containers.RunHealthCheck(ic.ClientCxt, nameOrId) +} diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 6a3adc9ee..a4a0fccaf 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -2,10 +2,13 @@ package tunnel import ( "context" + "os" + "github.com/containers/image/v5/docker/reference" images "github.com/containers/libpod/pkg/bindings/images" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/utils" + "github.com/pkg/errors" ) func (ir *ImageEngine) Exists(_ context.Context, nameOrId string) (*entities.BoolReport, error) { @@ -85,3 +88,82 @@ func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOption } return &report, nil } + +func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, options entities.ImagePullOptions) (*entities.ImagePullReport, error) { + pulledImages, err := images.Pull(ir.ClientCxt, rawImage, options) + if err != nil { + return nil, err + } + return &entities.ImagePullReport{Images: pulledImages}, nil +} + +func (ir *ImageEngine) Tag(ctx context.Context, nameOrId string, tags []string, options entities.ImageTagOptions) error { + for _, newTag := range tags { + var ( + tag, repo string + ) + ref, err := reference.Parse(newTag) + if err != nil { + return err + } + if t, ok := ref.(reference.Tagged); ok { + tag = t.Tag() + } + if r, ok := ref.(reference.Named); ok { + repo = r.Name() + } + if len(repo) < 1 { + return errors.Errorf("invalid image name %q", nameOrId) + } + if err := images.Tag(ir.ClientCxt, nameOrId, tag, repo); err != nil { + return err + } + } + return nil +} + +func (ir *ImageEngine) Untag(ctx context.Context, nameOrId string, tags []string, options entities.ImageUntagOptions) error { + for _, newTag := range tags { + var ( + tag, repo string + ) + ref, err := reference.Parse(newTag) + if err != nil { + return err + } + if t, ok := ref.(reference.Tagged); ok { + tag = t.Tag() + } + if r, ok := ref.(reference.Named); ok { + repo = r.Name() + } + if len(repo) < 1 { + return errors.Errorf("invalid image name %q", nameOrId) + } + if err := images.Untag(ir.ClientCxt, nameOrId, tag, repo); err != nil { + return err + } + } + return nil +} + +func (ir *ImageEngine) Inspect(_ context.Context, names []string, opts entities.InspectOptions) (*entities.ImageInspectReport, error) { + report := entities.ImageInspectReport{} + for _, id := range names { + r, err := images.GetImage(ir.ClientCxt, id, &opts.Size) + if err != nil { + report.Errors[id] = err + } + report.Images = append(report.Images, r) + } + return &report, nil +} + +func (ir *ImageEngine) Load(ctx context.Context, opts entities.ImageLoadOptions) (*entities.ImageLoadReport, error) { + f, err := os.Open(opts.Input) + if err != nil { + return nil, err + } + defer f.Close() + return images.Load(ir.ClientCxt, f, &opts.Name) +} diff --git a/pkg/domain/infra/tunnel/pods.go b/pkg/domain/infra/tunnel/pods.go index 9561a9807..ad87a0a29 100644 --- a/pkg/domain/infra/tunnel/pods.go +++ b/pkg/domain/infra/tunnel/pods.go @@ -193,3 +193,7 @@ func (ic *ContainerEngine) PodTop(ctx context.Context, options entities.PodTopOp } return &entities.StringSliceReport{Value: topOutput}, nil } + +func (ic *ContainerEngine) PodPs(ctx context.Context, options entities.PodPSOptions) ([]*entities.ListPodsReport, error) { + return pods.List(ic.ClientCxt, options.Filters) +} |