From d7553fabc7445901c24ead9f867b06de9aa6a910 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 | 39 ++ cmd/podman/artifact/artifact.go | 24 ++ cmd/podman/artifact/inspect.go | 51 +++ cmd/podman/artifact/list.go | 134 +++++++ cmd/podman/artifact/pull.go | 162 ++++++++ cmd/podman/artifact/push.go | 229 +++++++++++ cmd/podman/artifact/rm.go | 50 +++ cmd/podman/common/completion.go | 41 ++ cmd/podman/inspect/inspect.go | 13 +- cmd/podman/main.go | 1 + cmd/podman/utils/utils.go | 10 + docs/source/Commands.rst | 2 + docs/source/markdown/.gitignore | 2 + docs/source/markdown/options/authfile.md | 2 +- docs/source/markdown/options/cert-dir.md | 2 +- docs/source/markdown/options/creds.md | 2 +- .../source/markdown/options/decryption-key.md | 2 +- docs/source/markdown/options/digestfile.md | 2 +- docs/source/markdown/options/retry-delay.md | 2 +- docs/source/markdown/options/retry.md | 2 +- .../markdown/options/sign-by-sigstore.md | 2 +- .../markdown/options/sign-passphrase-file.md | 2 +- docs/source/markdown/options/tls-verify.md | 2 +- 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 +++ .../markdown/podman-artifact-pull.1.md.in | 75 ++++ .../markdown/podman-artifact-push.1.md.in | 71 ++++ docs/source/markdown/podman-artifact-rm.1.md | 46 +++ docs/source/markdown/podman-artifact.1.md | 36 ++ docs/source/markdown/podman.1.md | 127 +++--- 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/config.go | 41 ++ pkg/libartifact/store/store.go | 366 ++++++++++++++++++ pkg/libartifact/types/config.go | 5 + test/e2e/artifact_test.go | 160 ++++++++ test/e2e/common_test.go | 47 +++ 41 files changed, 2183 insertions(+), 84 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/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.in create mode 100644 docs/source/markdown/podman-artifact-push.1.md.in 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/config.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..3f3d2fb286 --- /dev/null +++ b/cmd/podman/artifact/add.go @@ -0,0 +1,39 @@ +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`, + Annotations: map[string]string{registry.EngineMode: registry.ABIMode}, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: addCmd, + Parent: artifactCmd, + }) +} + +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..2b05fd8f12 --- /dev/null +++ b/cmd/podman/artifact/artifact.go @@ -0,0 +1,24 @@ +package artifact + +import ( + "github.com/containers/podman/v5/cmd/podman/registry" + "github.com/containers/podman/v5/cmd/podman/validate" + "github.com/spf13/cobra" +) + +var ( + // Command: podman _artifact_ + artifactCmd = &cobra.Command{ + Use: "artifact", + Short: "Manage OCI artifacts", + Long: "Manage OCI artifacts", + RunE: validate.SubCommandExists, + Annotations: map[string]string{registry.EngineMode: registry.ABIMode}, + } +) + +func init() { + 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..3137d94321 --- /dev/null +++ b/cmd/podman/artifact/inspect.go @@ -0,0 +1,51 @@ +package artifact + +import ( + "github.com/containers/podman/v5/cmd/podman/common" + "github.com/containers/podman/v5/cmd/podman/registry" + "github.com/containers/podman/v5/cmd/podman/utils" + "github.com/containers/podman/v5/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`, + Annotations: map[string]string{registry.EngineMode: registry.ABIMode}, + } +) + +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 { + artifactOptions := entities.ArtifactInspectOptions{} + inspectData, err := registry.ImageEngine().ArtifactInspect(registry.GetContext(), args[0], artifactOptions) + if err != nil { + return err + } + return utils.PrintGenericJSON(inspectData) +} diff --git a/cmd/podman/artifact/list.go b/cmd/podman/artifact/list.go new file mode 100644 index 0000000000..e9ad56542b --- /dev/null +++ b/cmd/podman/artifact/list.go @@ -0,0 +1,134 @@ +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`, + Annotations: map[string]string{registry.EngineMode: registry.ABIMode}, + } + 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{})) +} + +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..12d78c46fb --- /dev/null +++ b/cmd/podman/artifact/pull.go @@ -0,0 +1,162 @@ +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`, + Annotations: map[string]string{registry.EngineMode: registry.ABIMode}, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: pullCmd, + Parent: artifactCmd, + }) + pullFlags(pullCmd) +} + +// 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) + + 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) + } +} + +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..e5bd68dffc --- /dev/null +++ b/cmd/podman/artifact/push.go @@ -0,0 +1,229 @@ +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`, + Annotations: map[string]string{registry.EngineMode: registry.ABIMode}, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: pushCmd, + Parent: artifactCmd, + }) + pushFlags(pushCmd) +} + +// 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..8ff6bc1bf7 --- /dev/null +++ b/cmd/podman/artifact/rm.go @@ -0,0 +1,50 @@ +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`, + Annotations: map[string]string{registry.EngineMode: registry.ABIMode}, + } + // 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, + }) +} + +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/inspect/inspect.go b/cmd/podman/inspect/inspect.go index 69864071aa..7fd5db8195 100644 --- a/cmd/podman/inspect/inspect.go +++ b/cmd/podman/inspect/inspect.go @@ -2,7 +2,6 @@ package inspect import ( "context" - "encoding/json" // due to a bug in json-iterator it cannot be used here "errors" "fmt" "os" @@ -12,6 +11,7 @@ import ( "github.com/containers/common/pkg/report" "github.com/containers/podman/v5/cmd/podman/common" "github.com/containers/podman/v5/cmd/podman/registry" + "github.com/containers/podman/v5/cmd/podman/utils" "github.com/containers/podman/v5/cmd/podman/validate" "github.com/containers/podman/v5/pkg/domain/entities" "github.com/spf13/cobra" @@ -163,7 +163,7 @@ func (i *inspector) inspect(namesOrIDs []string) error { var err error switch { case report.IsJSON(i.options.Format) || i.options.Format == "": - err = printJSON(data) + err = utils.PrintGenericJSON(data) default: // Landing here implies user has given a custom --format var rpt *report.Formatter @@ -191,15 +191,6 @@ func (i *inspector) inspect(namesOrIDs []string) error { return nil } -func printJSON(data interface{}) error { - enc := json.NewEncoder(os.Stdout) - // by default, json marshallers will force utf=8 from - // a string. this breaks healthchecks that use <,>, &&. - enc.SetEscapeHTML(false) - enc.SetIndent("", " ") - return enc.Encode(data) -} - func (i *inspector) inspectAll(ctx context.Context, namesOrIDs []string) ([]interface{}, []error, error) { var data []interface{} allErrs := []error{} 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/cmd/podman/utils/utils.go b/cmd/podman/utils/utils.go index 3a33967a24..5f4baae5c7 100644 --- a/cmd/podman/utils/utils.go +++ b/cmd/podman/utils/utils.go @@ -2,6 +2,7 @@ package utils import ( "context" + "encoding/json" "fmt" "os" "strings" @@ -148,3 +149,12 @@ func RemoveSlash(input []string) []string { } return output } + +func PrintGenericJSON(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/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/.gitignore b/docs/source/markdown/.gitignore index 7cef2f7d80..370b2e81c9 100644 --- a/docs/source/markdown/.gitignore +++ b/docs/source/markdown/.gitignore @@ -1,3 +1,5 @@ +podman-artifact-pull.1.md +podman-artifact-push.1.md podman-attach.1.md podman-auto-update.1.md podman-build.1.md diff --git a/docs/source/markdown/options/authfile.md b/docs/source/markdown/options/authfile.md index 005573b1a9..e6724b4b8a 100644 --- a/docs/source/markdown/options/authfile.md +++ b/docs/source/markdown/options/authfile.md @@ -1,5 +1,5 @@ ####> This option file is used in: -####> podman auto update, build, container runlabel, create, farm build, image sign, kube play, login, logout, manifest add, manifest inspect, manifest push, pull, push, run, search +####> podman artifact pull, artifact push, auto update, build, container runlabel, create, farm build, image sign, kube play, login, logout, manifest add, manifest inspect, manifest push, pull, push, run, search ####> If file is edited, make sure the changes ####> are applicable to all of those. #### **--authfile**=*path* diff --git a/docs/source/markdown/options/cert-dir.md b/docs/source/markdown/options/cert-dir.md index dbb008a2d0..014b1cd452 100644 --- a/docs/source/markdown/options/cert-dir.md +++ b/docs/source/markdown/options/cert-dir.md @@ -1,5 +1,5 @@ ####> This option file is used in: -####> podman build, container runlabel, farm build, image sign, kube play, login, manifest add, manifest push, pull, push, search +####> podman artifact pull, artifact push, build, container runlabel, farm build, image sign, kube play, login, manifest add, manifest push, pull, push, search ####> If file is edited, make sure the changes ####> are applicable to all of those. #### **--cert-dir**=*path* diff --git a/docs/source/markdown/options/creds.md b/docs/source/markdown/options/creds.md index 910895c20e..76bdfb5035 100644 --- a/docs/source/markdown/options/creds.md +++ b/docs/source/markdown/options/creds.md @@ -1,5 +1,5 @@ ####> This option file is used in: -####> podman build, container runlabel, farm build, kube play, manifest add, manifest push, pull, push, search +####> podman artifact pull, artifact push, build, container runlabel, farm build, kube play, manifest add, manifest push, pull, push, search ####> If file is edited, make sure the changes ####> are applicable to all of those. #### **--creds**=*[username[:password]]* diff --git a/docs/source/markdown/options/decryption-key.md b/docs/source/markdown/options/decryption-key.md index 67d6535086..ee7ed881bc 100644 --- a/docs/source/markdown/options/decryption-key.md +++ b/docs/source/markdown/options/decryption-key.md @@ -1,5 +1,5 @@ ####> This option file is used in: -####> podman build, create, farm build, pull, run +####> podman artifact pull, build, create, farm build, pull, run ####> If file is edited, make sure the changes ####> are applicable to all of those. #### **--decryption-key**=*key[:passphrase]* diff --git a/docs/source/markdown/options/digestfile.md b/docs/source/markdown/options/digestfile.md index 64166b94ff..d479337a79 100644 --- a/docs/source/markdown/options/digestfile.md +++ b/docs/source/markdown/options/digestfile.md @@ -1,5 +1,5 @@ ####> This option file is used in: -####> podman manifest push, push +####> podman artifact push, manifest push, push ####> If file is edited, make sure the changes ####> are applicable to all of those. #### **--digestfile**=*Digestfile* diff --git a/docs/source/markdown/options/retry-delay.md b/docs/source/markdown/options/retry-delay.md index 005f471e4a..14c652ffb1 100644 --- a/docs/source/markdown/options/retry-delay.md +++ b/docs/source/markdown/options/retry-delay.md @@ -1,5 +1,5 @@ ####> This option file is used in: -####> podman build, create, farm build, pull, push, run +####> podman artifact pull, artifact push, build, create, farm build, pull, push, run ####> If file is edited, make sure the changes ####> are applicable to all of those. #### **--retry-delay**=*duration* diff --git a/docs/source/markdown/options/retry.md b/docs/source/markdown/options/retry.md index 0abca43aa0..08c8c263e3 100644 --- a/docs/source/markdown/options/retry.md +++ b/docs/source/markdown/options/retry.md @@ -1,5 +1,5 @@ ####> This option file is used in: -####> podman build, create, farm build, pull, push, run +####> podman artifact pull, artifact push, build, create, farm build, pull, push, run ####> If file is edited, make sure the changes ####> are applicable to all of those. #### **--retry**=*attempts* diff --git a/docs/source/markdown/options/sign-by-sigstore.md b/docs/source/markdown/options/sign-by-sigstore.md index 3ab4d7f0b4..78919daf28 100644 --- a/docs/source/markdown/options/sign-by-sigstore.md +++ b/docs/source/markdown/options/sign-by-sigstore.md @@ -1,5 +1,5 @@ ####> This option file is used in: -####> podman manifest push, push +####> podman artifact push, manifest push, push ####> If file is edited, make sure the changes ####> are applicable to all of those. #### **--sign-by-sigstore**=*param-file* diff --git a/docs/source/markdown/options/sign-passphrase-file.md b/docs/source/markdown/options/sign-passphrase-file.md index c3b272d444..f25233ac1d 100644 --- a/docs/source/markdown/options/sign-passphrase-file.md +++ b/docs/source/markdown/options/sign-passphrase-file.md @@ -1,5 +1,5 @@ ####> This option file is used in: -####> podman manifest push, push +####> podman artifact push, manifest push, push ####> If file is edited, make sure the changes ####> are applicable to all of those. #### **--sign-passphrase-file**=*path* diff --git a/docs/source/markdown/options/tls-verify.md b/docs/source/markdown/options/tls-verify.md index 62f1e3609c..c789a53058 100644 --- a/docs/source/markdown/options/tls-verify.md +++ b/docs/source/markdown/options/tls-verify.md @@ -1,5 +1,5 @@ ####> This option file is used in: -####> podman auto update, build, container runlabel, create, farm build, kube play, login, manifest add, manifest create, manifest inspect, manifest push, pull, push, run, search +####> podman artifact pull, artifact push, auto update, build, container runlabel, create, farm build, kube play, login, manifest add, manifest create, manifest inspect, manifest push, pull, push, run, search ####> If file is edited, make sure the changes ####> are applicable to all of those. #### **--tls-verify** 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.in b/docs/source/markdown/podman-artifact-pull.1.md.in new file mode 100644 index 0000000000..4733cb8ddb --- /dev/null +++ b/docs/source/markdown/podman-artifact-pull.1.md.in @@ -0,0 +1,75 @@ +% 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 artifact pull quay.io/foobar/artifact:special +``` + +## OPTIONS + +@@option authfile + +@@option cert-dir + +@@option creds + +@@option decryption-key + + +#### **--help**, **-h** + +Print the usage statement. + +#### **--quiet**, **-q** + +Suppress output information when pulling images + +@@option retry + +@@option retry-delay + +@@option tls-verify + +## 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.in b/docs/source/markdown/podman-artifact-push.1.md.in new file mode 100644 index 0000000000..e304152965 --- /dev/null +++ b/docs/source/markdown/podman-artifact-push.1.md.in @@ -0,0 +1,71 @@ +% 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 artifact push quay.io/artifact/foobar1:latest +``` + +## OPTIONS + +@@option authfile + +@@option cert-dir + +@@option creds + +@@option digestfile + +#### **--quiet**, **-q** + +When writing the output image, suppress progress output + +@@option retry + +@@option retry-delay + +#### **--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) + +@@option sign-by-sigstore + + +#### **--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) + +@@option sign-passphrase-file + +@@option tls-verify + +## 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/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..126db79e26 --- /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, 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/config.go b/pkg/libartifact/store/config.go new file mode 100644 index 0000000000..5613e18554 --- /dev/null +++ b/pkg/libartifact/store/config.go @@ -0,0 +1,41 @@ +//go:build !remote + +package store + +import ( + "context" + "encoding/json" + + "github.com/containers/image/v5/types" + specV1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +// unparsedArtifactImage is an interface based on the UnParsedImage and +// is used only for the commit of the manifest +type unparsedArtifactImage struct { + ir types.ImageReference + mannyfest specV1.Manifest +} + +func (u unparsedArtifactImage) Reference() types.ImageReference { + return u.ir +} + +func (u unparsedArtifactImage) Manifest(ctx context.Context) ([]byte, string, error) { + b, err := json.Marshal(u.mannyfest) + if err != nil { + return nil, "", err + } + return b, specV1.MediaTypeImageIndex, nil +} + +func (u unparsedArtifactImage) Signatures(ctx context.Context) ([][]byte, error) { + return [][]byte{}, nil +} + +func newUnparsedArtifactImage(ir types.ImageReference, mannyfest specV1.Manifest) unparsedArtifactImage { + return unparsedArtifactImage{ + ir: ir, + mannyfest: mannyfest, + } +} diff --git a/pkg/libartifact/store/store.go b/pkg/libartifact/store/store.go new file mode 100644 index 0000000000..c33e17d9ef --- /dev/null +++ b/pkg/libartifact/store/store.go @@ -0,0 +1,366 @@ +//go:build !remote + +package store + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "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" + libartTypes "github.com/containers/podman/v5/pkg/libartifact/types" + "github.com/containers/storage/pkg/fileutils" + "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 ( + 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) { + if storePath == "" { + return nil, errors.New("store path cannot be empty") + } + 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 + if err := fileutils.Exists(artifactStore.indexPath()); err != nil && 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, 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, as.SystemContext) +} + +// 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 + } + defer imageDest.Close() + + 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 + } + detectedType, err := determineManifestType(path) + if err != nil { + return nil, err + } + newArtifactAnnotations := map[string]string{} + newArtifactAnnotations[specV1.AnnotationTitle] = filepath.Base(path) + newLayer := specV1.Descriptor{ + MediaType: detectedType, + Digest: newBlobDigest, + Size: newBlobSize, + Annotations: newArtifactAnnotations, + } + artifactManifestLayers = append(artifactManifestLayers, newLayer) + } + + artifactManifest := specV1.Manifest{ + Versioned: specs.Versioned{SchemaVersion: 2}, + MediaType: specV1.MediaTypeImageManifest, + // TODO This should probably be configurable once the CLI is capable + 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 + } + unparsed := newUnparsedArtifactImage(ir, artifactManifest) + if err := imageDest.Commit(ctx, unparsed); 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 := createEmptyStanza(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) + } + return &artifactManifestDigest, nil +} + +// readIndex is currently unused but I want to keep this around until +// the artifact code is more mature. +func (as ArtifactStore) readIndex() (*specV1.Index, error) { //nolint:unused + 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) createEmptyManifest() error { + index := specV1.Index{ + MediaType: specV1.MediaTypeImageIndex, + Versioned: specs.Versioned{SchemaVersion: 2}, + } + 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, specV1.ImageIndexFile) +} + +// 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, _ *libartTypes.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) + imgSrc.Close() + if err != nil { + return nil, err + } + artifact := libartifact.Artifact{ + Manifests: manifests, + } + if val, ok := l.ManifestDescriptor.Annotations[specV1.AnnotationRefName]; 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 +// an 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 createEmptyStanza(path string) error { + if err := fileutils.Exists(path); err == nil { + return nil + } + return os.WriteFile(path, specV1.DescriptorEmptyJSON.Data, 0644) +} + +func determineManifestType(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + // DetectContentType looks at the first 512 bytes + b := make([]byte, 512) + // Because DetectContentType will return a default value + // we don't sweat the error + n, err := f.Read(b) + if err != nil && !errors.Is(err, io.EOF) { + return "", err + } + return http.DetectContentType(b[:n]), nil +} diff --git a/pkg/libartifact/types/config.go b/pkg/libartifact/types/config.go new file mode 100644 index 0000000000..c52d677942 --- /dev/null +++ b/pkg/libartifact/types/config.go @@ -0,0 +1,5 @@ +package types + +// GetArtifactOptions is a struct containing options that for obtaining artifacts. +// It is meant for future growth or changes required without wacking the API +type GetArtifactOptions struct{} diff --git a/test/e2e/artifact_test.go b/test/e2e/artifact_test.go new file mode 100644 index 0000000000..13deb8589a --- /dev/null +++ b/test/e2e/artifact_test.go @@ -0,0 +1,160 @@ +//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() { + BeforeEach(func() { + SkipIfRemote("artifacts are not supported on the remote client yet due to being in development still") + }) + + 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..65a04de847 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,49 @@ 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.MkdirAll(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 +}