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 <pholzing@redhat.com>
Signed-off-by: Brent Baude <bbaude@redhat.com>
This commit is contained in:
Brent Baude
2024-08-22 12:13:01 -05:00
parent c586d36223
commit d7553fabc7
41 changed files with 2183 additions and 84 deletions

View File

@ -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
}

View File

@ -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,
})
}

View File

@ -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)
}

134
cmd/podman/artifact/list.go Normal file
View File

@ -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)
}

162
cmd/podman/artifact/pull.go Normal file
View File

@ -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
}

229
cmd/podman/artifact/push.go Normal file
View File

@ -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
}

50
cmd/podman/artifact/rm.go Normal file
View File

@ -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
}

View File

@ -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) {

View File

@ -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{}

View File

@ -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"

View File

@ -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)
}

View File

@ -5,6 +5,8 @@ Commands
:doc:`Podman <markdown/podman.1>` (Pod Manager) Global Options, Environment Variables, Exit Codes, Configuration Files, and more
:doc:`artifact <markdown/podman-artifact.1>` Manage OCI artifacts
:doc:`attach <markdown/podman-attach.1>` Attach to a running container
:doc:`auto-update <markdown/podman-auto-update.1>` Auto update containers according to their auto-update policy

View File

@ -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

View File

@ -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*

View File

@ -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*

View File

@ -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]]*

View File

@ -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]*

View File

@ -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*

View File

@ -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*

View File

@ -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*

View File

@ -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*

View File

@ -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*

View File

@ -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**

View File

@ -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 <bbaude@redhat.com>

View File

@ -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 <bbaude@redhat.com>

View File

@ -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 <bbaude@redhat.com>

View File

@ -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 <bbaude@redhat.com>

View File

@ -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 <bbaude@redhat.com>

View File

@ -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 <bbaude@redhat.com>

View File

@ -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 <bbaude@redhat.com>

View File

@ -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

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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")
}

View File

@ -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)
}

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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{}

160
test/e2e/artifact_test.go Normal file
View File

@ -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]}...)
})
})

View File

@ -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
}