Merge pull request #25005 from baude/artifactrebase

podman artifact
This commit is contained in:
openshift-merge-bot[bot]
2025-01-22 14:21:17 +00:00
committed by GitHub
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
}