Files
podman/cmd/podman/inspect/inspect.go
Akash Yadav 1fbf24b65b feat: add --format flag to artifact inspect
Many commands support the `--format` flag which accept a go template to
allow for formatting for certain values, but it is not
yet implemented for artifact inspect command.

Adding this feature will allow easy formatting in scripts as well as
running it on a terminal.

This feature is implemented for artifact inspect by taking reference
from images and network commands implementation.

Fixes: [#27112](https://github.com/containers/podman/issues/27112)

Signed-off-by: Akash Yadav <akashyadav256526@gmail.com>
2025-10-21 16:58:03 +05:30

300 lines
8.6 KiB
Go

package inspect
import (
"context"
"errors"
"fmt"
"os"
"regexp"
"strings"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
"go.podman.io/common/pkg/report"
)
// AddInspectFlagSet takes a command and adds the inspect flags and returns an
// InspectOptions object.
func AddInspectFlagSet(cmd *cobra.Command) *entities.InspectOptions {
opts := entities.InspectOptions{}
flags := cmd.Flags()
flags.BoolVarP(&opts.Size, "size", "s", false, "Display total file size")
formatFlagName := "format"
flags.StringVarP(&opts.Format, formatFlagName, "f", "json", "Format the output to a Go template or json")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil)) // passing nil as the type selection logic is in AutocompleteFormat function
typeFlagName := "type"
flags.StringVarP(&opts.Type, typeFlagName, "t", common.AllType, "Specify inspect-object type")
_ = cmd.RegisterFlagCompletionFunc(typeFlagName, common.AutocompleteInspectType)
validate.AddLatestFlag(cmd, &opts.Latest)
return &opts
}
// Inspect inspects the specified container/image/pod/volume names or IDs.
func Inspect(namesOrIDs []string, options entities.InspectOptions) error {
inspector, err := newInspector(options)
if err != nil {
return err
}
return inspector.inspect(namesOrIDs)
}
// inspector allows for inspecting images and containers.
type inspector struct {
containerEngine entities.ContainerEngine
imageEngine entities.ImageEngine
options entities.InspectOptions
}
// newInspector creates a new inspector based on the specified options.
func newInspector(options entities.InspectOptions) (*inspector, error) {
if options.Type == common.ImageType {
if options.Latest {
return nil, fmt.Errorf("latest is not supported for type %q", common.ImageType)
}
if options.Size {
return nil, fmt.Errorf("size is not supported for type %q", common.ImageType)
}
}
if options.Type == common.PodType && options.Size {
return nil, fmt.Errorf("size is not supported for type %q", common.PodType)
}
return &inspector{
containerEngine: registry.ContainerEngine(),
imageEngine: registry.ImageEngine(),
options: options,
}, nil
}
// inspect inspects the specified container/image names or IDs.
func (i *inspector) inspect(namesOrIDs []string) error {
// data - dumping place for inspection results.
var data []any
var errs []error
ctx := context.Background()
if len(namesOrIDs) == 0 {
if !i.options.Latest && !i.options.All {
return errors.New("no names or ids specified")
}
}
tmpType := i.options.Type
if i.options.Latest {
if len(namesOrIDs) > 0 {
return errors.New("--latest and arguments cannot be used together")
}
if i.options.Type == common.AllType {
tmpType = common.ContainerType // -l works with --type=all, defaults to containertype
}
}
// Inspect - note that AllType requires us to expensively query one-by-one.
switch tmpType {
case common.AllType:
allData, allErrs, err := i.inspectAll(ctx, namesOrIDs)
if err != nil {
return err
}
data = allData
errs = allErrs
case common.ImageType:
imgData, allErrs, err := i.imageEngine.Inspect(ctx, namesOrIDs, i.options)
if err != nil {
return err
}
errs = allErrs
for i := range imgData {
data = append(data, imgData[i])
}
case common.ContainerType:
ctrData, allErrs, err := i.containerEngine.ContainerInspect(ctx, namesOrIDs, i.options)
if err != nil {
return err
}
errs = allErrs
for i := range ctrData {
data = append(data, ctrData[i])
}
case common.PodType:
podData, allErrs, err := i.containerEngine.PodInspect(ctx, namesOrIDs, i.options)
if err != nil {
return err
}
errs = allErrs
for i := range podData {
data = append(data, podData[i])
}
case common.NetworkType:
networkData, allErrs, err := registry.ContainerEngine().NetworkInspect(ctx, namesOrIDs, i.options)
if err != nil {
return err
}
errs = allErrs
for i := range networkData {
data = append(data, networkData[i])
}
case common.VolumeType:
volumeData, allErrs, err := i.containerEngine.VolumeInspect(ctx, namesOrIDs, i.options)
if err != nil {
return err
}
errs = allErrs
for i := range volumeData {
data = append(data, volumeData[i])
}
case common.ArtifactType:
for _, name := range namesOrIDs {
artifactData, err := i.imageEngine.ArtifactInspect(ctx, name, entities.ArtifactInspectOptions{})
if err != nil {
errs = append(errs, err)
continue
}
data = append(data, artifactData)
}
default:
return fmt.Errorf("invalid type %q: must be %q, %q, %q, %q, %q, %q, or %q", i.options.Type,
common.ImageType, common.ContainerType, common.PodType, common.NetworkType, common.VolumeType, common.ArtifactType, common.AllType)
}
// Always print an empty array
if data == nil {
data = []any{}
}
var err error
switch {
case report.IsJSON(i.options.Format) || i.options.Format == "":
err = utils.PrintGenericJSON(data)
default:
// Landing here implies user has given a custom --format
var rpt *report.Formatter
format := InspectNormalize(i.options.Format, i.options.Type)
rpt, err = report.New(os.Stdout, "inspect").Parse(report.OriginUser, format)
if err != nil {
return err
}
defer rpt.Flush()
err = rpt.Execute(data)
}
if err != nil {
errs = append(errs, err)
}
if len(errs) > 0 {
if len(errs) > 1 {
for _, err := range errs[1:] {
fmt.Fprintf(os.Stderr, "%v\n", err)
}
}
return errs[0]
}
return nil
}
func (i *inspector) inspectAll(ctx context.Context, namesOrIDs []string) ([]any, []error, error) {
var data []any
allErrs := []error{}
for _, name := range namesOrIDs {
ctrData, errs, err := i.containerEngine.ContainerInspect(ctx, []string{name}, i.options)
if err != nil {
return nil, nil, err
}
if len(errs) == 0 {
data = append(data, ctrData[0])
continue
}
imgData, errs, err := i.imageEngine.Inspect(ctx, []string{name}, i.options)
if err != nil {
return nil, nil, err
}
if len(errs) == 0 {
data = append(data, imgData[0])
continue
}
volumeData, errs, err := i.containerEngine.VolumeInspect(ctx, []string{name}, i.options)
if err != nil {
return nil, nil, err
}
if len(errs) == 0 {
data = append(data, volumeData[0])
continue
}
networkData, errs, err := registry.ContainerEngine().NetworkInspect(ctx, namesOrIDs, i.options)
if err != nil {
return nil, nil, err
}
if len(errs) == 0 {
data = append(data, networkData[0])
continue
}
podData, errs, err := i.containerEngine.PodInspect(ctx, []string{name}, i.options)
if err != nil {
return nil, nil, err
}
if len(errs) == 0 {
data = append(data, podData[0])
continue
}
artifactData, err := i.imageEngine.ArtifactInspect(ctx, name, entities.ArtifactInspectOptions{})
if err == nil {
data = append(data, artifactData)
continue
}
if len(errs) > 0 {
allErrs = append(allErrs, fmt.Errorf("no such object: %q", name))
continue
}
}
return data, allErrs, nil
}
// 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}}")
r := strings.NewReplacer(
".Src", ".Source",
".Dst", ".Destination",
".ImageID", ".Image",
)
// If inspect type is `image` we need to replace
// certain additional fields like `.Config.HealthCheck`
// but don't want to replace them for other inspect types.
if inspectType == common.ImageType {
r = strings.NewReplacer(
".Src", ".Source",
".Dst", ".Destination",
".ImageID", ".Image",
".Config.Healthcheck", ".HealthCheck",
)
}
return r.Replace(row)
}