Skip to content

Commit

Permalink
podman artifact
Browse files Browse the repository at this point in the history
the podman artifact verb is used to manage OCI artifacts.  the following
verbs were added to `podman artifact`:

* add
* inspect
* ls
* pull
* push
* rm

Notable items with this PR:

* all artifact commands and their output are subject to change. i.e.
  consider all of this tech preview
* there is no way to add a file to an artifact that already exists in
  the store.  you would need to delete and recreate the artifact.
* all references to artifacts names should be fully qualified names in
  the form of repo/name:tag (i.e. quay.io/artifact/foobar:latest)
* i understand that we will likely want to be able to attribute things
  like arch, etc to artifact files.  this function is not available yet.

Many thanks to Paul Holzinger for autocompletion PRs and review PRs that
fixed issues early on.

Also fix up some Args function to specify the correct number of args.

Signed-off-by: Paul Holzinger <[email protected]>
Signed-off-by: Brent Baude <[email protected]>
  • Loading branch information
baude committed Jan 15, 2025
1 parent 04e6488 commit ef9b936
Show file tree
Hide file tree
Showing 29 changed files with 2,344 additions and 64 deletions.
42 changes: 42 additions & 0 deletions cmd/podman/artifact/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package artifact

import (
"fmt"

"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)

var (
addCmd = &cobra.Command{
Use: "add ARTIFACT PATH [...PATH]",
Short: "Add an OCI artifact to the local store",
Long: "Add an OCI artifact to the local store from the local filesystem",
RunE: add,
Args: cobra.MinimumNArgs(2),
ValidArgsFunction: common.AutocompleteArtifactAdd,
Example: `podman artifact add quay.io/myimage/myartifact:latest /tmp/foobar.txt`,
}
)

func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: addCmd,
Parent: artifactCmd,
})

// TODO When the inspect structure has been defined, we need to uncommand 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{}))
}

func add(cmd *cobra.Command, args []string) error {
report, err := registry.ImageEngine().ArtifactAdd(registry.Context(), args[0], args[1:], entities.ArtifactAddoptions{})
if err != nil {
return err
}
fmt.Println(report.ArtifactDigest.Encoded())
return nil
}
27 changes: 27 additions & 0 deletions cmd/podman/artifact/artifact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package artifact

import (
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/spf13/cobra"
)

var (
json = registry.JSONLibrary()
// Command: podman _artifact_
artifactCmd = &cobra.Command{
Use: "artifact",
Short: "Manage OCI artifacts",
Long: "Manage OCI artifacts",
//PersistentPreRunE: validate.NoOp,
RunE: validate.SubCommandExists,
}
)

func init() {
if !registry.IsRemote() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: artifactCmd,
})
}
}
75 changes: 75 additions & 0 deletions cmd/podman/artifact/inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package artifact

import (
"fmt"
"os"

"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)

var (
inspectCmd = &cobra.Command{
Use: "inspect [ARTIFACT...]",
Short: "Inspect an OCI artifact",
Long: "Provide details on an OCI artifact",
RunE: inspect,
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: common.AutocompleteArtifacts,
Example: `podman artifact inspect quay.io/myimage/myartifact:latest`,
}
inspectFlag = inspectFlagType{}
)

type inspectFlagType struct {
format string
remote bool
}

func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: inspectCmd,
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")

// This is something we wanted to do but did not seem important enough for initial PR
// remoteFlagName := "remote"
// flags.BoolVar(&inspectFlag.remote, remoteFlagName, false, "Inspect the image on a container image registry")

// 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{}))
}

func inspect(cmd *cobra.Command, args []string) error {
if inspectFlag.remote {
return fmt.Errorf("not implemented")
}

if inspectFlag.format != "" {
return fmt.Errorf("not implemented")
}

artifactOptions := entities.ArtifactInspectOptions{}
inspectData, err := registry.ImageEngine().ArtifactInspect(registry.GetContext(), args[0], artifactOptions)
if err != nil {
return err
}
return printJSON(inspectData)
}

func printJSON(data interface{}) error {
enc := json.NewEncoder(os.Stdout)
// by default, json marshallers will force utf=8 from
// a string.
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
return enc.Encode(data)
}
136 changes: 136 additions & 0 deletions cmd/podman/artifact/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package artifact

import (
"fmt"
"os"

"github.com/containers/common/pkg/completion"
"github.com/containers/common/pkg/report"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/docker/go-units"
"github.com/spf13/cobra"
)

var (
ListCmd = &cobra.Command{
Use: "ls [options]",
Aliases: []string{"list"},
Short: "List OCI artifacts",
Long: "List OCI artifacts in local store",
RunE: list,
Args: validate.NoArgs,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman artifact ls`,
}
listFlag = listFlagType{}
)

type listFlagType struct {
format string
}

type artifactListOutput struct {
Digest string
Repository string
Size string
Tag string
}

var (
defaultArtifactListOutputFormat = "{{range .}}{{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.Size}}\n{{end -}}"
)

func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: ListCmd,
Parent: artifactCmd,
})
flags := ListCmd.Flags()
formatFlagName := "format"
flags.StringVar(&listFlag.format, formatFlagName, defaultArtifactListOutputFormat, "Format volume output using JSON or a Go template")
_ = ListCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&artifactListOutput{}))
// 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{}))
}

func list(cmd *cobra.Command, _ []string) error {
reports, err := registry.ImageEngine().ArtifactList(registry.GetContext(), entities.ArtifactListOptions{})
if err != nil {
return err
}

return outputTemplate(cmd, reports)
}

func outputTemplate(cmd *cobra.Command, lrs []*entities.ArtifactListReport) error {
var err error
artifacts := make([]artifactListOutput, 0)
for _, lr := range lrs {
var (
tag string
)
artifactName, err := lr.Artifact.GetName()
if err != nil {
return err
}
repo, err := reference.Parse(artifactName)
if err != nil {
return err
}
named, ok := repo.(reference.Named)
if !ok {
return fmt.Errorf("%q is an invalid artifact name", artifactName)
}
if tagged, ok := named.(reference.Tagged); ok {
tag = tagged.Tag()
}

// Note: Right now we only support things that are single manifests
// We should certainly expand this support for things like arch, etc
// as we move on
artifactDigest, err := lr.Artifact.GetDigest()
if err != nil {
return err
}
// TODO when we default to shorter ids, i would foresee a switch
// like images that will show the full ids.
artifacts = append(artifacts, artifactListOutput{
Digest: artifactDigest.Encoded(),
Repository: named.Name(),
Size: units.HumanSize(float64(lr.Artifact.TotalSizeBytes())),
Tag: tag,
})
}

headers := report.Headers(artifactListOutput{}, map[string]string{
"REPOSITORY": "REPOSITORY",
"Tag": "TAG",
"Size": "SIZE",
"Digest": "DIGEST",
})

rpt := report.New(os.Stdout, cmd.Name())
defer rpt.Flush()

switch {
case cmd.Flag("format").Changed:
rpt, err = rpt.Parse(report.OriginUser, listFlag.format)
default:
rpt, err = rpt.Parse(report.OriginPodman, listFlag.format)
}
if err != nil {
return err
}

if rpt.RenderHeaders {
if err := rpt.Execute(headers); err != nil {
return fmt.Errorf("failed to write report column headers: %w", err)
}
}
return rpt.Execute(artifacts)
}
Loading

0 comments on commit ef9b936

Please sign in to comment.