diff --git a/cmd/podman/artifact/inspect.go b/cmd/podman/artifact/inspect.go index a252e71f65..ca51612001 100644 --- a/cmd/podman/artifact/inspect.go +++ b/cmd/podman/artifact/inspect.go @@ -1,23 +1,28 @@ package artifact import ( + "os" + "github.com/containers/podman/v5/cmd/podman/common" + "github.com/containers/podman/v5/cmd/podman/inspect" "github.com/containers/podman/v5/cmd/podman/registry" "github.com/containers/podman/v5/cmd/podman/utils" "github.com/containers/podman/v5/pkg/domain/entities" "github.com/spf13/cobra" + "go.podman.io/common/pkg/report" ) var ( inspectCmd = &cobra.Command{ - Use: "inspect [ARTIFACT...]", + Use: "inspect [options] ARTIFACT", Short: "Inspect an OCI artifact", Long: "Provide details on an OCI artifact", - RunE: inspect, - Args: cobra.MinimumNArgs(1), + RunE: artifactInspect, + Args: cobra.ExactArgs(1), ValidArgsFunction: common.AutocompleteArtifacts, Example: `podman artifact inspect quay.io/myimage/myartifact:latest`, } + inspectOpts *entities.InspectOptions ) func init() { @@ -26,10 +31,11 @@ func init() { Parent: artifactCmd, }) - // TODO When things firm up on inspect looks, we can do a format implementation - // flags := inspectCmd.Flags() - // formatFlagName := "format" - // flags.StringVar(&inspectFlag.format, formatFlagName, "", "Format volume output using JSON or a Go template") + inspectOpts = new(entities.InspectOptions) + + flags := inspectCmd.Flags() + formatFlagName := "format" + flags.StringVarP(&inspectOpts.Format, formatFlagName, "f", "json", "Format volume output using JSON or a Go template") // This is something we wanted to do but did not seem important enough for initial PR // remoteFlagName := "remote" @@ -37,14 +43,33 @@ func init() { // TODO When the inspect structure has been defined, we need to uncomment and redirect this. Reminder, this // will also need to be reflected in the podman-artifact-inspect man page - // _ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&machine.InspectInfo{})) + _ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&entities.ArtifactInspectReport{})) } -func inspect(_ *cobra.Command, args []string) error { +func artifactInspect(_ *cobra.Command, args []string) error { artifactOptions := entities.ArtifactInspectOptions{} inspectData, err := registry.ImageEngine().ArtifactInspect(registry.Context(), args[0], artifactOptions) if err != nil { return err } - return utils.PrintGenericJSON(inspectData) + + switch { + case report.IsJSON(inspectOpts.Format) || inspectOpts.Format == "": + return utils.PrintGenericJSON(inspectData) + default: + // Landing here implies user has given a custom --format + var rpt *report.Formatter + format := inspect.InspectNormalize(inspectOpts.Format, inspectOpts.Type) + rpt, err = report.New(os.Stdout, "inspect").Parse(report.OriginUser, format) + if err != nil { + return err + } + defer rpt.Flush() + + // Storing and passing inspectData in an array to [Execute] is workaround to avoid getting an error. + // Which seems to happen when type passed to [Execute] is not a slice. + // Error: template: inspect:1:8: executing "inspect" at <.>: range can't iterate over {0x6600020c444 sha256:4bafff5c1b2c950651101d22d3dbf76744446aeb5f79fc926674e0db1083qew456} + data := []any{inspectData} + return rpt.Execute(data) + } } diff --git a/cmd/podman/inspect/inspect.go b/cmd/podman/inspect/inspect.go index cbba736a61..e9f397e5ba 100644 --- a/cmd/podman/inspect/inspect.go +++ b/cmd/podman/inspect/inspect.go @@ -176,7 +176,7 @@ func (i *inspector) inspect(namesOrIDs []string) error { default: // Landing here implies user has given a custom --format var rpt *report.Formatter - format := inspectNormalize(i.options.Format, i.options.Type) + format := InspectNormalize(i.options.Format, i.options.Type) rpt, err = report.New(os.Stdout, "inspect").Parse(report.OriginUser, format) if err != nil { return err @@ -258,7 +258,22 @@ func (i *inspector) inspectAll(ctx context.Context, namesOrIDs []string) ([]any, return data, allErrs, nil } -func inspectNormalize(row string, inspectType string) string { +// InspectNormalize modifies a given row string based on the specified inspect type. +// It replaces specific field names within the row string for standardization. +// For the `image` inspect type, it includes additional field replacements like `.Config.Healthcheck`. +// +// Parameters: +// - row: The input string that represents a data row to be modified. +// - inspectType: The type of inspection (e.g., "image") to determine specific replacements. +// +// Returns: +// - A new string with the necessary replacements applied based on the inspect type. +// +// InspectNormalize does not need to be exported but to avoid de-duplication of code. We had to export it. +// It can be reverted back once `podman artifact inspect` can use [Inspect] to fetch artifact data instead of +// fetching it itself. +// The reason why we did it in this way can be further read [here](https://github.com/containers/podman/pull/27182#issuecomment-3402465389). +func InspectNormalize(row string, inspectType string) string { m := regexp.MustCompile(`{{\s*\.Id\s*}}`) row = m.ReplaceAllString(row, "{{.ID}}") diff --git a/docs/source/markdown/podman-artifact-inspect.1.md b/docs/source/markdown/podman-artifact-inspect.1.md index 5d4cf47f8d..54e765da01 100644 --- a/docs/source/markdown/podman-artifact-inspect.1.md +++ b/docs/source/markdown/podman-artifact-inspect.1.md @@ -4,7 +4,7 @@ podman\-artifact\-inspect - Inspect an OCI artifact ## SYNOPSIS -**podman artifact inspect** [*name*] ... +**podman artifact inspect** *name* ## DESCRIPTION @@ -20,15 +20,62 @@ annotation using RFC3339Nano format, showing when the artifact was initially cre ## OPTIONS -#### **--help** +#### **--format**, **-f**=*format* -Print usage statement. +Format the output using the given Go template. +The keys of the returned JSON can be used as the values for the --format flag (see examples below). + +Valid placeholders for the Go template are listed below: + +| **Placeholder** | **Description** | +| ------------------------ | -------------------------------------------------- | +| .Artifact ... | Artifact details (nested struct) | +| .Digest | Artifact digest (sha256:+64-char hash) | +| .Manifest ... | Artifact manifest details (struct) | +| .Name | Artifact name (string) | +| .TotalSizeBytes | Total Size of the artifact in bytes | + +#### **--help**, **-h** + +Print usage statement ## EXAMPLES Inspect an OCI image in the local store. -``` + +```shell $ podman artifact inspect quay.io/myartifact/myml:latest +{ + "Manifest": { + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.empty.v1+json", + "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "size": 2, + "data": "e30=" + }, + "layers": [ + { + "mediaType": "text/plain; charset=utf-8", + "digest": "sha256:f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2", + "size": 5, + "annotations": { + "org.opencontainers.image.title": "foobar.txt" + } + } + ] + }, + "Name": "quay.io/myartifact/mytxt:latest", + "Digest": "sha256:6c28fa07a5b0a1cee29862c1f6ea38a66df982495b14da2c052427eb628ed8c6" +} +``` + +Inspect artifact digest for the specified artifact: + +```shell +$ podman artifact inspect quay.io/myartifact/mytxt:latest --format {{.Digest}} +sha256:6c28fa07a5b0a1cee29862c1f6ea38a66df982495b14da2c052427eb628ed8c6 ``` ## SEE ALSO diff --git a/test/e2e/artifact_test.go b/test/e2e/artifact_test.go index fd640aca11..37683be3a2 100644 --- a/test/e2e/artifact_test.go +++ b/test/e2e/artifact_test.go @@ -668,6 +668,20 @@ var _ = Describe("Podman artifact", func() { // Verify we have 2 layers Expect(a.Manifest.Layers).To(HaveLen(2)) }) + + It("podman artifact inspect with --format", func() { + artifact1File, err := createArtifactFile(4192) + Expect(err).ToNot(HaveOccurred()) + artifact1Name := "localhost/test/artifact1" + addArtifact1 := podmanTest.PodmanExitCleanly("artifact", "add", artifact1Name, artifact1File) + + artifactDigest := addArtifact1.OutputToString() + + session := podmanTest.PodmanExitCleanly("artifact", "inspect", artifactDigest, "--format", "{{.Digest}}") + Expect(session.OutputToString()).To(Equal("sha256:" + artifactDigest)) + session = podmanTest.PodmanExitCleanly("artifact", "inspect", artifactDigest[:12], "-f", "{{.Name}}") + Expect(session.OutputToString()).To(Equal(artifact1Name)) + }) }) func digestToFilename(digest string) string { diff --git a/test/system/610-format.bats b/test/system/610-format.bats index a0d7e119d1..35a5cf7d64 100644 --- a/test/system/610-format.bats +++ b/test/system/610-format.bats @@ -12,6 +12,7 @@ function teardown() { run_podman '?' secret rm "s-$(safename)" run_podman '?' pod rm -f "p-$(safename)" run_podman '?' rm -f -t0 "c-$(safename)" + run_podman '?' artifact rm "a-$(safename)" basic_teardown } @@ -125,10 +126,12 @@ function check_subcommand() { ctrname="c-$(safename)" podname="p-$(safename)" secretname="s-$(safename)" + artifactname="a-$(safename)" # Setup: some commands need a container, pod, secret, ... run_podman run -d --name $ctrname $IMAGE top run_podman pod create $podname run_podman secret create $secretname /etc/hosts + run_podman artifact add $artifactname /etc/hosts # For 'search' and 'image search': if local cache registry is available, # use it. This bypasses quay, and thus prevents flakes. @@ -147,7 +150,7 @@ image inspect | $IMAGE container inspect | $ctrname inspect | $ctrname - +artifact inspect | $artifactname volume inspect | -a secret inspect | $secretname network inspect | podman @@ -200,6 +203,7 @@ stats | --no-stream run_podman rm -f -t0 $ctrname run_podman secret rm $secretname run_podman '?' machine rm -f $machinename + run_podman artifact rm $artifactname # Make sure there are no leftover commands in our table - this would # indicate a typo in the table, or a flaw in our logic such that