From 274d0f49565e5b2dfe8814106bee43c81501ece2 Mon Sep 17 00:00:00 2001 From: Daniel J Walsh Date: Wed, 17 Aug 2022 10:26:59 -0400 Subject: [PATCH] Add --insecure,--tls-verify,--verbose flags to podman manifest inspect --insecure and --verbose flags for docker compatibility --tls-verify for syntax compatibility and allow users to inspect manifests at remote Container Registiries without requiring tls. Helps fix: https://github.com/containers/podman/issues/14917 Signed-off-by: Daniel J Walsh --- cmd/podman/manifest/inspect.go | 23 +++++++++++++-- docs/source/markdown/.gitignore | 1 + docs/source/markdown/options/tls-verify.md | 2 +- ...t.1.md => podman-manifest-inspect.1.md.in} | 7 +++-- pkg/api/handlers/libpod/manifests.go | 18 +++++++++++- pkg/api/server/register_manifest.go | 5 ++++ pkg/bindings/manifests/manifests.go | 18 ++++++++++-- pkg/bindings/manifests/types.go | 1 + .../manifests/types_inspect_options.go | 15 ++++++++++ pkg/domain/entities/engine_image.go | 2 +- pkg/domain/entities/manifest.go | 6 ++++ pkg/domain/infra/abi/manifest.go | 12 ++++++-- pkg/domain/infra/tunnel/manifest.go | 13 +++++++-- test/system/010-images.bats | 14 --------- test/system/012-manifest.bats | 20 +++++++++++++ test/system/150-login.bats | 29 +++++++++++++++++++ 16 files changed, 157 insertions(+), 29 deletions(-) rename docs/source/markdown/{podman-manifest-inspect.1.md => podman-manifest-inspect.1.md.in} (82%) create mode 100644 test/system/012-manifest.bats diff --git a/cmd/podman/manifest/inspect.go b/cmd/podman/manifest/inspect.go index 2cb6941c42..d23e5b3e47 100644 --- a/cmd/podman/manifest/inspect.go +++ b/cmd/podman/manifest/inspect.go @@ -3,14 +3,17 @@ package manifest import ( "fmt" + "github.com/containers/image/v5/types" "github.com/containers/podman/v4/cmd/podman/common" "github.com/containers/podman/v4/cmd/podman/registry" + "github.com/containers/podman/v4/pkg/domain/entities" "github.com/spf13/cobra" ) var ( - inspectCmd = &cobra.Command{ - Use: "inspect IMAGE", + tlsVerifyCLI bool + inspectCmd = &cobra.Command{ + Use: "inspect [options] IMAGE", Short: "Display the contents of a manifest list or image index", Long: "Display the contents of a manifest list or image index.", RunE: inspect, @@ -25,10 +28,24 @@ func init() { Command: inspectCmd, Parent: manifestCmd, }) + flags := inspectCmd.Flags() + + flags.Bool("verbose", false, "Added for Docker compatibility") + _ = flags.MarkHidden("verbose") + flags.BoolVar(&tlsVerifyCLI, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry") + flags.Bool("insecure", false, "Purely for Docker compatibility") + _ = flags.MarkHidden("insecure") } func inspect(cmd *cobra.Command, args []string) error { - buf, err := registry.ImageEngine().ManifestInspect(registry.Context(), args[0]) + opts := entities.ManifestInspectOptions{} + if cmd.Flags().Changed("tls-verify") { + opts.SkipTLSVerify = types.NewOptionalBool(!tlsVerifyCLI) + } else if cmd.Flags().Changed("insecure") { + insecure, _ := cmd.Flags().GetBool("insecure") + opts.SkipTLSVerify = types.NewOptionalBool(insecure) + } + buf, err := registry.ImageEngine().ManifestInspect(registry.Context(), args[0], opts) if err != nil { return err } diff --git a/docs/source/markdown/.gitignore b/docs/source/markdown/.gitignore index 584e88e75b..398b541dc6 100644 --- a/docs/source/markdown/.gitignore +++ b/docs/source/markdown/.gitignore @@ -15,6 +15,7 @@ podman-manifest-add.1.md podman-manifest-annotate.1.md podman-manifest-create.1.md podman-manifest-push.1.md +podman-manifest-inspect.1.md podman-pause.1.md podman-pod-clone.1.md podman-pod-create.1.md diff --git a/docs/source/markdown/options/tls-verify.md b/docs/source/markdown/options/tls-verify.md index 597beb6755..b2905f661e 100644 --- a/docs/source/markdown/options/tls-verify.md +++ b/docs/source/markdown/options/tls-verify.md @@ -1,5 +1,5 @@ ####> This option file is used in: -####> podman build, container runlabel, create, kube play, login, manifest add, manifest create, manifest push, pull, push, run, search +####> podman build, container runlabel, create, kube play, login, manifest add, manifest create, manifest inspect, manifest push, pull, push, run, search ####> If you edit this file, make sure your changes ####> are applicable to all of those. #### **--tls-verify** diff --git a/docs/source/markdown/podman-manifest-inspect.1.md b/docs/source/markdown/podman-manifest-inspect.1.md.in similarity index 82% rename from docs/source/markdown/podman-manifest-inspect.1.md rename to docs/source/markdown/podman-manifest-inspect.1.md.in index 4b7fc3a408..89993eaa8a 100644 --- a/docs/source/markdown/podman-manifest-inspect.1.md +++ b/docs/source/markdown/podman-manifest-inspect.1.md.in @@ -4,16 +4,19 @@ podman\-manifest\-inspect - Display a manifest list or image index ## SYNOPSIS -**podman manifest inspect** *listnameorindexname* +**podman manifest inspect** [*options*] *listnameorindexname* ## DESCRIPTION Displays the manifest list or image index stored using the specified image name. - ## RETURN VALUE A formatted JSON representation of the manifest list or image index. +## OPTIONS + +@@option tls-verify + ## EXAMPLES ``` diff --git a/pkg/api/handlers/libpod/manifests.go b/pkg/api/handlers/libpod/manifests.go index c96e4936bb..e9cf8f259d 100644 --- a/pkg/api/handlers/libpod/manifests.go +++ b/pkg/api/handlers/libpod/manifests.go @@ -139,10 +139,26 @@ func ManifestExists(w http.ResponseWriter, r *http.Request) { func ManifestInspect(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) + decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) name := utils.GetName(r) + // Wrapper to support 3.x with 4.x libpod + query := struct { + TLSVerify bool `schema:"tlsVerify"` + }{} + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusBadRequest, + fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) + return + } imageEngine := abi.ImageEngine{Libpod: runtime} - rawManifest, err := imageEngine.ManifestInspect(r.Context(), name) + opts := entities.ManifestInspectOptions{} + if _, found := r.URL.Query()["tlsVerify"]; found { + opts.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify) + } + + rawManifest, err := imageEngine.ManifestInspect(r.Context(), name, opts) if err != nil { utils.Error(w, http.StatusNotFound, err) return diff --git a/pkg/api/server/register_manifest.go b/pkg/api/server/register_manifest.go index 7a55eaefe0..90edad1d02 100644 --- a/pkg/api/server/register_manifest.go +++ b/pkg/api/server/register_manifest.go @@ -175,6 +175,11 @@ func (s *APIServer) registerManifestHandlers(r *mux.Router) error { // type: string // required: true // description: the name or ID of the manifest list + // - in: query + // name: tlsVerify + // type: boolean + // default: true + // description: Require HTTPS and verify signatures when contacting registries. // responses: // 200: // $ref: "#/responses/manifestInspect" diff --git a/pkg/bindings/manifests/manifests.go b/pkg/bindings/manifests/manifests.go index ed605bd7b2..4cf96c2968 100644 --- a/pkg/bindings/manifests/manifests.go +++ b/pkg/bindings/manifests/manifests.go @@ -71,13 +71,27 @@ func Exists(ctx context.Context, name string, options *ExistsOptions) (bool, err } // Inspect returns a manifest list for a given name. -func Inspect(ctx context.Context, name string, _ *InspectOptions) (*manifest.Schema2List, error) { +func Inspect(ctx context.Context, name string, options *InspectOptions) (*manifest.Schema2List, error) { conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } + if options == nil { + options = new(InspectOptions) + } + + params, err := options.ToParams() + if err != nil { + return nil, err + } + // SkipTLSVerify is special. We need to delete the param added by + // ToParams() and change the key and flip the bool + if options.SkipTLSVerify != nil { + params.Del("SkipTLSVerify") + params.Set("tlsVerify", strconv.FormatBool(!options.GetSkipTLSVerify())) + } - response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/manifests/%s/json", nil, nil, name) + response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/manifests/%s/json", params, nil, name) if err != nil { return nil, err } diff --git a/pkg/bindings/manifests/types.go b/pkg/bindings/manifests/types.go index 45c15f9540..d0485e9319 100644 --- a/pkg/bindings/manifests/types.go +++ b/pkg/bindings/manifests/types.go @@ -4,6 +4,7 @@ package manifests // //go:generate go run ../generator/generator.go InspectOptions type InspectOptions struct { + SkipTLSVerify *bool } // CreateOptions are optional options for creating manifests diff --git a/pkg/bindings/manifests/types_inspect_options.go b/pkg/bindings/manifests/types_inspect_options.go index f5a2df4dd4..fb0c397aec 100644 --- a/pkg/bindings/manifests/types_inspect_options.go +++ b/pkg/bindings/manifests/types_inspect_options.go @@ -16,3 +16,18 @@ func (o *InspectOptions) Changed(fieldName string) bool { func (o *InspectOptions) ToParams() (url.Values, error) { return util.ToParams(o) } + +// WithSkipTLSVerify set field SkipTLSVerify to given value +func (o *InspectOptions) WithSkipTLSVerify(value bool) *InspectOptions { + o.SkipTLSVerify = &value + return o +} + +// GetSkipTLSVerify returns value of field SkipTLSVerify +func (o *InspectOptions) GetSkipTLSVerify() bool { + if o.SkipTLSVerify == nil { + var z bool + return z + } + return *o.SkipTLSVerify +} diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index 43de27a756..0de0a889ca 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -34,7 +34,7 @@ type ImageEngine interface { //nolint:interfacebloat Untag(ctx context.Context, nameOrID string, tags []string, options ImageUntagOptions) error ManifestCreate(ctx context.Context, name string, images []string, opts ManifestCreateOptions) (string, error) ManifestExists(ctx context.Context, name string) (*BoolReport, error) - ManifestInspect(ctx context.Context, name string) ([]byte, error) + ManifestInspect(ctx context.Context, name string, opts ManifestInspectOptions) ([]byte, error) ManifestAdd(ctx context.Context, listName string, imageNames []string, opts ManifestAddOptions) (string, error) ManifestAnnotate(ctx context.Context, names, image string, opts ManifestAnnotateOptions) (string, error) ManifestRemoveDigest(ctx context.Context, names, image string) (string, error) diff --git a/pkg/domain/entities/manifest.go b/pkg/domain/entities/manifest.go index f17079271a..2da8e7a7c6 100644 --- a/pkg/domain/entities/manifest.go +++ b/pkg/domain/entities/manifest.go @@ -12,6 +12,12 @@ type ManifestCreateOptions struct { SkipTLSVerify types.OptionalBool `json:"-" schema:"-"` } +// ManifestInspectOptions provides model for inspecting manifest +type ManifestInspectOptions struct { + // Should TLS registry certificate be verified? + SkipTLSVerify types.OptionalBool `json:"-" schema:"-"` +} + // ManifestAddOptions provides model for adding digests to manifest list // // swagger:model diff --git a/pkg/domain/infra/abi/manifest.go b/pkg/domain/infra/abi/manifest.go index ac3eedbe83..e5438421d9 100644 --- a/pkg/domain/infra/abi/manifest.go +++ b/pkg/domain/infra/abi/manifest.go @@ -17,6 +17,7 @@ import ( "github.com/containers/image/v5/pkg/shortnames" "github.com/containers/image/v5/transports" "github.com/containers/image/v5/transports/alltransports" + "github.com/containers/image/v5/types" "github.com/containers/podman/v4/pkg/domain/entities" "github.com/containers/storage" "github.com/opencontainers/go-digest" @@ -67,7 +68,7 @@ func (ir *ImageEngine) ManifestExists(ctx context.Context, name string) (*entiti } // ManifestInspect returns the content of a manifest list or image -func (ir *ImageEngine) ManifestInspect(ctx context.Context, name string) ([]byte, error) { +func (ir *ImageEngine) ManifestInspect(ctx context.Context, name string, opts entities.ManifestInspectOptions) ([]byte, error) { // NOTE: we have to do a bit of a limbo here as `podman manifest // inspect foo` wants to do a remote-inspect of foo iff "foo" in the // containers storage is an ordinary image but not a manifest list. @@ -77,7 +78,7 @@ func (ir *ImageEngine) ManifestInspect(ctx context.Context, name string) ([]byte if errors.Is(err, storage.ErrImageUnknown) || errors.Is(err, libimage.ErrNotAManifestList) { // Do a remote inspect if there's no local image or if the // local image is not a manifest list. - return ir.remoteManifestInspect(ctx, name) + return ir.remoteManifestInspect(ctx, name, opts) } return nil, err @@ -101,9 +102,14 @@ func (ir *ImageEngine) ManifestInspect(ctx context.Context, name string) ([]byte } // inspect a remote manifest list. -func (ir *ImageEngine) remoteManifestInspect(ctx context.Context, name string) ([]byte, error) { +func (ir *ImageEngine) remoteManifestInspect(ctx context.Context, name string, opts entities.ManifestInspectOptions) ([]byte, error) { sys := ir.Libpod.SystemContext() + sys.DockerInsecureSkipTLSVerify = opts.SkipTLSVerify + if opts.SkipTLSVerify == types.OptionalBoolTrue { + sys.OCIInsecureSkipTLSVerify = true + } + resolved, err := shortnames.Resolve(sys, name) if err != nil { return nil, err diff --git a/pkg/domain/infra/tunnel/manifest.go b/pkg/domain/infra/tunnel/manifest.go index 696d0a963a..6e2513ef2b 100644 --- a/pkg/domain/infra/tunnel/manifest.go +++ b/pkg/domain/infra/tunnel/manifest.go @@ -33,8 +33,17 @@ func (ir *ImageEngine) ManifestExists(ctx context.Context, name string) (*entiti } // ManifestInspect returns contents of manifest list with given name -func (ir *ImageEngine) ManifestInspect(_ context.Context, name string) ([]byte, error) { - list, err := manifests.Inspect(ir.ClientCtx, name, nil) +func (ir *ImageEngine) ManifestInspect(ctx context.Context, name string, opts entities.ManifestInspectOptions) ([]byte, error) { + options := new(manifests.InspectOptions) + if s := opts.SkipTLSVerify; s != types.OptionalBoolUndefined { + if s == types.OptionalBoolTrue { + options.WithSkipTLSVerify(true) + } else { + options.WithSkipTLSVerify(false) + } + } + + list, err := manifests.Inspect(ir.ClientCtx, name, options) if err != nil { return nil, fmt.Errorf("getting content of manifest list or image %s: %w", name, err) } diff --git a/test/system/010-images.bats b/test/system/010-images.bats index 16ee681a3a..7f9abcfdbf 100644 --- a/test/system/010-images.bats +++ b/test/system/010-images.bats @@ -232,20 +232,6 @@ Labels.created_at | 20[0-9-]\\\+T[0-9:]\\\+Z run_podman rmi ${aaa_name}:${aaa_tag} ${zzz_name}:${zzz_tag} } -# Regression test for #8931 -@test "podman images - bare manifest list" { - # Create an empty manifest list and list images. - - run_podman inspect --format '{{.ID}}' $IMAGE - iid=$output - - run_podman manifest create test:1.0 - run_podman images --format '{{.ID}}' --no-trunc - [[ "$output" == *"sha256:$iid"* ]] - - run_podman rmi test:1.0 -} - @test "podman images - rmi -af removes all containers and pods" { pname=$(random_string) run_podman create --pod new:$pname $IMAGE diff --git a/test/system/012-manifest.bats b/test/system/012-manifest.bats new file mode 100644 index 0000000000..49ccd5e344 --- /dev/null +++ b/test/system/012-manifest.bats @@ -0,0 +1,20 @@ +#!/usr/bin/env bats + +load helpers + +# Regression test for #8931 +@test "podman images - bare manifest list" { + # Create an empty manifest list and list images. + + run_podman inspect --format '{{.ID}}' $IMAGE + iid=$output + + run_podman manifest create test:1.0 + run_podman manifest inspect --verbose $output + is "$output" ".*\"mediaType\": \"application/vnd.docker.distribution.manifest.list.v2+json\"" "--insecure is a noop want to make sure manifest inspect is successful" + run_podman images --format '{{.ID}}' --no-trunc + is "$output" ".*sha256:$iid" "Original image ID still shown in podman-images output" + run_podman rmi test:1.0 +} + +# vim: filetype=sh diff --git a/test/system/150-login.bats b/test/system/150-login.bats index 27689452ff..685befbff7 100644 --- a/test/system/150-login.bats +++ b/test/system/150-login.bats @@ -290,6 +290,35 @@ function _test_skopeo_credential_sharing() { rm -f $authfile } +@test "podman manifest --tls-verify - basic test" { + run_podman login --tls-verify=false \ + --username ${PODMAN_LOGIN_USER} \ + --password-stdin \ + localhost:${PODMAN_LOGIN_REGISTRY_PORT} <<<"${PODMAN_LOGIN_PASS}" + is "$output" "Login Succeeded!" "output from podman login" + + manifest1="localhost:${PODMAN_LOGIN_REGISTRY_PORT}/test:1.0" + run_podman manifest create $manifest1 + mid=$output + run_podman manifest push --authfile=$authfile \ + --tls-verify=false $mid \ + $manifest1 + run_podman manifest rm $manifest1 + run_podman manifest inspect --insecure $manifest1 + is "$output" ".*\"mediaType\": \"application/vnd.docker.distribution.manifest.list.v2+json\"" "Verify --insecure works against an insecure registry" + run_podman 125 manifest inspect --insecure=false $manifest1 + is "$output" ".*Error: reading image \"docker://$manifest1\": pinging container registry localhost:${PODMAN_LOGIN_REGISTRY_PORT}:" "Verify --insecure=false fails" + run_podman manifest inspect --tls-verify=false $manifest1 + is "$output" ".*\"mediaType\": \"application/vnd.docker.distribution.manifest.list.v2+json\"" "Verify --tls-verify=false works against an insecure registry" + run_podman 125 manifest inspect --tls-verify=true $manifest1 + is "$output" ".*Error: reading image \"docker://$manifest1\": pinging container registry localhost:${PODMAN_LOGIN_REGISTRY_PORT}:" "Verify --tls-verify=true fails" + + # Now log out + run_podman logout localhost:${PODMAN_LOGIN_REGISTRY_PORT} + is "$output" "Removed login credentials for localhost:${PODMAN_LOGIN_REGISTRY_PORT}" \ + "output from podman logout" +} + # END cooperation with skopeo # END actual tests ###############################################################################