From ef9b936cfb79c44e63d2b1a556ad88dd27e32b59 Mon Sep 17 00:00:00 2001 From: Brent Baude Date: Thu, 22 Aug 2024 12:13:01 -0500 Subject: [PATCH] podman artifact 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 Signed-off-by: Brent Baude --- cmd/podman/artifact/add.go | 42 ++ cmd/podman/artifact/artifact.go | 27 ++ cmd/podman/artifact/inspect.go | 75 ++++ cmd/podman/artifact/list.go | 136 +++++++ cmd/podman/artifact/pull.go | 193 +++++++++ cmd/podman/artifact/push.go | 233 +++++++++++ cmd/podman/artifact/rm.go | 53 +++ cmd/podman/common/completion.go | 41 ++ cmd/podman/main.go | 1 + docs/source/Commands.rst | 2 + docs/source/markdown/msg/devcode.md | 5 + docs/source/markdown/podman-artifact-add.1.md | 48 +++ .../markdown/podman-artifact-inspect.1.md | 38 ++ docs/source/markdown/podman-artifact-ls.1.md | 57 +++ .../source/markdown/podman-artifact-pull.1.md | 134 ++++++ .../source/markdown/podman-artifact-push.1.md | 145 +++++++ docs/source/markdown/podman-artifact-rm.1.md | 46 +++ docs/source/markdown/podman-artifact.1.md | 36 ++ docs/source/markdown/podman.1.md | 127 +++--- go.mod | 2 +- pkg/domain/entities/artifact.go | 72 ++++ pkg/domain/entities/engine_image.go | 6 + pkg/domain/infra/abi/artifact.go | 167 ++++++++ pkg/domain/infra/tunnel/artifact.go | 38 ++ pkg/libartifact/artifact.go | 89 ++++ pkg/libartifact/store/store.go | 381 ++++++++++++++++++ pkg/libartifact/types/config.go | 8 + test/e2e/artifact_test.go | 157 ++++++++ test/e2e/common_test.go | 49 +++ 29 files changed, 2344 insertions(+), 64 deletions(-) create mode 100644 cmd/podman/artifact/add.go create mode 100644 cmd/podman/artifact/artifact.go create mode 100644 cmd/podman/artifact/inspect.go create mode 100644 cmd/podman/artifact/list.go create mode 100644 cmd/podman/artifact/pull.go create mode 100644 cmd/podman/artifact/push.go create mode 100644 cmd/podman/artifact/rm.go create mode 100644 docs/source/markdown/msg/devcode.md create mode 100644 docs/source/markdown/podman-artifact-add.1.md create mode 100644 docs/source/markdown/podman-artifact-inspect.1.md create mode 100644 docs/source/markdown/podman-artifact-ls.1.md create mode 100644 docs/source/markdown/podman-artifact-pull.1.md create mode 100644 docs/source/markdown/podman-artifact-push.1.md create mode 100644 docs/source/markdown/podman-artifact-rm.1.md create mode 100644 docs/source/markdown/podman-artifact.1.md create mode 100644 pkg/domain/entities/artifact.go create mode 100644 pkg/domain/infra/abi/artifact.go create mode 100644 pkg/domain/infra/tunnel/artifact.go create mode 100644 pkg/libartifact/artifact.go create mode 100644 pkg/libartifact/store/store.go create mode 100644 pkg/libartifact/types/config.go create mode 100644 test/e2e/artifact_test.go diff --git a/cmd/podman/artifact/add.go b/cmd/podman/artifact/add.go new file mode 100644 index 0000000000..fa0d9d786c --- /dev/null +++ b/cmd/podman/artifact/add.go @@ -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 +} diff --git a/cmd/podman/artifact/artifact.go b/cmd/podman/artifact/artifact.go new file mode 100644 index 0000000000..cc4a7b53a7 --- /dev/null +++ b/cmd/podman/artifact/artifact.go @@ -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, + }) + } +} diff --git a/cmd/podman/artifact/inspect.go b/cmd/podman/artifact/inspect.go new file mode 100644 index 0000000000..3054abbe67 --- /dev/null +++ b/cmd/podman/artifact/inspect.go @@ -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) +} diff --git a/cmd/podman/artifact/list.go b/cmd/podman/artifact/list.go new file mode 100644 index 0000000000..8a2d77379b --- /dev/null +++ b/cmd/podman/artifact/list.go @@ -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) +} diff --git a/cmd/podman/artifact/pull.go b/cmd/podman/artifact/pull.go new file mode 100644 index 0000000000..e94d381b19 --- /dev/null +++ b/cmd/podman/artifact/pull.go @@ -0,0 +1,193 @@ +package artifact + +import ( + "fmt" + "os" + + "github.com/containers/buildah/pkg/cli" + "github.com/containers/common/pkg/auth" + "github.com/containers/common/pkg/completion" + "github.com/containers/image/v5/types" + "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/containers/podman/v5/pkg/util" + "github.com/spf13/cobra" +) + +// pullOptionsWrapper wraps entities.ImagePullOptions and prevents leaking +// CLI-only fields into the API types. +type pullOptionsWrapper struct { + entities.ArtifactPullOptions + TLSVerifyCLI bool // CLI only + CredentialsCLI string + DecryptionKeys []string +} + +var ( + pullOptions = pullOptionsWrapper{} + pullDescription = `Pulls an artifact from a registry and stores it locally.` + + pullCmd = &cobra.Command{ + Use: "pull [options] ARTIFACT", + Short: "Pull an OCI artifact", + Long: pullDescription, + RunE: artifactPull, + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.AutocompleteArtifacts, + Example: `podman artifact pull quay.io/myimage/myartifact:latest`, + // TODO Autocomplete function needs to be done + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: pullCmd, + Parent: artifactCmd, + }) + pullFlags(pullCmd) + + // 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{})) +} + +// pullFlags set the flags for the pull command. +func pullFlags(cmd *cobra.Command) { + flags := cmd.Flags() + + credsFlagName := "creds" + flags.StringVar(&pullOptions.CredentialsCLI, credsFlagName, "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry") + _ = cmd.RegisterFlagCompletionFunc(credsFlagName, completion.AutocompleteNone) + + // TODO I think these can be removed + /* + archFlagName := "arch" + flags.StringVar(&pullOptions.Arch, archFlagName, "", "Use `ARCH` instead of the architecture of the machine for choosing images") + _ = cmd.RegisterFlagCompletionFunc(archFlagName, completion.AutocompleteArch) + + + osFlagName := "os" + flags.StringVar(&pullOptions.OS, osFlagName, "", "Use `OS` instead of the running OS for choosing images") + _ = cmd.RegisterFlagCompletionFunc(osFlagName, completion.AutocompleteOS) + + variantFlagName := "variant" + flags.StringVar(&pullOptions.Variant, variantFlagName, "", "Use VARIANT instead of the running architecture variant for choosing images") + _ = cmd.RegisterFlagCompletionFunc(variantFlagName, completion.AutocompleteNone) + + platformFlagName := "platform" + flags.String(platformFlagName, "", "Specify the platform for selecting the image. (Conflicts with arch and os)") + _ = cmd.RegisterFlagCompletionFunc(platformFlagName, completion.AutocompleteNone) + + + flags.Bool("disable-content-trust", false, "This is a Docker specific option and is a NOOP") + */ + + flags.BoolVarP(&pullOptions.Quiet, "quiet", "q", false, "Suppress output information when pulling images") + flags.BoolVar(&pullOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries") + + authfileFlagName := "authfile" + flags.StringVar(&pullOptions.AuthFilePath, authfileFlagName, auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") + _ = cmd.RegisterFlagCompletionFunc(authfileFlagName, completion.AutocompleteDefault) + + decryptionKeysFlagName := "decryption-key" + flags.StringArrayVar(&pullOptions.DecryptionKeys, decryptionKeysFlagName, nil, "Key needed to decrypt the image (e.g. /path/to/key.pem)") + _ = cmd.RegisterFlagCompletionFunc(decryptionKeysFlagName, completion.AutocompleteDefault) + + retryFlagName := "retry" + flags.Uint(retryFlagName, registry.RetryDefault(), "number of times to retry in case of failure when performing pull") + _ = cmd.RegisterFlagCompletionFunc(retryFlagName, completion.AutocompleteNone) + retryDelayFlagName := "retry-delay" + flags.String(retryDelayFlagName, registry.RetryDelayDefault(), "delay between retries in case of pull failures") + _ = cmd.RegisterFlagCompletionFunc(retryDelayFlagName, completion.AutocompleteNone) + + if registry.IsRemote() { + _ = flags.MarkHidden(decryptionKeysFlagName) + } else { + certDirFlagName := "cert-dir" + flags.StringVar(&pullOptions.CertDirPath, certDirFlagName, "", "`Pathname` of a directory containing TLS certificates and keys") + _ = cmd.RegisterFlagCompletionFunc(certDirFlagName, completion.AutocompleteDefault) + + signaturePolicyFlagName := "signature-policy" + flags.StringVar(&pullOptions.SignaturePolicyPath, signaturePolicyFlagName, "", "`Pathname` of signature policy file (not usually used)") + _ = flags.MarkHidden(signaturePolicyFlagName) + } +} + +func artifactPull(cmd *cobra.Command, args []string) error { + // 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") { + pullOptions.InsecureSkipTLSVerify = types.NewOptionalBool(!pullOptions.TLSVerifyCLI) + } + + if cmd.Flags().Changed("retry") { + retry, err := cmd.Flags().GetUint("retry") + if err != nil { + return err + } + + pullOptions.MaxRetries = &retry + } + + if cmd.Flags().Changed("retry-delay") { + val, err := cmd.Flags().GetString("retry-delay") + if err != nil { + return err + } + + pullOptions.RetryDelay = val + } + + if cmd.Flags().Changed("authfile") { + if err := auth.CheckAuthFile(pullOptions.AuthFilePath); err != nil { + return err + } + } + + // TODO Once we have a decision about the flag removal above, this should be safe to delete + /* + platform, err := cmd.Flags().GetString("platform") + if err != nil { + return err + } + if platform != "" { + if pullOptions.Arch != "" || pullOptions.OS != "" { + return errors.New("--platform option can not be specified with --arch or --os") + } + + specs := strings.Split(platform, "/") + pullOptions.OS = specs[0] // may be empty + if len(specs) > 1 { + pullOptions.Arch = specs[1] + if len(specs) > 2 { + pullOptions.Variant = specs[2] + } + } + } + */ + + if pullOptions.CredentialsCLI != "" { + creds, err := util.ParseRegistryCreds(pullOptions.CredentialsCLI) + if err != nil { + return err + } + pullOptions.Username = creds.Username + pullOptions.Password = creds.Password + } + + decConfig, err := cli.DecryptConfig(pullOptions.DecryptionKeys) + if err != nil { + return fmt.Errorf("unable to obtain decryption config: %w", err) + } + pullOptions.OciDecryptConfig = decConfig + + if !pullOptions.Quiet { + pullOptions.Writer = os.Stdout + } + + _, err = registry.ImageEngine().ArtifactPull(registry.GetContext(), args[0], pullOptions.ArtifactPullOptions) + return err +} diff --git a/cmd/podman/artifact/push.go b/cmd/podman/artifact/push.go new file mode 100644 index 0000000000..cc7d2e43eb --- /dev/null +++ b/cmd/podman/artifact/push.go @@ -0,0 +1,233 @@ +package artifact + +import ( + "fmt" + "os" + + "github.com/containers/buildah/pkg/cli" + "github.com/containers/common/pkg/auth" + "github.com/containers/common/pkg/completion" + "github.com/containers/image/v5/types" + "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/containers/podman/v5/pkg/util" + "github.com/spf13/cobra" +) + +// pushOptionsWrapper wraps entities.ImagepushOptions and prevents leaking +// CLI-only fields into the API types. +type pushOptionsWrapper struct { + entities.ArtifactPushOptions + TLSVerifyCLI bool // CLI only + CredentialsCLI string + SignPassphraseFileCLI string + SignBySigstoreParamFileCLI string + EncryptionKeys []string + EncryptLayers []int + DigestFile string +} + +var ( + pushOptions = pushOptionsWrapper{} + pushDescription = `Push an OCI artifact from local storage to an image registry` + + pushCmd = &cobra.Command{ + Use: "push [options] ARTIFACT.", + Short: "Push an OCI artifact", + Long: pushDescription, + RunE: artifactPush, + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.AutocompleteArtifacts, + Example: `podman artifact push quay.io/myimage/myartifact:latest`, + // TODO Autocomplete function needs to be done + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: pushCmd, + Parent: artifactCmd, + }) + pushFlags(pushCmd) + + // 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(&artifact.InspectInfo{})) +} + +// pullFlags set the flags for the pull command. +func pushFlags(cmd *cobra.Command) { + flags := cmd.Flags() + + // For now default All flag to true, for pushing of manifest lists + pushOptions.All = true + authfileFlagName := "authfile" + flags.StringVar(&pushOptions.Authfile, authfileFlagName, auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") + _ = cmd.RegisterFlagCompletionFunc(authfileFlagName, completion.AutocompleteDefault) + + certDirFlagName := "cert-dir" + flags.StringVar(&pushOptions.CertDir, certDirFlagName, "", "Path to a directory containing TLS certificates and keys") + _ = cmd.RegisterFlagCompletionFunc(certDirFlagName, completion.AutocompleteDefault) + + // This is a flag I didn't wire up but could be considered + // flags.BoolVar(&pushOptions.Compress, "compress", false, "Compress tarball image layers when pushing to a directory using the 'dir' transport. (default is same compression type as source)") + + credsFlagName := "creds" + flags.StringVar(&pushOptions.CredentialsCLI, credsFlagName, "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry") + _ = cmd.RegisterFlagCompletionFunc(credsFlagName, completion.AutocompleteNone) + + digestfileFlagName := "digestfile" + flags.StringVar(&pushOptions.DigestFile, digestfileFlagName, "", "Write the digest of the pushed image to the specified file") + _ = cmd.RegisterFlagCompletionFunc(digestfileFlagName, completion.AutocompleteDefault) + + flags.BoolVarP(&pushOptions.Quiet, "quiet", "q", false, "Suppress output information when pushing images") + + retryFlagName := "retry" + flags.Uint(retryFlagName, registry.RetryDefault(), "number of times to retry in case of failure when performing push") + _ = cmd.RegisterFlagCompletionFunc(retryFlagName, completion.AutocompleteNone) + + retryDelayFlagName := "retry-delay" + flags.String(retryDelayFlagName, registry.RetryDelayDefault(), "delay between retries in case of push failures") + _ = cmd.RegisterFlagCompletionFunc(retryDelayFlagName, completion.AutocompleteNone) + + signByFlagName := "sign-by" + flags.StringVar(&pushOptions.SignBy, signByFlagName, "", "Add a signature at the destination using the specified key") + _ = cmd.RegisterFlagCompletionFunc(signByFlagName, completion.AutocompleteNone) + + signBySigstoreFlagName := "sign-by-sigstore" + flags.StringVar(&pushOptions.SignBySigstoreParamFileCLI, signBySigstoreFlagName, "", "Sign the image using a sigstore parameter file at `PATH`") + _ = cmd.RegisterFlagCompletionFunc(signBySigstoreFlagName, completion.AutocompleteDefault) + + signBySigstorePrivateKeyFlagName := "sign-by-sigstore-private-key" + flags.StringVar(&pushOptions.SignBySigstorePrivateKeyFile, signBySigstorePrivateKeyFlagName, "", "Sign the image using a sigstore private key at `PATH`") + _ = cmd.RegisterFlagCompletionFunc(signBySigstorePrivateKeyFlagName, completion.AutocompleteDefault) + + signPassphraseFileFlagName := "sign-passphrase-file" + flags.StringVar(&pushOptions.SignPassphraseFileCLI, signPassphraseFileFlagName, "", "Read a passphrase for signing an image from `PATH`") + _ = cmd.RegisterFlagCompletionFunc(signPassphraseFileFlagName, completion.AutocompleteDefault) + + flags.BoolVar(&pushOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries") + + // TODO I think these two can be removed? + /* + compFormat := "compression-format" + flags.StringVar(&pushOptions.CompressionFormat, compFormat, compressionFormat(), "compression format to use") + _ = cmd.RegisterFlagCompletionFunc(compFormat, common.AutocompleteCompressionFormat) + + compLevel := "compression-level" + flags.Int(compLevel, compressionLevel(), "compression level to use") + _ = cmd.RegisterFlagCompletionFunc(compLevel, completion.AutocompleteNone) + + */ + + // Potential options that could be wired up if deemed necessary + // encryptionKeysFlagName := "encryption-key" + // flags.StringArrayVar(&pushOptions.EncryptionKeys, encryptionKeysFlagName, nil, "Key with the encryption protocol to use to encrypt the image (e.g. jwe:/path/to/key.pem)") + // _ = cmd.RegisterFlagCompletionFunc(encryptionKeysFlagName, completion.AutocompleteDefault) + + // encryptLayersFlagName := "encrypt-layer" + // flags.IntSliceVar(&pushOptions.EncryptLayers, encryptLayersFlagName, nil, "Layers to encrypt, 0-indexed layer indices with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer). If not defined, will encrypt all layers if encryption-key flag is specified") + // _ = cmd.RegisterFlagCompletionFunc(encryptLayersFlagName, completion.AutocompleteDefault) + + if registry.IsRemote() { + _ = flags.MarkHidden("cert-dir") + _ = flags.MarkHidden("compress") + _ = flags.MarkHidden("quiet") + _ = flags.MarkHidden(signByFlagName) + _ = flags.MarkHidden(signBySigstoreFlagName) + _ = flags.MarkHidden(signBySigstorePrivateKeyFlagName) + _ = flags.MarkHidden(signPassphraseFileFlagName) + } else { + signaturePolicyFlagName := "signature-policy" + flags.StringVar(&pushOptions.SignaturePolicy, signaturePolicyFlagName, "", "Path to a signature-policy file") + _ = flags.MarkHidden(signaturePolicyFlagName) + } +} + +func artifactPush(cmd *cobra.Command, args []string) error { + source := args[0] + // Should we just make destination == origin ? + // destination := args[len(args)-1] + + // 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") { + pushOptions.SkipTLSVerify = types.NewOptionalBool(!pushOptions.TLSVerifyCLI) + } + + if cmd.Flags().Changed("authfile") { + if err := auth.CheckAuthFile(pushOptions.Authfile); err != nil { + return err + } + } + + if pushOptions.CredentialsCLI != "" { + creds, err := util.ParseRegistryCreds(pushOptions.CredentialsCLI) + if err != nil { + return err + } + pushOptions.Username = creds.Username + pushOptions.Password = creds.Password + } + + if !pushOptions.Quiet { + pushOptions.Writer = os.Stderr + } + + signingCleanup, err := common.PrepareSigning(&pushOptions.ImagePushOptions, + pushOptions.SignPassphraseFileCLI, pushOptions.SignBySigstoreParamFileCLI) + if err != nil { + return err + } + defer signingCleanup() + + encConfig, encLayers, err := cli.EncryptConfig(pushOptions.EncryptionKeys, pushOptions.EncryptLayers) + if err != nil { + return fmt.Errorf("unable to obtain encryption config: %w", err) + } + pushOptions.OciEncryptConfig = encConfig + pushOptions.OciEncryptLayers = encLayers + + if cmd.Flags().Changed("retry") { + retry, err := cmd.Flags().GetUint("retry") + if err != nil { + return err + } + + pushOptions.Retry = &retry + } + + if cmd.Flags().Changed("retry-delay") { + val, err := cmd.Flags().GetString("retry-delay") + if err != nil { + return err + } + + pushOptions.RetryDelay = val + } + + // TODO If not compression options are supported, we do not need the following + /* + if cmd.Flags().Changed("compression-level") { + val, err := cmd.Flags().GetInt("compression-level") + if err != nil { + return err + } + pushOptions.CompressionLevel = &val + } + + if cmd.Flags().Changed("compression-format") { + if !cmd.Flags().Changed("force-compression") { + // If `compression-format` is set and no value for `--force-compression` + // is selected then defaults to `true`. + pushOptions.ForceCompressionFormat = true + } + } + */ + + _, err = registry.ImageEngine().ArtifactPush(registry.GetContext(), source, pushOptions.ArtifactPushOptions) + return err +} diff --git a/cmd/podman/artifact/rm.go b/cmd/podman/artifact/rm.go new file mode 100644 index 0000000000..c78a1c24b6 --- /dev/null +++ b/cmd/podman/artifact/rm.go @@ -0,0 +1,53 @@ +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 ( + rmCmd = &cobra.Command{ + Use: "rm ARTIFACT", + Short: "Remove an OCI artifact", + Long: "Remove an OCI from local storage", + RunE: rm, + Aliases: []string{"remove"}, + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.AutocompleteArtifacts, + Example: `podman artifact rm quay.io/myimage/myartifact:latest`, + } + // The lint avoid here is because someday soon we will need flags for + // this command + rmFlag = rmFlagType{} //nolint:unused +) + +// TODO at some point force will be a required option; but this cannot be +// until we have artifacts being consumed by other parts of libpod like +// volumes +type rmFlagType struct { //nolint:unused + force bool +} + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: rmCmd, + 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 rm(cmd *cobra.Command, args []string) error { + artifactRemoveReport, err := registry.ImageEngine().ArtifactRm(registry.GetContext(), args[0], entities.ArtifactRemoveOptions{}) + if err != nil { + return err + } + fmt.Println(artifactRemoveReport.ArtfactDigest.Encoded()) + return nil +} diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index e46675733c..3476f33be8 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -317,6 +317,29 @@ func getNetworks(cmd *cobra.Command, toComplete string, cType completeType) ([]s return suggestions, cobra.ShellCompDirectiveNoFileComp } +func getArtifacts(cmd *cobra.Command, toComplete string) ([]string, cobra.ShellCompDirective) { + suggestions := []string{} + listOptions := entities.ArtifactListOptions{} + + engine, err := setupImageEngine(cmd) + if err != nil { + cobra.CompErrorln(err.Error()) + return nil, cobra.ShellCompDirectiveNoFileComp + } + artifacts, err := engine.ArtifactList(registry.GetContext(), listOptions) + if err != nil { + cobra.CompErrorln(err.Error()) + return nil, cobra.ShellCompDirectiveNoFileComp + } + + for _, artifact := range artifacts { + if strings.HasPrefix(artifact.Name, toComplete) { + suggestions = append(suggestions, artifact.Name) + } + } + return suggestions, cobra.ShellCompDirectiveNoFileComp +} + func fdIsNotDir(f *os.File) bool { stat, err := f.Stat() if err != nil { @@ -493,6 +516,24 @@ func getBoolCompletion(_ string) ([]string, cobra.ShellCompDirective) { /* Autocomplete Functions for cobra ValidArgsFunction */ +func AutocompleteArtifacts(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return getArtifacts(cmd, toComplete) +} + +func AutocompleteArtifactAdd(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + if len(args) == 0 { + // first argument accepts the name reference + return getArtifacts(cmd, toComplete) + } + return nil, cobra.ShellCompDirectiveDefault +} + // AutocompleteContainers - Autocomplete all container names. func AutocompleteContainers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if !validCurrentCmdLine(cmd, args, toComplete) { diff --git a/cmd/podman/main.go b/cmd/podman/main.go index dd5b984d8d..309a421529 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + _ "github.com/containers/podman/v5/cmd/podman/artifact" _ "github.com/containers/podman/v5/cmd/podman/completion" _ "github.com/containers/podman/v5/cmd/podman/farm" _ "github.com/containers/podman/v5/cmd/podman/generate" diff --git a/docs/source/Commands.rst b/docs/source/Commands.rst index 6a81adfa89..29a41b8e33 100644 --- a/docs/source/Commands.rst +++ b/docs/source/Commands.rst @@ -5,6 +5,8 @@ Commands :doc:`Podman ` (Pod Manager) Global Options, Environment Variables, Exit Codes, Configuration Files, and more +:doc:`artifact ` Manage OCI artifacts + :doc:`attach ` Attach to a running container :doc:`auto-update ` Auto update containers according to their auto-update policy diff --git a/docs/source/markdown/msg/devcode.md b/docs/source/markdown/msg/devcode.md new file mode 100644 index 0000000000..ad266f1e06 --- /dev/null +++ b/docs/source/markdown/msg/devcode.md @@ -0,0 +1,5 @@ +####> This can be used to cite that a command is tech-preview, in development, or highly +####> experimental +## WARNING: Experimental command +*This command is considered experimental and still in development. Inputs, options, and outputs are all +subject to change.* diff --git a/docs/source/markdown/podman-artifact-add.1.md b/docs/source/markdown/podman-artifact-add.1.md new file mode 100644 index 0000000000..6ad757a3e4 --- /dev/null +++ b/docs/source/markdown/podman-artifact-add.1.md @@ -0,0 +1,48 @@ +% podman-artifact-add 1 + +## WARNING: Experimental command +*This command is considered experimental and still in development. Inputs, options, and outputs are all +subject to change.* + +## NAME +podman\-artifact\-add - Add an OCI artifact to the local store + +## SYNOPSIS +**podman artifact add** *name* *file* [*file*]... + +## DESCRIPTION + +Add an OCI artifact to the local store from the local filesystem. You must +provide at least one file to create the artifact, but several can also be +added. + + +## OPTIONS + +#### **--help** + +Print usage statement. + + +## EXAMPLES + +Add a single file to an artifact + +``` +$ podman artifact add quay.io/myartifact/myml:latest /tmp/foobar.ml +0fe1488ecdef8cc4093e11a55bc048d9fc3e13a4ba846efd24b5a715006c95b3 +``` + +Add multiple files to an artifact +``` +$ podman artifact add quay.io/myartifact/myml:latest /tmp/foobar1.ml /tmp/foobar2.ml +1487acae11b5a30948c50762882036b41ac91a7b9514be8012d98015c95ddb78 +``` + + + +## SEE ALSO +**[podman(1)](podman.1.md)**, **[podman-artifact(1)](podman-artifact.1.md)** + +## HISTORY +Jan 2025, Originally compiled by Brent Baude diff --git a/docs/source/markdown/podman-artifact-inspect.1.md b/docs/source/markdown/podman-artifact-inspect.1.md new file mode 100644 index 0000000000..7d4b21920f --- /dev/null +++ b/docs/source/markdown/podman-artifact-inspect.1.md @@ -0,0 +1,38 @@ +% podman-artifact-inspect 1 + + +## WARNING: Experimental command +*This command is considered experimental and still in development. Inputs, options, and outputs are all +subject to change.* + +## NAME +podman\-artifact\-inspect - Inspect an OCI artifact + +## SYNOPSIS +**podman artifact inspect** [*name*] ... + +## DESCRIPTION + +Inspect an artifact in the local store. The artifact can be referred to with either: + +1. Fully qualified artifact name +2. Full or partial digest of the artifact's manifest + +## OPTIONS + +#### **--help** + +Print usage statement. + +## EXAMPLES + +Inspect an OCI image in the local store. +``` +$ podman artifact inspect quay.io/myartifact/myml:latest +``` + +## SEE ALSO +**[podman(1)](podman.1.md)**, **[podman-artifact(1)](podman-artifact.1.md)** + +## HISTORY +Sept 2024, Originally compiled by Brent Baude diff --git a/docs/source/markdown/podman-artifact-ls.1.md b/docs/source/markdown/podman-artifact-ls.1.md new file mode 100644 index 0000000000..720c1dec36 --- /dev/null +++ b/docs/source/markdown/podman-artifact-ls.1.md @@ -0,0 +1,57 @@ +% podman-artifact-ls 1 + + +## WARNING: Experimental command +*This command is considered experimental and still in development. Inputs, options, and outputs are all +subject to change.* + +## NAME +podman\-artifact\-ls - List OCI artifacts in local store + +## SYNOPSIS +**podman artifact ls** [*options*] + +## DESCRIPTION + +List all local artifacts in your local store. + +## OPTIONS + +#### **--format** + +Print results with a Go template. + +| **Placeholder** | **Description** | +|-----------------|------------------------------------------------| +| .Digest | The computed digest of the artifact's manifest | +| .Repository | Repository name of the artifact | +| .Size | Size artifact in human readable units | +| .Tag | Tag of the artifact name | + + + +## EXAMPLES + +List artifacts in the local store +``` +$ podman artifact ls +REPOSITORY TAG DIGEST SIZE +quay.io/artifact/foobar1 latest ab609fad386df1433f461b0643d9cf575560baf633809dcc9c190da6cc3a3c29 2.097GB +quay.io/artifact/foobar2 special cd734b558ceb8ccc0281ca76530e1dea1eb479407d3163f75fb601bffb6f73d0 12.58MB + + +``` +List artifact digests and size using a --format +``` +$ podman artifact ls --format "{{.Digest}} {{.Size}}" +ab609fad386df1433f461b0643d9cf575560baf633809dcc9c190da6cc3a3c29 2.097GB +cd734b558ceb8ccc0281ca76530e1dea1eb479407d3163f75fb601bffb6f73d0 12.58MB +``` + + + +## SEE ALSO +**[podman(1)](podman.1.md)**, **[podman-artifact(1)](podman-artifact.1.md)** + +## HISTORY +Jan 2025, Originally compiled by Brent Baude diff --git a/docs/source/markdown/podman-artifact-pull.1.md b/docs/source/markdown/podman-artifact-pull.1.md new file mode 100644 index 0000000000..a7c1b2a80b --- /dev/null +++ b/docs/source/markdown/podman-artifact-pull.1.md @@ -0,0 +1,134 @@ +% podman-artifact-pull 1 + + +## WARNING: Experimental command +*This command is considered experimental and still in development. Inputs, options, and outputs are all +subject to change.* + +## NAME +podman\-artifact\-pull - Pulls an artifact from a registry and stores it locally + +## SYNOPSIS +**podman artifact pull** [*options*] *source* + + +## DESCRIPTION +podman artifact pull copies an artifact from a registry onto the local machine. + + +## SOURCE +SOURCE is the location from which the artifact image is obtained. + +``` +# Pull from a registry +$ podman pull quay.io/username/myimage:latest +``` + +## OPTIONS + +[//]: # (BEGIN included file options/authfile.md) +#### **--authfile**=*path* + +Path of the authentication file. Default is `${XDG_RUNTIME_DIR}/containers/auth.json` on Linux, and `$HOME/.config/containers/auth.json` on Windows/macOS. +The file is created by **[podman login](podman-login.1.md)**. If the authorization state is not found there, `$HOME/.docker/config.json` is checked, which is set using **docker login**. + +Note: There is also the option to override the default path of the authentication file by setting the `REGISTRY_AUTH_FILE` environment variable. This can be done with **export REGISTRY_AUTH_FILE=_path_**. + +[//]: # (END included file options/authfile.md) + + +[//]: # (BEGIN included file options/cert-dir.md) +#### **--cert-dir**=*path* + +Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry. (Default: /etc/containers/certs.d) +For details, see **[containers-certs.d(5)](https://github.com/containers/image/blob/main/docs/containers-certs.d.5.md)**. +(This option is not available with the remote Podman client, including Mac and Windows (excluding WSL2) machines) + +[//]: # (END included file options/cert-dir.md) + + +[//]: # (BEGIN included file options/creds.md) +#### **--creds**=*[username[:password]]* + +The [username[:password]] to use to authenticate with the registry, if required. +If one or both values are not supplied, a command line prompt appears and the +value can be entered. The password is entered without echo. + +Note that the specified credentials are only used to authenticate against +target registries. They are not used for mirrors or when the registry gets +rewritten (see `containers-registries.conf(5)`); to authenticate against those +consider using a `containers-auth.json(5)` file. + +[//]: # (END included file options/creds.md) + + +[//]: # (BEGIN included file options/decryption-key.md) +#### **--decryption-key**=*key[:passphrase]* + +The [key[:passphrase]] to be used for decryption of images. Key can point to keys and/or certificates. Decryption is tried with all keys. If the key is protected by a passphrase, it is required to be passed in the argument and omitted otherwise. + +[//]: # (END included file options/decryption-key.md) + +#### **--help**, **-h** + +Print the usage statement. + +#### **--quiet**, **-q** + +Suppress output information when pulling images + + +[//]: # (BEGIN included file options/retry.md) +#### **--retry**=*attempts* + +Number of times to retry pulling or pushing images between the registry and +local storage in case of failure. Default is **3**. + +[//]: # (END included file options/retry.md) + + +[//]: # (BEGIN included file options/retry-delay.md) +#### **--retry-delay**=*duration* + +Duration of delay between retry attempts when pulling or pushing images between +the registry and local storage in case of failure. The default is to start at two seconds and then exponentially back off. The delay is used when this value is set, and no exponential back off occurs. + +[//]: # (END included file options/retry-delay.md) + + +[//]: # (BEGIN included file options/tls-verify.md) +#### **--tls-verify** + +Require HTTPS and verify certificates when contacting registries (default: **true**). +If explicitly set to **true**, TLS verification is used. +If set to **false**, TLS verification is not used. +If not specified, TLS verification is used unless the target registry +is listed as an insecure registry in **[containers-registries.conf(5)](https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md)** + +[//]: # (END included file options/tls-verify.md) + + +## FILES + +## EXAMPLES +Pull an artifact from a registry + +``` +podman artifact pull quay.io/baude/artifact:josey +Getting image source signatures +Copying blob e741c35a27bb done | +Copying config 44136fa355 done | +Writing manifest to image destination + +``` + +## SEE ALSO +**[podman(1)](podman.1.md)**, **[podman-artifact(1)](podman-artifact.1.md)**, **[podman-login(1)](podman-login.1.md)**, **[containers-certs.d(5)](https://github.com/containers/image/blob/main/docs/containers-certs.d.5.md)** + +### Troubleshooting + +See [podman-troubleshooting(7)](https://github.com/containers/podman/blob/main/troubleshooting.md) +for solutions to common issues. + +## HISTORY +Jan 2025, Originally compiled by Brent Baude diff --git a/docs/source/markdown/podman-artifact-push.1.md b/docs/source/markdown/podman-artifact-push.1.md new file mode 100644 index 0000000000..73738c45a1 --- /dev/null +++ b/docs/source/markdown/podman-artifact-push.1.md @@ -0,0 +1,145 @@ +% podman-artifact-push 1 + + +## WARNING: Experimental command +*This command is considered experimental and still in development. Inputs, options, and outputs are all +subject to change.* + +## NAME +podman\-artifact\-push - Push an OCI artifact from local storage to an image registry + +## SYNOPSIS +**podman artifact push** [*options*] *image* + +## DESCRIPTION +Pushes an artifact from the local artifact store to an image registry. + +``` +# Push artifact to a container registry +$ podman push quay.io/artifact/foobar1:latest +``` + +## OPTIONS + + +[//]: # (BEGIN included file options/authfile.md) +#### **--authfile**=*path* + +Path of the authentication file. Default is `${XDG_RUNTIME_DIR}/containers/auth.json` on Linux, and `$HOME/.config/containers/auth.json` on Windows/macOS. +The file is created by **[podman login](podman-login.1.md)**. If the authorization state is not found there, `$HOME/.docker/config.json` is checked, which is set using **docker login**. + +Note: There is also the option to override the default path of the authentication file by setting the `REGISTRY_AUTH_FILE` environment variable. This can be done with **export REGISTRY_AUTH_FILE=_path_**. + +[//]: # (END included file options/authfile.md) + + +[//]: # (BEGIN included file options/cert-dir.md) +#### **--cert-dir**=*path* + +Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry. (Default: /etc/containers/certs.d) +For details, see **[containers-certs.d(5)](https://github.com/containers/image/blob/main/docs/containers-certs.d.5.md)**. +(This option is not available with the remote Podman client, including Mac and Windows (excluding WSL2) machines) + +[//]: # (END included file options/cert-dir.md) + + +[//]: # (BEGIN included file options/creds.md) +#### **--creds**=*[username[:password]]* + +The [username[:password]] to use to authenticate with the registry, if required. +If one or both values are not supplied, a command line prompt appears and the +value can be entered. The password is entered without echo. + +Note that the specified credentials are only used to authenticate against +target registries. They are not used for mirrors or when the registry gets +rewritten (see `containers-registries.conf(5)`); to authenticate against those +consider using a `containers-auth.json(5)` file. + +[//]: # (END included file options/creds.md) + + +[//]: # (BEGIN included file options/digestfile.md) +#### **--digestfile**=*Digestfile* + +After copying the image, write the digest of the resulting image to the file. + +[//]: # (END included file options/digestfile.md) + + + +#### **--quiet**, **-q** + +When writing the output image, suppress progress output + + +[//]: # (BEGIN included file options/retry.md) +#### **--retry**=*attempts* + +Number of times to retry pulling or pushing images between the registry and +local storage in case of failure. Default is **3**. + +[//]: # (END included file options/retry.md) + + +[//]: # (BEGIN included file options/retry-delay.md) +#### **--retry-delay**=*duration* + +Duration of delay between retry attempts when pulling or pushing images between +the registry and local storage in case of failure. The default is to start at two seconds and then exponentially back off. The delay is used when this value is set, and no exponential back off occurs. + +[//]: # (END included file options/retry-delay.md) + +#### **--sign-by**=*key* + +Add a “simple signing” signature at the destination using the specified key. (This option is not available with the remote Podman client, including Mac and Windows (excluding WSL2) machines) + + +[//]: # (BEGIN included file options/sign-by-sigstore.md) +#### **--sign-by-sigstore**=*param-file* + +Add a sigstore signature based on further options specified in a container's sigstore signing parameter file *param-file*. +See containers-sigstore-signing-params.yaml(5) for details about the file format. + +[//]: # (END included file options/sign-by-sigstore.md) + +#### **--sign-by-sigstore-private-key**=*path* + +Add a sigstore signature at the destination using a private key at the specified path. (This option is not available with the remote Podman client, including Mac and Windows (excluding WSL2) machines) + + +[//]: # (BEGIN included file options/sign-passphrase-file.md) +#### **--sign-passphrase-file**=*path* + +If signing the image (using either **--sign-by** or **--sign-by-sigstore-private-key**), read the passphrase to use from the specified path. + +[//]: # (END included file options/sign-passphrase-file.md) + + +[//]: # (BEGIN included file options/tls-verify.md) +#### **--tls-verify** + +Require HTTPS and verify certificates when contacting registries (default: **true**). +If explicitly set to **true**, TLS verification is used. +If set to **false**, TLS verification is not used. +If not specified, TLS verification is used unless the target registry +is listed as an insecure registry in **[containers-registries.conf(5)](https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md)** + +[//]: # (END included file options/tls-verify.md) + +## EXAMPLE + +Push the specified iage to a container registry: +``` +$ podman artifact push quay.io/baude/artifact:single +Getting image source signatures +Copying blob 3ddc0a3cdb61 done | +Copying config 44136fa355 done | +Writing manifest to image destination +``` + +## SEE ALSO +**[podman(1)](podman.1.md)**, **[podman-artifact(1)](podman-artifact.1.md)**, **[podman-pull(1)](podman-pull.1.md)**, **[podman-login(1)](podman-login.1.md)**, **[containers-certs.d(5)](https://github.com/containers/image/blob/main/docs/containers-certs.d.5.md)** + + +## HISTORY +Jan 2025, Originally compiled by Brent Baude diff --git a/docs/source/markdown/podman-artifact-rm.1.md b/docs/source/markdown/podman-artifact-rm.1.md new file mode 100644 index 0000000000..207c416cf3 --- /dev/null +++ b/docs/source/markdown/podman-artifact-rm.1.md @@ -0,0 +1,46 @@ +% podman-artifact-rm 1 + + +## WARNING: Experimental command +*This command is considered experimental and still in development. Inputs, options, and outputs are all +subject to change.* + +## NAME +podman\-artifact\-rm - Remove an OCI from local storage + +## SYNOPSIS +**podman artifact rm** *name* + +## DESCRIPTION + +Remove an artifact from the local artifact store. The input may be the fully +qualified artifact name or a full or partial artifact digest. + +## OPTIONS + +#### **--help** + +Print usage statement. + + +## EXAMPLES + +Remove an artifact by name + +``` +$ podman artifact rm quay.io/artifact/foobar2:test +e7b417f49fc24fc7ead6485da0ebd5bc4419d8a3f394c169fee5a6f38faa4056 +``` + +Remove an artifact by partial digest + +``` +$ podman artifact rm e7b417f49fc +e7b417f49fc24fc7ead6485da0ebd5bc4419d8a3f394c169fee5a6f38faa4056 +``` + +## SEE ALSO +**[podman(1)](podman.1.md)**, **[podman-artifact(1)](podman-artifact.1.md)** + +## HISTORY +Jan 2025, Originally compiled by Brent Baude diff --git a/docs/source/markdown/podman-artifact.1.md b/docs/source/markdown/podman-artifact.1.md new file mode 100644 index 0000000000..8597464edb --- /dev/null +++ b/docs/source/markdown/podman-artifact.1.md @@ -0,0 +1,36 @@ +% podman-artifact 1 + +## WARNING: Experimental command +*This command is considered experimental and still in development. Inputs, options, and outputs are all +subject to change.* + +## NAME +podman\-artifact - Manage OCI artifacts + +## SYNOPSIS +**podman artifact** *subcommand* + +## DESCRIPTION +`podman artifact` is a set of subcommands that manage OCI artifacts. + +OCI artifacts are a common way to distribute files that are associated with OCI images and +containers. Podman is capable of managing (pulling, inspecting, pushing) these artifacts +from its local "artifact store". + +## SUBCOMMANDS + +| Command | Man Page | Description | +|---------|------------------------------------------------------------|--------------------------------------------------------------| +| add | [podman-artifact-add(1)](podman-artifact-add.1.md) | Add an OCI artifact to the local store | +| inspect | [podman-artifact-inspect(1)](podman-artifact-inspect.1.md) | Inspect an OCI artifact | +| ls | [podman-artifact-ls(1)](podman-artifact-ls.1.md) | List OCI artifacts in local store | +| pull | [podman-artifact-pull(1)](podman-artifact-pull.1.md) | Pulls an artifact from a registry and stores it locally | +| push | [podman-artifact-push(1)](podman-artifact-push.1.md) | Push an OCI artifact from local storage to an image registry | +| rm | [podman-artifact-rm(1)](podman-artifact-rm.1.md) | Remove an OCI from local storage | + + +## SEE ALSO +**[podman(1)](podman.1.md)** + +## HISTORY +Sept 2024, Originally compiled by Brent Baude diff --git a/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md index 5484080618..e57245d84a 100644 --- a/docs/source/markdown/podman.1.md +++ b/docs/source/markdown/podman.1.md @@ -325,69 +325,70 @@ the exit codes follow the `chroot` standard, see below: ## COMMANDS -| Command | Description | -| ------------------------------------------------ | --------------------------------------------------------------------------- | -| [podman-attach(1)](podman-attach.1.md) | Attach to a running container. | -| [podman-auto-update(1)](podman-auto-update.1.md) | Auto update containers according to their auto-update policy | -| [podman-build(1)](podman-build.1.md) | Build a container image using a Containerfile. | -| [podman-farm(1)](podman-farm.1.md) | Farm out builds to machines running podman for different architectures | -| [podman-commit(1)](podman-commit.1.md) | Create new image based on the changed container. | -| [podman-completion(1)](podman-completion.1.md) | Generate shell completion scripts | -| [podman-compose(1)](podman-compose.1.md) | Run Compose workloads via an external compose provider. | -| [podman-container(1)](podman-container.1.md) | Manage containers. | -| [podman-cp(1)](podman-cp.1.md) | Copy files/folders between a container and the local filesystem. | -| [podman-create(1)](podman-create.1.md) | Create a new container. | -| [podman-diff(1)](podman-diff.1.md) | Inspect changes on a container or image's filesystem. | -| [podman-events(1)](podman-events.1.md) | Monitor Podman events | -| [podman-exec(1)](podman-exec.1.md) | Execute a command in a running container. | -| [podman-export(1)](podman-export.1.md) | Export a container's filesystem contents as a tar archive. | -| [podman-generate(1)](podman-generate.1.md) | Generate structured data based on containers, pods or volumes. | -| [podman-healthcheck(1)](podman-healthcheck.1.md) | Manage healthchecks for containers | -| [podman-history(1)](podman-history.1.md) | Show the history of an image. | -| [podman-image(1)](podman-image.1.md) | Manage images. | -| [podman-images(1)](podman-images.1.md) | List images in local storage. | -| [podman-import(1)](podman-import.1.md) | Import a tarball and save it as a filesystem image. | -| [podman-info(1)](podman-info.1.md) | Display Podman related system information. | -| [podman-init(1)](podman-init.1.md) | Initialize one or more containers | -| [podman-inspect(1)](podman-inspect.1.md) | Display a container, image, volume, network, or pod's configuration. | -| [podman-kill(1)](podman-kill.1.md) | Kill the main process in one or more containers. | -| [podman-load(1)](podman-load.1.md) | Load image(s) from a tar archive into container storage. | -| [podman-login(1)](podman-login.1.md) | Log in to a container registry. | -| [podman-logout(1)](podman-logout.1.md) | Log out of a container registry. | -| [podman-logs(1)](podman-logs.1.md) | Display the logs of one or more containers. | -| [podman-machine(1)](podman-machine.1.md) | Manage Podman's virtual machine | -| [podman-manifest(1)](podman-manifest.1.md) | Create and manipulate manifest lists and image indexes. | -| [podman-mount(1)](podman-mount.1.md) | Mount a working container's root filesystem. | -| [podman-network(1)](podman-network.1.md) | Manage Podman networks. | -| [podman-pause(1)](podman-pause.1.md) | Pause one or more containers. | -| [podman-kube(1)](podman-kube.1.md) | Play containers, pods or volumes based on a structured input file. | -| [podman-pod(1)](podman-pod.1.md) | Management tool for groups of containers, called pods. | -| [podman-port(1)](podman-port.1.md) | List port mappings for a container. | -| [podman-ps(1)](podman-ps.1.md) | Print out information about containers. | -| [podman-pull(1)](podman-pull.1.md) | Pull an image from a registry. | -| [podman-push(1)](podman-push.1.md) | Push an image, manifest list or image index from local storage to elsewhere.| -| [podman-rename(1)](podman-rename.1.md) | Rename an existing container. | -| [podman-restart(1)](podman-restart.1.md) | Restart one or more containers. | -| [podman-rm(1)](podman-rm.1.md) | Remove one or more containers. | -| [podman-rmi(1)](podman-rmi.1.md) | Remove one or more locally stored images. | -| [podman-run(1)](podman-run.1.md) | Run a command in a new container. | -| [podman-save(1)](podman-save.1.md) | Save image(s) to an archive. | -| [podman-search(1)](podman-search.1.md) | Search a registry for an image. | -| [podman-secret(1)](podman-secret.1.md) | Manage podman secrets. | -| [podman-start(1)](podman-start.1.md) | Start one or more containers. | -| [podman-stats(1)](podman-stats.1.md) | Display a live stream of one or more container's resource usage statistics. | -| [podman-stop(1)](podman-stop.1.md) | Stop one or more running containers. | -| [podman-system(1)](podman-system.1.md) | Manage podman. | -| [podman-tag(1)](podman-tag.1.md) | Add an additional name to a local image. | -| [podman-top(1)](podman-top.1.md) | Display the running processes of a container. | -| [podman-unmount(1)](podman-unmount.1.md) | Unmount a working container's root filesystem. | -| [podman-unpause(1)](podman-unpause.1.md) | Unpause one or more containers. | -| [podman-unshare(1)](podman-unshare.1.md) | Run a command inside of a modified user namespace. | -| [podman-untag(1)](podman-untag.1.md) | Remove one or more names from a locally-stored image. | -| [podman-update(1)](podman-update.1.md) | Update the configuration of a given container. | -| [podman-version(1)](podman-version.1.md) | Display the Podman version information. | -| [podman-volume(1)](podman-volume.1.md) | Simple management tool for volumes. | -| [podman-wait(1)](podman-wait.1.md) | Wait on one or more containers to stop and print their exit codes. | +| Command | Description | +|--------------------------------------------------|------------------------------------------------------------------------------| +| [podman-artifact(1)](podman-artifact.1.md) | Manage OCI artifacts. | +| [podman-attach(1)](podman-attach.1.md) | Attach to a running container. | +| [podman-auto-update(1)](podman-auto-update.1.md) | Auto update containers according to their auto-update policy | +| [podman-build(1)](podman-build.1.md) | Build a container image using a Containerfile. | +| [podman-farm(1)](podman-farm.1.md) | Farm out builds to machines running podman for different architectures | +| [podman-commit(1)](podman-commit.1.md) | Create new image based on the changed container. | +| [podman-completion(1)](podman-completion.1.md) | Generate shell completion scripts | +| [podman-compose(1)](podman-compose.1.md) | Run Compose workloads via an external compose provider. | +| [podman-container(1)](podman-container.1.md) | Manage containers. | +| [podman-cp(1)](podman-cp.1.md) | Copy files/folders between a container and the local filesystem. | +| [podman-create(1)](podman-create.1.md) | Create a new container. | +| [podman-diff(1)](podman-diff.1.md) | Inspect changes on a container or image's filesystem. | +| [podman-events(1)](podman-events.1.md) | Monitor Podman events | +| [podman-exec(1)](podman-exec.1.md) | Execute a command in a running container. | +| [podman-export(1)](podman-export.1.md) | Export a container's filesystem contents as a tar archive. | +| [podman-generate(1)](podman-generate.1.md) | Generate structured data based on containers, pods or volumes. | +| [podman-healthcheck(1)](podman-healthcheck.1.md) | Manage healthchecks for containers | +| [podman-history(1)](podman-history.1.md) | Show the history of an image. | +| [podman-image(1)](podman-image.1.md) | Manage images. | +| [podman-images(1)](podman-images.1.md) | List images in local storage. | +| [podman-import(1)](podman-import.1.md) | Import a tarball and save it as a filesystem image. | +| [podman-info(1)](podman-info.1.md) | Display Podman related system information. | +| [podman-init(1)](podman-init.1.md) | Initialize one or more containers | +| [podman-inspect(1)](podman-inspect.1.md) | Display a container, image, volume, network, or pod's configuration. | +| [podman-kill(1)](podman-kill.1.md) | Kill the main process in one or more containers. | +| [podman-load(1)](podman-load.1.md) | Load image(s) from a tar archive into container storage. | +| [podman-login(1)](podman-login.1.md) | Log in to a container registry. | +| [podman-logout(1)](podman-logout.1.md) | Log out of a container registry. | +| [podman-logs(1)](podman-logs.1.md) | Display the logs of one or more containers. | +| [podman-machine(1)](podman-machine.1.md) | Manage Podman's virtual machine | +| [podman-manifest(1)](podman-manifest.1.md) | Create and manipulate manifest lists and image indexes. | +| [podman-mount(1)](podman-mount.1.md) | Mount a working container's root filesystem. | +| [podman-network(1)](podman-network.1.md) | Manage Podman networks. | +| [podman-pause(1)](podman-pause.1.md) | Pause one or more containers. | +| [podman-kube(1)](podman-kube.1.md) | Play containers, pods or volumes based on a structured input file. | +| [podman-pod(1)](podman-pod.1.md) | Management tool for groups of containers, called pods. | +| [podman-port(1)](podman-port.1.md) | List port mappings for a container. | +| [podman-ps(1)](podman-ps.1.md) | Print out information about containers. | +| [podman-pull(1)](podman-pull.1.md) | Pull an image from a registry. | +| [podman-push(1)](podman-push.1.md) | Push an image, manifest list or image index from local storage to elsewhere. | +| [podman-rename(1)](podman-rename.1.md) | Rename an existing container. | +| [podman-restart(1)](podman-restart.1.md) | Restart one or more containers. | +| [podman-rm(1)](podman-rm.1.md) | Remove one or more containers. | +| [podman-rmi(1)](podman-rmi.1.md) | Remove one or more locally stored images. | +| [podman-run(1)](podman-run.1.md) | Run a command in a new container. | +| [podman-save(1)](podman-save.1.md) | Save image(s) to an archive. | +| [podman-search(1)](podman-search.1.md) | Search a registry for an image. | +| [podman-secret(1)](podman-secret.1.md) | Manage podman secrets. | +| [podman-start(1)](podman-start.1.md) | Start one or more containers. | +| [podman-stats(1)](podman-stats.1.md) | Display a live stream of one or more container's resource usage statistics. | +| [podman-stop(1)](podman-stop.1.md) | Stop one or more running containers. | +| [podman-system(1)](podman-system.1.md) | Manage podman. | +| [podman-tag(1)](podman-tag.1.md) | Add an additional name to a local image. | +| [podman-top(1)](podman-top.1.md) | Display the running processes of a container. | +| [podman-unmount(1)](podman-unmount.1.md) | Unmount a working container's root filesystem. | +| [podman-unpause(1)](podman-unpause.1.md) | Unpause one or more containers. | +| [podman-unshare(1)](podman-unshare.1.md) | Run a command inside of a modified user namespace. | +| [podman-untag(1)](podman-untag.1.md) | Remove one or more names from a locally-stored image. | +| [podman-update(1)](podman-update.1.md) | Update the configuration of a given container. | +| [podman-version(1)](podman-version.1.md) | Display the Podman version information. | +| [podman-volume(1)](podman-volume.1.md) | Simple management tool for volumes. | +| [podman-wait(1)](podman-wait.1.md) | Wait on one or more containers to stop and print their exit codes. | ## CONFIGURATION FILES diff --git a/go.mod b/go.mod index dca79fb33a..c2874c0d68 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/docker/go-connections v0.5.0 github.com/docker/go-plugins-helpers v0.0.0-20240701071450-45e2431495c8 github.com/docker/go-units v0.5.0 + github.com/go-openapi/runtime v0.28.0 github.com/godbus/dbus/v5 v5.1.1-0.20241109141217-c266b19b28e9 github.com/google/gofuzz v1.2.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 @@ -133,7 +134,6 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/loads v0.22.0 // indirect - github.com/go-openapi/runtime v0.28.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/strfmt v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect diff --git a/pkg/domain/entities/artifact.go b/pkg/domain/entities/artifact.go new file mode 100644 index 0000000000..29f788a594 --- /dev/null +++ b/pkg/domain/entities/artifact.go @@ -0,0 +1,72 @@ +package entities + +import ( + "io" + + "github.com/containers/image/v5/types" + encconfig "github.com/containers/ocicrypt/config" + "github.com/containers/podman/v5/pkg/libartifact" + "github.com/opencontainers/go-digest" +) + +type ArtifactAddoptions struct { + ArtifactType string +} + +type ArtifactInspectOptions struct { + Remote bool +} + +type ArtifactListOptions struct { + ImagePushOptions +} + +type ArtifactPullOptions struct { + Architecture string + AuthFilePath string + CertDirPath string + InsecureSkipTLSVerify types.OptionalBool + MaxRetries *uint + OciDecryptConfig *encconfig.DecryptConfig + Password string + Quiet bool + RetryDelay string + SignaturePolicyPath string + Username string + Writer io.Writer +} + +type ArtifactPushOptions struct { + ImagePushOptions + CredentialsCLI string + DigestFile string + EncryptLayers []int + EncryptionKeys []string + SignBySigstoreParamFileCLI string + SignPassphraseFileCLI string + TLSVerifyCLI bool // CLI only +} + +type ArtifactRemoveOptions struct { +} + +type ArtifactPullReport struct{} + +type ArtifactPushReport struct{} + +type ArtifactInspectReport struct { + *libartifact.Artifact + Digest string +} + +type ArtifactListReport struct { + *libartifact.Artifact +} + +type ArtifactAddReport struct { + ArtifactDigest *digest.Digest +} + +type ArtifactRemoveReport struct { + ArtfactDigest *digest.Digest +} diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index 65844f676f..65c8cd8150 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -9,6 +9,12 @@ import ( ) type ImageEngine interface { //nolint:interfacebloat + ArtifactAdd(ctx context.Context, name string, paths []string, opts ArtifactAddoptions) (*ArtifactAddReport, error) + ArtifactInspect(ctx context.Context, name string, opts ArtifactInspectOptions) (*ArtifactInspectReport, error) + ArtifactList(ctx context.Context, opts ArtifactListOptions) ([]*ArtifactListReport, error) + ArtifactPull(ctx context.Context, name string, opts ArtifactPullOptions) (*ArtifactPullReport, error) + ArtifactPush(ctx context.Context, name string, opts ArtifactPushOptions) (*ArtifactPushReport, error) + ArtifactRm(ctx context.Context, name string, opts ArtifactRemoveOptions) (*ArtifactRemoveReport, error) Build(ctx context.Context, containerFiles []string, opts BuildOptions) (*BuildReport, error) Config(ctx context.Context) (*config.Config, error) Exists(ctx context.Context, nameOrID string) (*BoolReport, error) diff --git a/pkg/domain/infra/abi/artifact.go b/pkg/domain/infra/abi/artifact.go new file mode 100644 index 0000000000..cb756ad0e4 --- /dev/null +++ b/pkg/domain/infra/abi/artifact.go @@ -0,0 +1,167 @@ +//go:build !remote + +package abi + +import ( + "context" + "os" + "path/filepath" + "time" + + "github.com/containers/common/libimage" + "github.com/containers/podman/v5/pkg/domain/entities" + "github.com/containers/podman/v5/pkg/libartifact/store" +) + +func getDefaultArtifactStore(ir *ImageEngine) string { + return filepath.Join(ir.Libpod.StorageConfig().GraphRoot, "artifacts") +} + +func (ir *ImageEngine) ArtifactInspect(ctx context.Context, name string, _ entities.ArtifactInspectOptions) (*entities.ArtifactInspectReport, error) { + artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) + if err != nil { + return nil, err + } + art, err := artStore.Inspect(ctx, name) + if err != nil { + return nil, err + } + artDigest, err := art.GetDigest() + if err != nil { + return nil, err + } + artInspectReport := entities.ArtifactInspectReport{ + Artifact: art, + Digest: artDigest.String(), + } + return &artInspectReport, nil +} + +func (ir *ImageEngine) ArtifactList(ctx context.Context, _ entities.ArtifactListOptions) ([]*entities.ArtifactListReport, error) { + reports := make([]*entities.ArtifactListReport, 0) + artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) + if err != nil { + return nil, err + } + lrs, err := artStore.List(ctx) + if err != nil { + return nil, err + } + for _, lr := range lrs { + artListReport := entities.ArtifactListReport{ + Artifact: lr, + } + reports = append(reports, &artListReport) + } + return reports, nil +} + +func (ir *ImageEngine) ArtifactPull(ctx context.Context, name string, opts entities.ArtifactPullOptions) (*entities.ArtifactPullReport, error) { + pullOptions := &libimage.CopyOptions{} + pullOptions.AuthFilePath = opts.AuthFilePath + pullOptions.CertDirPath = opts.CertDirPath + pullOptions.Username = opts.Username + pullOptions.Password = opts.Password + // pullOptions.Architecture = opts.Arch + pullOptions.SignaturePolicyPath = opts.SignaturePolicyPath + pullOptions.InsecureSkipTLSVerify = opts.InsecureSkipTLSVerify + pullOptions.Writer = opts.Writer + pullOptions.OciDecryptConfig = opts.OciDecryptConfig + pullOptions.MaxRetries = opts.MaxRetries + + if !opts.Quiet && pullOptions.Writer == nil { + pullOptions.Writer = os.Stderr + } + artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) + if err != nil { + return nil, err + } + return nil, artStore.Pull(ctx, name, *pullOptions) +} + +func (ir *ImageEngine) ArtifactRm(ctx context.Context, name string, _ entities.ArtifactRemoveOptions) (*entities.ArtifactRemoveReport, error) { + artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) + if err != nil { + return nil, err + } + artifactDigest, err := artStore.Remove(ctx, artStore.SystemContext, name) + if err != nil { + return nil, err + } + artifactRemoveReport := entities.ArtifactRemoveReport{ + ArtfactDigest: artifactDigest, + } + return &artifactRemoveReport, err +} + +func (ir *ImageEngine) ArtifactPush(ctx context.Context, name string, opts entities.ArtifactPushOptions) (*entities.ArtifactPushReport, error) { + var retryDelay *time.Duration + + artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) + if err != nil { + return nil, err + } + + if opts.RetryDelay != "" { + rd, err := time.ParseDuration(opts.RetryDelay) + if err != nil { + return nil, err + } + retryDelay = &rd + } + + copyOpts := libimage.CopyOptions{ + SystemContext: nil, + SourceLookupReferenceFunc: nil, + DestinationLookupReferenceFunc: nil, + CompressionFormat: nil, + CompressionLevel: nil, + ForceCompressionFormat: false, + AuthFilePath: opts.Authfile, + BlobInfoCacheDirPath: "", + CertDirPath: opts.CertDir, + DirForceCompress: false, + ImageListSelection: 0, + InsecureSkipTLSVerify: opts.SkipTLSVerify, + MaxRetries: opts.Retry, + RetryDelay: retryDelay, + ManifestMIMEType: "", + OciAcceptUncompressedLayers: false, + OciEncryptConfig: nil, + OciEncryptLayers: opts.OciEncryptLayers, + OciDecryptConfig: nil, + Progress: nil, + PolicyAllowStorage: false, + SignaturePolicyPath: opts.SignaturePolicy, + Signers: opts.Signers, + SignBy: opts.SignBy, + SignPassphrase: opts.SignPassphrase, + SignBySigstorePrivateKeyFile: opts.SignBySigstorePrivateKeyFile, + SignSigstorePrivateKeyPassphrase: opts.SignSigstorePrivateKeyPassphrase, + RemoveSignatures: opts.RemoveSignatures, + Architecture: "", + OS: "", + Variant: "", + Username: "", + Password: "", + Credentials: opts.CredentialsCLI, + IdentityToken: "", + Writer: opts.Writer, + } + + err = artStore.Push(ctx, name, name, copyOpts) + return &entities.ArtifactPushReport{}, err +} +func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, paths []string, opts entities.ArtifactAddoptions) (*entities.ArtifactAddReport, error) { + artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) + if err != nil { + return nil, err + } + artifactDigest, err := artStore.Add(ctx, name, paths, opts.ArtifactType) + if err != nil { + return nil, err + } + return &entities.ArtifactAddReport{ + ArtifactDigest: artifactDigest, + }, nil +} diff --git a/pkg/domain/infra/tunnel/artifact.go b/pkg/domain/infra/tunnel/artifact.go new file mode 100644 index 0000000000..99657707dd --- /dev/null +++ b/pkg/domain/infra/tunnel/artifact.go @@ -0,0 +1,38 @@ +package tunnel + +import ( + "context" + "fmt" + + "github.com/containers/podman/v5/pkg/domain/entities" +) + +// TODO For now, no remote support has been added. We need the API to firm up first. + +func ArtifactAdd(ctx context.Context, path, name string, opts entities.ArtifactAddoptions) error { + return fmt.Errorf("not implemented") +} + +func (ir *ImageEngine) ArtifactInspect(ctx context.Context, name string, opts entities.ArtifactInspectOptions) (*entities.ArtifactInspectReport, error) { + return nil, fmt.Errorf("not implemented") +} + +func (ir *ImageEngine) ArtifactList(ctx context.Context, opts entities.ArtifactListOptions) ([]*entities.ArtifactListReport, error) { + return nil, fmt.Errorf("not implemented") +} + +func (ir *ImageEngine) ArtifactPull(ctx context.Context, name string, opts entities.ArtifactPullOptions) (*entities.ArtifactPullReport, error) { + return nil, fmt.Errorf("not implemented") +} + +func (ir *ImageEngine) ArtifactRm(ctx context.Context, name string, opts entities.ArtifactRemoveOptions) (*entities.ArtifactRemoveReport, error) { + return nil, fmt.Errorf("not implemented") +} + +func (ir *ImageEngine) ArtifactPush(ctx context.Context, name string, opts entities.ArtifactPushOptions) (*entities.ArtifactPushReport, error) { + return nil, fmt.Errorf("not implemented") +} + +func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, paths []string, opts entities.ArtifactAddoptions) (*entities.ArtifactAddReport, error) { + return nil, fmt.Errorf("not implemented") +} diff --git a/pkg/libartifact/artifact.go b/pkg/libartifact/artifact.go new file mode 100644 index 0000000000..326ea72f2c --- /dev/null +++ b/pkg/libartifact/artifact.go @@ -0,0 +1,89 @@ +package libartifact + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/containers/image/v5/manifest" + "github.com/opencontainers/go-digest" +) + +type Artifact struct { + Manifests []manifest.OCI1 + Name string +} + +// TotalSizeBytes returns the total bytes of the all the artifact layers +func (a *Artifact) TotalSizeBytes() int64 { + var s int64 + for _, artifact := range a.Manifests { + for _, layer := range artifact.Layers { + s += layer.Size + } + } + return s +} + +// GetName returns the "name" or "image reference" of the artifact +func (a *Artifact) GetName() (string, error) { + if a.Name != "" { + return a.Name, nil + } + // We don't have a concept of None for artifacts yet, but if we do, + // then we should probably not error but return `None` + return "", errors.New("artifact is unnamed") +} + +// SetName is a accessor for setting the artifact name +// Note: long term this may not be needed, and we would +// be comfortable with simply using the exported field +// called Name +func (a *Artifact) SetName(name string) { + a.Name = name +} + +func (a *Artifact) GetDigest() (*digest.Digest, error) { + if len(a.Manifests) > 1 { + return nil, fmt.Errorf("not supported: multiple manifests found in artifact") + } + if len(a.Manifests) < 1 { + return nil, fmt.Errorf("not supported: no manifests found in artifact") + } + b, err := json.Marshal(a.Manifests[0]) + if err != nil { + return nil, err + } + artifactDigest := digest.FromBytes(b) + return &artifactDigest, nil +} + +type ArtifactList []*Artifact + +// GetByNameOrDigest returns an artifact, if present, by a given name +// Returns an error if not found +func (al ArtifactList) GetByNameOrDigest(nameOrDigest string) (*Artifact, bool, error) { + // This is the hot route through + for _, artifact := range al { + if artifact.Name == nameOrDigest { + return artifact, false, nil + } + } + // Before giving up, check by digest + for _, artifact := range al { + // TODO Here we have to assume only a single manifest for the artifact; this will + // need to evolve + if len(artifact.Manifests) > 0 { + artifactDigest, err := artifact.GetDigest() + if err != nil { + return nil, false, err + } + // If the artifact's digest matches or is a prefix of ... + if artifactDigest.Encoded() == nameOrDigest || strings.HasPrefix(artifactDigest.Encoded(), nameOrDigest) { + return artifact, true, nil + } + } + } + return nil, false, fmt.Errorf("no artifact found with name or digest of %s", nameOrDigest) +} diff --git a/pkg/libartifact/store/store.go b/pkg/libartifact/store/store.go new file mode 100644 index 0000000000..78372b97e4 --- /dev/null +++ b/pkg/libartifact/store/store.go @@ -0,0 +1,381 @@ +//go:build !remote + +package store + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/containers/common/libimage" + "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/oci/layout" + "github.com/containers/image/v5/transports/alltransports" + "github.com/containers/image/v5/types" + "github.com/containers/podman/v5/pkg/libartifact" + types2 "github.com/containers/podman/v5/pkg/libartifact/types" + "github.com/containers/storage" + "github.com/go-openapi/runtime" + "github.com/opencontainers/go-digest" + "github.com/opencontainers/image-spec/specs-go" + specV1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/sirupsen/logrus" +) + +var ( + // indexName is the name of the JSON file in root of the artifact store + // that describes the store's contents + indexName = "index.json" + emptyStanza = []byte("{}") + + ErrEmptyArtifactName = errors.New("artifact name cannot be empty") +) + +type ArtifactStore struct { + SystemContext *types.SystemContext + storePath string +} + +// NewArtifactStore is a constructor for artifact stores. Most artifact dealings depend on this. Store path is +// the filesystem location. +func NewArtifactStore(storePath string, sc *types.SystemContext) (*ArtifactStore, error) { + // storePath here is an override + if storePath == "" { + storeOptions, err := storage.DefaultStoreOptions() + if err != nil { + return nil, err + } + if storeOptions.GraphRoot == "" { + return nil, errors.New("unable to determine artifact store") + } + storePath = filepath.Join(storeOptions.GraphRoot, "artifacts") + } + + logrus.Debugf("Using artifact store path: %s", storePath) + + artifactStore := &ArtifactStore{ + storePath: storePath, + SystemContext: sc, + } + + // if the storage dir does not exist, we need to create it. + baseDir := filepath.Dir(artifactStore.indexPath()) + if err := os.MkdirAll(baseDir, 0700); err != nil { + return nil, err + } + // if the index file is not present we need to create an empty one + _, err := os.Stat(artifactStore.indexPath()) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + if createErr := artifactStore.createEmptyManifest(); createErr != nil { + return nil, createErr + } + } + } + return artifactStore, nil +} + +// Remove an artifact from the local artifact store +func (as ArtifactStore) Remove(ctx context.Context, sys *types.SystemContext, name string) (*digest.Digest, error) { + if len(name) == 0 { + return nil, ErrEmptyArtifactName + } + + // validate and see if the input is a digest + artifacts, err := as.getArtifacts(ctx, nil) + if err != nil { + return nil, err + } + + arty, nameIsDigest, err := artifacts.GetByNameOrDigest(name) + if err != nil { + return nil, err + } + if nameIsDigest { + name = arty.Name + } + ir, err := layout.NewReference(as.storePath, name) + if err != nil { + return nil, err + } + artifactDigest, err := arty.GetDigest() + if err != nil { + return nil, err + } + return artifactDigest, ir.DeleteImage(ctx, sys) +} + +// Inspect an artifact in a local store +func (as ArtifactStore) Inspect(ctx context.Context, nameOrDigest string) (*libartifact.Artifact, error) { + if len(nameOrDigest) == 0 { + return nil, ErrEmptyArtifactName + } + artifacts, err := as.getArtifacts(ctx, nil) + if err != nil { + return nil, err + } + inspectData, _, err := artifacts.GetByNameOrDigest(nameOrDigest) + return inspectData, err +} + +// List artifacts in the local store +func (as ArtifactStore) List(ctx context.Context) (libartifact.ArtifactList, error) { + return as.getArtifacts(ctx, nil) +} + +// Pull an artifact from an image registry to a local store +func (as ArtifactStore) Pull(ctx context.Context, name string, opts libimage.CopyOptions) error { + if len(name) == 0 { + return ErrEmptyArtifactName + } + srcRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%s", name)) + if err != nil { + return err + } + destRef, err := layout.NewReference(as.storePath, name) + if err != nil { + return err + } + copyer, err := libimage.NewCopier(&opts, as.SystemContext, nil) + if err != nil { + return err + } + _, err = copyer.Copy(ctx, srcRef, destRef) + if err != nil { + return err + } + return copyer.Close() +} + +// Push an artifact to an image registry +func (as ArtifactStore) Push(ctx context.Context, src, dest string, opts libimage.CopyOptions) error { + if len(dest) == 0 { + return ErrEmptyArtifactName + } + destRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%s", dest)) + if err != nil { + return err + } + srcRef, err := layout.NewReference(as.storePath, src) + if err != nil { + return err + } + copyer, err := libimage.NewCopier(&opts, as.SystemContext, nil) + if err != nil { + return err + } + _, err = copyer.Copy(ctx, srcRef, destRef) + if err != nil { + return err + } + return copyer.Close() +} + +// Add takes one or more local files and adds them to the local artifact store. The empty +// string input is for possible custom artifact types. +func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, _ string) (*digest.Digest, error) { + if len(dest) == 0 { + return nil, ErrEmptyArtifactName + } + + artifactManifestLayers := make([]specV1.Descriptor, 0) + + // Check if artifact already exists + artifacts, err := as.getArtifacts(ctx, nil) + if err != nil { + return nil, err + } + + // Check if artifact exists; in GetByName not getting an + // error means it exists + if _, _, err := artifacts.GetByNameOrDigest(dest); err == nil { + return nil, fmt.Errorf("artifact %s already exists", dest) + } + + ir, err := layout.NewReference(as.storePath, dest) + if err != nil { + return nil, err + } + + imageDest, err := ir.NewImageDestination(ctx, as.SystemContext) + if err != nil { + return nil, err + } + + for _, path := range paths { + // get the new artifact into the local store + newBlobDigest, newBlobSize, err := layout.PutBlobFromLocalFile(ctx, imageDest, path) + if err != nil { + return nil, err + } + + newArtifactAnnotations := map[string]string{} + newArtifactAnnotations[specV1.AnnotationTitle] = filepath.Base(path) + newLayer := specV1.Descriptor{ + MediaType: runtime.DefaultMime, + Digest: newBlobDigest, + Size: newBlobSize, + Annotations: newArtifactAnnotations, + } + artifactManifestLayers = append(artifactManifestLayers, newLayer) + } + + artifactManifest := specV1.Manifest{ + Versioned: specs.Versioned{SchemaVersion: 2}, + MediaType: specV1.MediaTypeImageManifest, + ArtifactType: "", + Config: specV1.DescriptorEmptyJSON, + Layers: artifactManifestLayers, + } + + rawData, err := json.Marshal(artifactManifest) + if err != nil { + return nil, err + } + if err := imageDest.PutManifest(ctx, rawData, nil); err != nil { + return nil, err + } + + artifactManifestDigest := digest.FromBytes(rawData) + + // the config is an empty JSON stanza i.e. '{}'; if it does not yet exist, it needs + // to be created + if err := checkForEmptyStanzaFile(filepath.Join(as.storePath, specV1.ImageBlobsDir, artifactManifestDigest.Algorithm().String(), artifactManifest.Config.Digest.Encoded())); err != nil { + logrus.Errorf("failed to check or write empty stanza file: %v", err) + } + + indexAnnotation := map[string]string{} + indexAnnotation[specV1.AnnotationRefName] = dest + manifestDescriptor := specV1.Descriptor{ + MediaType: specV1.MediaTypeImageManifest, // TODO: the media type should be configurable + Digest: artifactManifestDigest, + Size: int64(len(rawData)), + Annotations: indexAnnotation, + } + + // Update the artifact in index.json + // Begin with reading it + storeIndex, err := as.readIndex() + if err != nil { + return nil, err + } + + // Update the index.json + storeIndex.Manifests = append(storeIndex.Manifests, manifestDescriptor) + + // Write index.json + if err := as.writeIndex(*storeIndex); err != nil { + return nil, err + } + return &artifactManifestDigest, nil +} + +func (as ArtifactStore) readIndex() (*specV1.Index, error) { + index := specV1.Index{} + rawData, err := os.ReadFile(as.indexPath()) + if err != nil { + return nil, err + } + err = json.Unmarshal(rawData, &index) + return &index, err +} + +func (as ArtifactStore) writeIndex(index specV1.Index) error { + rawData, err := json.Marshal(&index) + if err != nil { + return err + } + return os.WriteFile(as.indexPath(), rawData, 0o644) +} + +func (as ArtifactStore) createEmptyManifest() error { + index := specV1.Index{} + rawData, err := json.Marshal(&index) + if err != nil { + return err + } + + return os.WriteFile(as.indexPath(), rawData, 0o644) +} + +func (as ArtifactStore) indexPath() string { + return filepath.Join(as.storePath, indexName) +} + +// getArtifacts returns an ArtifactList based on the artifact's store. The return error and +// unused opts is meant for future growth like filters, etc so the API does not change. +func (as ArtifactStore) getArtifacts(ctx context.Context, _ *types2.GetArtifactOptions) (libartifact.ArtifactList, error) { + var ( + al libartifact.ArtifactList + ) + lrs, err := layout.List(as.storePath) + if err != nil { + return nil, err + } + for _, l := range lrs { + imgSrc, err := l.Reference.NewImageSource(ctx, as.SystemContext) + if err != nil { + return nil, err + } + manifests, err := getManifests(ctx, imgSrc, nil) + if err != nil { + return nil, err + } + artifact := libartifact.Artifact{ + Manifests: manifests, + } + if val, ok := l.ManifestDescriptor.Annotations[types2.AnnotatedName]; ok { + artifact.SetName(val) + } + + al = append(al, &artifact) + } + return al, nil +} + +// getManifests takes an imgSrc and starting digest (nil means "top") and collects all the manifests "under" +// it. this func calls itself recursively with a new startingDigest assuming that we are dealing with +// and index list +func getManifests(ctx context.Context, imgSrc types.ImageSource, startingDigest *digest.Digest) ([]manifest.OCI1, error) { + var ( + manifests []manifest.OCI1 + ) + b, manifestType, err := imgSrc.GetManifest(ctx, startingDigest) + if err != nil { + return nil, err + } + + // this assumes that there are only single, and multi-images + if !manifest.MIMETypeIsMultiImage(manifestType) { + // these are the keepers + mani, err := manifest.OCI1FromManifest(b) + if err != nil { + return nil, err + } + manifests = append(manifests, *mani) + return manifests, nil + } + // We are dealing with an oci index list + maniList, err := manifest.OCI1IndexFromManifest(b) + if err != nil { + return nil, err + } + for _, m := range maniList.Manifests { + iterManifests, err := getManifests(ctx, imgSrc, &m.Digest) + if err != nil { + return nil, err + } + manifests = append(manifests, iterManifests...) + } + return manifests, nil +} + +func checkForEmptyStanzaFile(path string) error { + if _, err := os.Stat(path); err == nil { + return nil + } + return os.WriteFile(path, emptyStanza, 0644) +} diff --git a/pkg/libartifact/types/config.go b/pkg/libartifact/types/config.go new file mode 100644 index 0000000000..9bbf363b96 --- /dev/null +++ b/pkg/libartifact/types/config.go @@ -0,0 +1,8 @@ +package types + +var ( + // AnnotatedName is the label name where the artifact tag reference lives + AnnotatedName = "org.opencontainers.image.ref.name" +) + +type GetArtifactOptions struct{} diff --git a/test/e2e/artifact_test.go b/test/e2e/artifact_test.go new file mode 100644 index 0000000000..deeba78cd1 --- /dev/null +++ b/test/e2e/artifact_test.go @@ -0,0 +1,157 @@ +//go:build linux || freebsd + +package integration + +import ( + "encoding/json" + "fmt" + + "github.com/containers/podman/v5/pkg/libartifact" + . "github.com/containers/podman/v5/test/utils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Podman artifact", func() { + + It("podman artifact ls", func() { + artifact1File, err := createArtifactFile(4192) + Expect(err).ToNot(HaveOccurred()) + artifact1Name := "localhost/test/artifact1" + podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact1Name, artifact1File}...) + + artifact2File, err := createArtifactFile(10240) + Expect(err).ToNot(HaveOccurred()) + artifact2Name := "localhost/test/artifact2" + podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact2Name, artifact2File}...) + + // Should be three items in the list + listSession := podmanTest.PodmanExitCleanly([]string{"artifact", "ls"}...) + Expect(listSession.OutputToStringArray()).To(HaveLen(3)) + + // --format should work + listFormatSession := podmanTest.PodmanExitCleanly([]string{"artifact", "ls", "--format", "{{.Repository}}"}...) + output := listFormatSession.OutputToStringArray() + + // There should be only 2 "lines" because the header should not be output + Expect(output).To(HaveLen(2)) + + // Make sure the names are what we expect + Expect(output).To(ContainElement(artifact1Name)) + Expect(output).To(ContainElement(artifact2Name)) + }) + + It("podman artifact simple add", func() { + artifact1File, err := createArtifactFile(1024) + Expect(err).ToNot(HaveOccurred()) + + artifact1Name := "localhost/test/artifact1" + podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact1Name, artifact1File}...) + + inspectSingleSession := podmanTest.PodmanExitCleanly([]string{"artifact", "inspect", artifact1Name}...) + + a := libartifact.Artifact{} + inspectOut := inspectSingleSession.OutputToString() + err = json.Unmarshal([]byte(inspectOut), &a) + Expect(err).ToNot(HaveOccurred()) + Expect(a.Name).To(Equal(artifact1Name)) + + // Adding an artifact with an existing name should fail + addAgain := podmanTest.Podman([]string{"artifact", "add", artifact1Name, artifact1File}) + addAgain.WaitWithDefaultTimeout() + Expect(addAgain).ShouldNot(ExitCleanly()) + Expect(addAgain.ErrorToString()).To(Equal(fmt.Sprintf("Error: artifact %s already exists", artifact1Name))) + }) + + It("podman artifact add multiple", func() { + artifact1File1, err := createArtifactFile(1024) + Expect(err).ToNot(HaveOccurred()) + artifact1File2, err := createArtifactFile(8192) + Expect(err).ToNot(HaveOccurred()) + + artifact1Name := "localhost/test/artifact1" + + podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact1Name, artifact1File1, artifact1File2}...) + + inspectSingleSession := podmanTest.PodmanExitCleanly([]string{"artifact", "inspect", artifact1Name}...) + + a := libartifact.Artifact{} + inspectOut := inspectSingleSession.OutputToString() + err = json.Unmarshal([]byte(inspectOut), &a) + Expect(err).ToNot(HaveOccurred()) + Expect(a.Name).To(Equal(artifact1Name)) + + var layerCount int + for _, layer := range a.Manifests { + layerCount += len(layer.Layers) + } + Expect(layerCount).To(Equal(2)) + }) + + It("podman artifact push and pull", func() { + artifact1File, err := createArtifactFile(1024) + Expect(err).ToNot(HaveOccurred()) + + lock, port, err := setupRegistry(nil) + if err == nil { + defer lock.Unlock() + } + Expect(err).ToNot(HaveOccurred()) + + artifact1Name := fmt.Sprintf("localhost:%s/test/artifact1", port) + podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact1Name, artifact1File}...) + + podmanTest.PodmanExitCleanly([]string{"artifact", "push", "-q", "--tls-verify=false", artifact1Name}...) + + podmanTest.PodmanExitCleanly([]string{"artifact", "rm", artifact1Name}...) + + podmanTest.PodmanExitCleanly([]string{"artifact", "pull", "--tls-verify=false", artifact1Name}...) + + inspectSingleSession := podmanTest.PodmanExitCleanly([]string{"artifact", "inspect", artifact1Name}...) + + a := libartifact.Artifact{} + inspectOut := inspectSingleSession.OutputToString() + err = json.Unmarshal([]byte(inspectOut), &a) + Expect(err).ToNot(HaveOccurred()) + Expect(a.Name).To(Equal(artifact1Name)) + }) + + It("podman artifact remove", func() { + // Trying to remove an image that does not exist should fail + rmFail := podmanTest.Podman([]string{"artifact", "rm", "foobar"}) + rmFail.WaitWithDefaultTimeout() + Expect(rmFail).Should(Exit(125)) + Expect(rmFail.ErrorToString()).Should(Equal(fmt.Sprintf("Error: no artifact found with name or digest of %s", "foobar"))) + + // Add an artifact to remove later + artifact1File, err := createArtifactFile(4192) + Expect(err).ToNot(HaveOccurred()) + artifact1Name := "localhost/test/artifact1" + addArtifact1 := podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact1Name, artifact1File}...) + + // Removing that artifact should work + rmWorks := podmanTest.PodmanExitCleanly([]string{"artifact", "rm", artifact1Name}...) + // The digests printed by removal should be the same as the digest that was added + Expect(addArtifact1.OutputToString()).To(Equal(rmWorks.OutputToString())) + + // Inspecting that the removed artifact should fail + inspectArtifact := podmanTest.Podman([]string{"artifact", "inspect", artifact1Name}) + inspectArtifact.WaitWithDefaultTimeout() + Expect(inspectArtifact).Should(Exit(125)) + Expect(inspectArtifact.ErrorToString()).To(Equal(fmt.Sprintf("Error: no artifact found with name or digest of %s", artifact1Name))) + }) + + It("podman artifact inspect with full or partial digest", func() { + artifact1File, err := createArtifactFile(4192) + Expect(err).ToNot(HaveOccurred()) + artifact1Name := "localhost/test/artifact1" + addArtifact1 := podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact1Name, artifact1File}...) + + artifactDigest := addArtifact1.OutputToString() + + podmanTest.PodmanExitCleanly([]string{"artifact", "inspect", artifactDigest}...) + podmanTest.PodmanExitCleanly([]string{"artifact", "inspect", artifactDigest[:12]}...) + + }) +}) diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index 65edfc050d..db77604a01 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -27,6 +27,7 @@ import ( "github.com/containers/podman/v5/libpod/define" "github.com/containers/podman/v5/pkg/inspect" . "github.com/containers/podman/v5/test/utils" + "github.com/containers/podman/v5/utils" "github.com/containers/storage/pkg/lockfile" "github.com/containers/storage/pkg/reexec" "github.com/containers/storage/pkg/stringid" @@ -1537,3 +1538,51 @@ func CopySymLink(source, dest string) error { func UsingCacheRegistry() bool { return os.Getenv("CI_USE_REGISTRY_CACHE") != "" } + +func setupRegistry(portOverride *int) (*lockfile.LockFile, string, error) { + var port string + if isRootless() { + if err := podmanTest.RestoreArtifact(REGISTRY_IMAGE); err != nil { + return nil, "", err + } + } + + if portOverride != nil { + port = strconv.Itoa(*portOverride) + } else { + p, err := utils.GetRandomPort() + if err != nil { + return nil, "", err + } + port = strconv.Itoa(p) + } + + lock := GetPortLock(port) + + session := podmanTest.Podman([]string{"run", "-d", "--name", "registry", "-p", fmt.Sprintf("%s:5000", port), REGISTRY_IMAGE, "/entrypoint.sh", "/etc/docker/registry/config.yml"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + + if !WaitContainerReady(podmanTest, "registry", "listening on", 20, 1) { + lock.Unlock() + Skip("Cannot start docker registry.") + } + return lock, port, nil +} + +func createArtifactFile(numBytes int64) (string, error) { + artifactDir := filepath.Join(podmanTest.TempDir, "artifacts") + if _, err := os.Stat(artifactDir); errors.Is(err, fs.ErrNotExist) { + if err := os.Mkdir(artifactDir, 0755); err != nil { + return "", err + } + } + filename := RandomString(8) + outFile := filepath.Join(artifactDir, filename) + session := podmanTest.Podman([]string{"run", "-v", fmt.Sprintf("%s:/artifacts:z", artifactDir), ALPINE, "dd", "if=/dev/urandom", fmt.Sprintf("of=%s", filepath.Join("/artifacts", filename)), "bs=1b", fmt.Sprintf("count=%d", numBytes)}) + session.WaitWithDefaultTimeout() + if session.ExitCode() != 0 { + return "", errors.New("unable to generate artifact file") + } + return outFile, nil +}