mirror of
https://github.com/containers/podman.git
synced 2025-07-03 09:17:15 +08:00
39
cmd/podman/artifact/add.go
Normal file
39
cmd/podman/artifact/add.go
Normal 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
|
||||
}
|
24
cmd/podman/artifact/artifact.go
Normal file
24
cmd/podman/artifact/artifact.go
Normal 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,
|
||||
})
|
||||
}
|
51
cmd/podman/artifact/inspect.go
Normal file
51
cmd/podman/artifact/inspect.go
Normal 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
134
cmd/podman/artifact/list.go
Normal 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
162
cmd/podman/artifact/pull.go
Normal 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
229
cmd/podman/artifact/push.go
Normal 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
50
cmd/podman/artifact/rm.go
Normal 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
|
||||
}
|
@ -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) {
|
||||
|
@ -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{}
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
2
docs/source/markdown/.gitignore
vendored
2
docs/source/markdown/.gitignore
vendored
@ -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
|
||||
|
@ -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*
|
||||
|
@ -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*
|
||||
|
@ -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]]*
|
||||
|
@ -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]*
|
||||
|
@ -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*
|
||||
|
@ -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*
|
||||
|
@ -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*
|
||||
|
@ -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*
|
||||
|
@ -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*
|
||||
|
@ -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**
|
||||
|
48
docs/source/markdown/podman-artifact-add.1.md
Normal file
48
docs/source/markdown/podman-artifact-add.1.md
Normal 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>
|
38
docs/source/markdown/podman-artifact-inspect.1.md
Normal file
38
docs/source/markdown/podman-artifact-inspect.1.md
Normal 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>
|
57
docs/source/markdown/podman-artifact-ls.1.md
Normal file
57
docs/source/markdown/podman-artifact-ls.1.md
Normal 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>
|
75
docs/source/markdown/podman-artifact-pull.1.md.in
Normal file
75
docs/source/markdown/podman-artifact-pull.1.md.in
Normal 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>
|
71
docs/source/markdown/podman-artifact-push.1.md.in
Normal file
71
docs/source/markdown/podman-artifact-push.1.md.in
Normal 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>
|
46
docs/source/markdown/podman-artifact-rm.1.md
Normal file
46
docs/source/markdown/podman-artifact-rm.1.md
Normal 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>
|
36
docs/source/markdown/podman-artifact.1.md
Normal file
36
docs/source/markdown/podman-artifact.1.md
Normal 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>
|
@ -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
|
||||
|
||||
|
72
pkg/domain/entities/artifact.go
Normal file
72
pkg/domain/entities/artifact.go
Normal 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
|
||||
}
|
@ -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)
|
||||
|
167
pkg/domain/infra/abi/artifact.go
Normal file
167
pkg/domain/infra/abi/artifact.go
Normal 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
|
||||
}
|
38
pkg/domain/infra/tunnel/artifact.go
Normal file
38
pkg/domain/infra/tunnel/artifact.go
Normal 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")
|
||||
}
|
89
pkg/libartifact/artifact.go
Normal file
89
pkg/libartifact/artifact.go
Normal 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)
|
||||
}
|
41
pkg/libartifact/store/config.go
Normal file
41
pkg/libartifact/store/config.go
Normal 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,
|
||||
}
|
||||
}
|
366
pkg/libartifact/store/store.go
Normal file
366
pkg/libartifact/store/store.go
Normal 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
|
||||
}
|
5
pkg/libartifact/types/config.go
Normal file
5
pkg/libartifact/types/config.go
Normal 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
160
test/e2e/artifact_test.go
Normal 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]}...)
|
||||
|
||||
})
|
||||
})
|
@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user