feat: add Podman artifact support to Go bindings and remote clients

Add the Go bindings implementation necessary to support Artifacts.
Implement the tunnel interface that consumes the Artifacts Go bindings.

With this patch, users of the Podman remote clients will now be able to
manage OCI artifacts via the Podman CLI and Podman machine.

Jira: https://issues.redhat.com/browse/RUN-2714#

Signed-off-by: Lewis Roy <lewis@redhat.com>
This commit is contained in:
Lewis Roy
2025-07-31 22:34:14 +10:00
committed by Matt Heon
parent 906b97e3e1
commit 5dc87663a9
36 changed files with 1199 additions and 176 deletions

View File

@ -23,20 +23,17 @@ var (
Example: `podman artifact add quay.io/myimage/myartifact:latest /tmp/foobar.txt
podman artifact add --file-type text/yaml quay.io/myimage/myartifact:latest /tmp/foobar.yaml
podman artifact add --append quay.io/myimage/myartifact:latest /tmp/foobar.tar.gz`,
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
}
)
type artifactAddOptions struct {
ArtifactType string
Annotations []string
Append bool
FileType string
// AddOptionsWrapper wraps entities.ArtifactsAddOptions and prevents leaking
// CLI-only fields into the API types.
type AddOptionsWrapper struct {
entities.ArtifactAddOptions
AnnotationsCLI []string // CLI only
}
var (
addOpts artifactAddOptions
)
var addOpts AddOptionsWrapper
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
@ -46,34 +43,36 @@ func init() {
flags := addCmd.Flags()
annotationFlagName := "annotation"
flags.StringArrayVar(&addOpts.Annotations, annotationFlagName, nil, "set an `annotation` for the specified files of artifact")
flags.StringArrayVar(&addOpts.AnnotationsCLI, annotationFlagName, nil, "set an `annotation` for the specified files of artifact")
_ = addCmd.RegisterFlagCompletionFunc(annotationFlagName, completion.AutocompleteNone)
addTypeFlagName := "type"
flags.StringVar(&addOpts.ArtifactType, addTypeFlagName, "", "Use type to describe an artifact")
_ = addCmd.RegisterFlagCompletionFunc(addTypeFlagName, completion.AutocompleteNone)
addMIMETypeFlagName := "type"
flags.StringVar(&addOpts.ArtifactMIMEType, addMIMETypeFlagName, "", "Use type to describe an artifact")
_ = addCmd.RegisterFlagCompletionFunc(addMIMETypeFlagName, completion.AutocompleteNone)
appendFlagName := "append"
flags.BoolVarP(&addOpts.Append, appendFlagName, "a", false, "Append files to an existing artifact")
fileTypeFlagName := "file-type"
flags.StringVarP(&addOpts.FileType, fileTypeFlagName, "", "", "Set file type to use for the artifact (layer)")
_ = addCmd.RegisterFlagCompletionFunc(fileTypeFlagName, completion.AutocompleteNone)
fileMIMETypeFlagName := "file-type"
flags.StringVarP(&addOpts.FileMIMEType, fileMIMETypeFlagName, "", "", "Set file type to use for the artifact (layer)")
_ = addCmd.RegisterFlagCompletionFunc(fileMIMETypeFlagName, completion.AutocompleteNone)
}
func add(cmd *cobra.Command, args []string) error {
artifactName := args[0]
blobs := args[1:]
opts := new(entities.ArtifactAddOptions)
annots, err := utils.ParseAnnotations(addOpts.Annotations)
annots, err := utils.ParseAnnotations(addOpts.AnnotationsCLI)
if err != nil {
return err
}
opts.Annotations = annots
opts.ArtifactType = addOpts.ArtifactType
opts.Append = addOpts.Append
opts.FileType = addOpts.FileType
opts := entities.ArtifactAddOptions{
Annotations: annots,
ArtifactMIMEType: addOpts.ArtifactMIMEType,
Append: addOpts.Append,
FileMIMEType: addOpts.FileMIMEType,
}
artifactBlobs := make([]entities.ArtifactBlob, 0, len(blobs))

View File

@ -6,16 +6,13 @@ import (
"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},
}
)
// Command: podman _artifact_
var artifactCmd = &cobra.Command{
Use: "artifact",
Short: "Manage OCI artifacts",
Long: "Manage OCI artifacts",
RunE: validate.SubCommandExists,
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{

View File

@ -18,7 +18,6 @@ var (
ValidArgsFunction: common.AutocompleteArtifactAdd,
Example: `podman artifact Extract quay.io/myimage/myartifact:latest /tmp/foobar.txt
podman artifact Extract quay.io/myimage/myartifact:latest /home/paul/mydir`,
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
}
)
@ -43,7 +42,7 @@ func init() {
}
func extract(cmd *cobra.Command, args []string) error {
err := registry.ImageEngine().ArtifactExtract(registry.Context(), args[0], args[1], &extractOpts)
err := registry.ImageEngine().ArtifactExtract(registry.Context(), args[0], args[1], extractOpts)
if err != nil {
return err
}

View File

@ -17,7 +17,6 @@ var (
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: common.AutocompleteArtifacts,
Example: `podman artifact inspect quay.io/myimage/myartifact:latest`,
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
}
)

View File

@ -25,7 +25,6 @@ var (
Args: validate.NoArgs,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman artifact ls`,
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
}
listFlag = listFlagType{}
)

View File

@ -36,7 +36,6 @@ var (
Args: cobra.ExactArgs(1),
ValidArgsFunction: common.AutocompleteArtifacts,
Example: `podman artifact pull quay.io/myimage/myartifact:latest`,
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
}
)

View File

@ -40,7 +40,6 @@ var (
Args: cobra.ExactArgs(1),
ValidArgsFunction: common.AutocompleteArtifacts,
Example: `podman artifact push quay.io/myimage/myartifact:latest`,
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
}
)

View File

@ -23,7 +23,6 @@ var (
ValidArgsFunction: common.AutocompleteArtifacts,
Example: `podman artifact rm quay.io/myimage/myartifact:latest
podman artifact rm -a`,
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
}
rmOptions = entities.ArtifactRemoveOptions{}

View File

@ -127,12 +127,12 @@ func PullArtifact(w http.ResponseWriter, r *http.Request) {
rc := errcd.ErrorCode().Descriptor().HTTPStatusCode
// Check if the returned error is 401 StatusUnauthorized indicating the request was unauthorized
if rc == http.StatusUnauthorized {
utils.Error(w, http.StatusUnauthorized, errcd.ErrorCode())
utils.Error(w, http.StatusUnauthorized, err)
return
}
// Check if the returned error is 404 StatusNotFound indicating the artifact was not found
if rc == http.StatusNotFound {
utils.Error(w, http.StatusNotFound, errcd.ErrorCode())
utils.Error(w, http.StatusNotFound, err)
return
}
}
@ -191,11 +191,11 @@ func AddArtifact(w http.ResponseWriter, r *http.Request) {
return
}
artifactAddOptions := &entities.ArtifactAddOptions{
Append: query.Append,
Annotations: annotations,
ArtifactType: query.ArtifactMIMEType,
FileType: query.FileMIMEType,
artifactAddOptions := entities.ArtifactAddOptions{
Append: query.Append,
Annotations: annotations,
ArtifactMIMEType: query.ArtifactMIMEType,
FileMIMEType: query.FileMIMEType,
}
artifactBlobs := []entities.ArtifactBlob{{
@ -283,7 +283,7 @@ func PushArtifact(w http.ResponseWriter, r *http.Request) {
rc := errcd.ErrorCode().Descriptor().HTTPStatusCode
// Check if the returned error is 401 indicating the request was unauthorized
if rc == 401 {
utils.Error(w, 401, errcd.ErrorCode())
utils.Error(w, 401, err)
return
}
}
@ -306,8 +306,9 @@ func ExtractArtifact(w http.ResponseWriter, r *http.Request) {
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
query := struct {
Digest string `schema:"digest"`
Title string `schema:"title"`
Digest string `schema:"digest"`
Title string `schema:"title"`
ExcludeTitle bool `schema:"excludetitle"`
}{}
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
@ -316,15 +317,16 @@ func ExtractArtifact(w http.ResponseWriter, r *http.Request) {
}
extractOpts := entities.ArtifactExtractOptions{
Title: query.Title,
Digest: query.Digest,
Title: query.Title,
Digest: query.Digest,
ExcludeTitle: query.ExcludeTitle,
}
name := utils.GetName(r)
imageEngine := abi.ImageEngine{Libpod: runtime}
err := imageEngine.ArtifactExtractTarStream(r.Context(), w, name, &extractOpts)
err := imageEngine.ArtifactExtractTarStream(r.Context(), w, name, extractOpts)
if err != nil {
if errors.Is(err, libartifact_types.ErrArtifactNotExist) {
utils.ArtifactNotFound(w, name, err)

View File

@ -242,9 +242,15 @@ func (s *APIServer) registerArtifactHandlers(r *mux.Router) error {
// in: query
// description: Only extract blob with the given digest
// type: string
// - name: excludeTitle
// in: query
// description: When extracting a single Artifact blob, don't use the blob title as the filename in the tar
// type: boolean
// responses:
// 200:
// description: Extract successful
// schema:
// type: file
// 400:
// $ref: "#/responses/badParamError"
// 404:

View File

@ -0,0 +1,42 @@
package artifacts
import (
"context"
"io"
"net/http"
"github.com/containers/podman/v5/pkg/bindings"
entitiesTypes "github.com/containers/podman/v5/pkg/domain/entities/types"
)
func Add(ctx context.Context, artifactName string, blobName string, artifactBlob io.Reader, options *AddOptions) (*entitiesTypes.ArtifactAddReport, error) {
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
if options == nil {
options = new(AddOptions)
}
params, err := options.ToParams()
if err != nil {
return nil, err
}
params.Set("name", artifactName)
params.Set("fileName", blobName)
response, err := conn.DoRequest(ctx, artifactBlob, http.MethodPost, "/artifacts/add", params, nil)
if err != nil {
return nil, err
}
defer response.Body.Close()
var artifactAddReport entitiesTypes.ArtifactAddReport
if err := response.Process(&artifactAddReport); err != nil {
return nil, err
}
return &artifactAddReport, nil
}

View File

@ -0,0 +1,113 @@
package artifacts
import (
"archive/tar"
"context"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"github.com/containers/podman/v5/pkg/bindings"
)
func Extract(ctx context.Context, artifactName string, target string, options *ExtractOptions) error {
conn, err := bindings.GetClient(ctx)
if err != nil {
return fmt.Errorf("getting client: %w", err)
}
if options == nil {
options = new(ExtractOptions)
}
// Check if target is a directory to know if we can copy more than one blob
targetIsDirectory := false
stat, err := os.Stat(target)
if err == nil {
targetIsDirectory = stat.IsDir()
} else if !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("stat target %q failed: %w", target, err)
}
// If the target is not a directory, request API to return the blob without title.
// If a blob has a malicious title it will be returned from the API without it
// as the file will be written to the provided target
if !targetIsDirectory {
options.WithExcludeTitle(true)
}
params, err := options.ToParams()
if err != nil {
return fmt.Errorf("converting options to params: %w", err)
}
response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/artifacts/%s/extract", params, nil, artifactName)
if err != nil {
return err
}
defer response.Body.Close()
if !response.IsSuccess() {
return response.Process(nil)
}
multipleBlobs := false
tr := tar.NewReader(response.Body)
for {
header, err := tr.Next()
if err == io.EOF {
break // End of archive
}
if err != nil {
return err
}
if !targetIsDirectory && multipleBlobs {
return fmt.Errorf("the artifact consists of several blobs and the target %q is not a directory and neither digest or title was specified to only copy a single blob", target)
}
// If destination isn't a file, extract to target/filename
fileTarget := target
if targetIsDirectory {
fileTarget = filepath.Join(target, header.Name)
}
if header.Typeflag == tar.TypeReg {
err = extractFile(tr, fileTarget)
if err != nil {
return err
}
}
// Signal that the first blob has been extracted so we can return an error if more
// than one blob are being extracted when target is not a directory.
multipleBlobs = true
}
return nil
}
func extractFile(tr *tar.Reader, fileTarget string) (retErr error) {
outFile, err := os.Create(fileTarget)
if err != nil {
return err
}
// Use an anonymous function to enable capturing the error from
// outFile.Close() upon returning.
defer func() {
cErr := outFile.Close()
if retErr == nil {
retErr = cErr
}
}()
_, err = io.Copy(outFile, tr)
if err != nil {
return fmt.Errorf("failed to extract blob to %s: %w", fileTarget, err)
}
return nil
}

View File

@ -0,0 +1,29 @@
package artifacts
import (
"context"
"net/http"
"github.com/containers/podman/v5/pkg/bindings"
"github.com/containers/podman/v5/pkg/domain/entities/types"
)
func Inspect(ctx context.Context, nameOrID string, options *InspectOptions) (*types.ArtifactInspectReport, error) {
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/artifacts/%s/json", nil, nil, nameOrID)
if err != nil {
return nil, err
}
defer response.Body.Close()
var inspectedData types.ArtifactInspectReport
if err := response.Process(&inspectedData); err != nil {
return nil, err
}
return &inspectedData, nil
}

View File

@ -0,0 +1,30 @@
package artifacts
import (
"context"
"net/http"
"github.com/containers/podman/v5/pkg/bindings"
"github.com/containers/podman/v5/pkg/domain/entities"
)
// List returns a list of artifacts in local storage.
func List(ctx context.Context, options *ListOptions) ([]*entities.ArtifactListReport, error) {
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/artifacts/json", nil, nil)
if err != nil {
return nil, err
}
defer response.Body.Close()
var artifactSummary []*entities.ArtifactListReport
if err := response.Process(&artifactSummary); err != nil {
return nil, err
}
return artifactSummary, nil
}

View File

@ -0,0 +1,52 @@
package artifacts
import (
"context"
"net/http"
imageTypes "github.com/containers/image/v5/types"
"github.com/containers/podman/v5/pkg/auth"
"github.com/containers/podman/v5/pkg/bindings"
"github.com/containers/podman/v5/pkg/domain/entities"
)
func Pull(ctx context.Context, name string, options *PullOptions) (*entities.ArtifactPullReport, error) {
if options == nil {
options = new(PullOptions)
}
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params, err := options.ToParams()
if err != nil {
return nil, err
}
params.Set("name", name)
header, err := auth.MakeXRegistryAuthHeader(
&imageTypes.SystemContext{
AuthFilePath: options.GetAuthfile(),
},
options.GetUsername(),
options.GetPassword(),
)
if err != nil {
return nil, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/artifacts/pull", params, header)
if err != nil {
return nil, err
}
defer response.Body.Close()
var report entities.ArtifactPullReport
if err := response.Process(&report); err != nil {
return nil, err
}
return &report, nil
}

View File

@ -0,0 +1,45 @@
package artifacts
import (
"context"
"net/http"
imageTypes "github.com/containers/image/v5/types"
"github.com/containers/podman/v5/pkg/auth"
"github.com/containers/podman/v5/pkg/bindings"
"github.com/containers/podman/v5/pkg/domain/entities"
)
func Push(ctx context.Context, name string, options *PushOptions) (*entities.ArtifactPushReport, error) {
if options == nil {
options = new(PushOptions)
}
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params, err := options.ToParams()
if err != nil {
return nil, err
}
header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, options.GetUsername(), options.GetPassword())
if err != nil {
return nil, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/artifacts/%s/push", params, header, name)
if err != nil {
return nil, err
}
defer response.Body.Close()
var report entities.ArtifactPushReport
if err := response.Process(&report); err != nil {
return nil, err
}
return &report, nil
}

View File

@ -0,0 +1,30 @@
package artifacts
import (
"context"
"net/http"
"github.com/containers/podman/v5/pkg/bindings"
"github.com/containers/podman/v5/pkg/domain/entities"
)
// Remove removes an artifact from local storage.
func Remove(ctx context.Context, nameOrID string, options *RemoveOptions) (*entities.ArtifactRemoveReport, error) {
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodDelete, "/artifacts/%s", nil, nil, nameOrID)
if err != nil {
return nil, err
}
defer response.Body.Close()
var artifactRemoveReport entities.ArtifactRemoveReport
if err := response.Process(&artifactRemoveReport); err != nil {
return nil, err
}
return &artifactRemoveReport, nil
}

View File

@ -0,0 +1,89 @@
package artifacts
import "io"
// PullOptions are optional options for pulling images
//
//go:generate go run ../generator/generator.go PullOptions
type PullOptions struct {
// Authfile is the path to the authentication file.
Authfile *string `schema:"-"`
// Password for authenticating against the registry.
Password *string `schema:"-"`
// ProgressWriter is a writer where pull progress are sent.
ProgressWriter *io.Writer `schema:"-"`
// Quiet can be specified to suppress pull progress when pulling.
Quiet *bool
// Retry number of times to retry pull in case of failure
Retry *uint
// RetryDelay between retries in case of pull failures
RetryDelay *string
// SkipTLSVerify to skip HTTPS and certificate verification.
TlsVerify *bool
// Username for authenticating against the registry.
Username *string `schema:"-"`
}
// PushOptions are optional options for pushing images
//
//go:generate go run ../generator/generator.go PushOptions
type PushOptions struct {
// Authfile is the path to the authentication file.
Authfile *string `schema:"-"`
// Password for authenticating against the registry.
Password *string `schema:"-"`
// Quiet can be specified to suppress pull progress when pulling.
Quiet *bool
// Retry number of times to retry pull in case of failure
Retry *uint
// RetryDelay between retries in case of pull failures
RetryDelay *string
// SkipTLSVerify to skip HTTPS and certificate verification.
TlsVerify *bool
// Username for authenticating against the registry.
Username *string `schema:"-"`
}
// RemoveOptions are optional options for removing images
//
//go:generate go run ../generator/generator.go RemoveOptions
type RemoveOptions struct {
// Remove all artifacts
All *bool
}
// AddOptions are optional options for removing images
//
//go:generate go run ../generator/generator.go AddOptions
type AddOptions struct {
Annotations []string
ArtifactMIMEType *string
Append *bool
FileMIMEType *string
}
// ExtractOptions
//
//go:generate go run ../generator/generator.go ExtractOptions
type ExtractOptions struct {
// Title annotation value to extract only a single blob matching that name.
// Conflicts with Digest. Optional.
Title *string
// Digest of the blob to extract.
// Conflicts with Title. Optional.
Digest *string
// ExcludeTitle option allows single blobs to be exported
// with their title/filename empty. Optional.
// Default: False
ExcludeTitle *bool
}
// ListOptions
//
//go:generate go run ../generator/generator.go ListOptions
type ListOptions struct{}
// InspectOptions
//
//go:generate go run ../generator/generator.go InspectOptions
type InspectOptions struct{}

View File

@ -0,0 +1,78 @@
// Code generated by go generate; DO NOT EDIT.
package artifacts
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *AddOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *AddOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}
// WithAnnotations set field Annotations to given value
func (o *AddOptions) WithAnnotations(value []string) *AddOptions {
o.Annotations = value
return o
}
// GetAnnotations returns value of field Annotations
func (o *AddOptions) GetAnnotations() []string {
if o.Annotations == nil {
var z []string
return z
}
return o.Annotations
}
// WithArtifactMIMEType set field ArtifactMIMEType to given value
func (o *AddOptions) WithArtifactMIMEType(value string) *AddOptions {
o.ArtifactMIMEType = &value
return o
}
// GetArtifactMIMEType returns value of field ArtifactMIMEType
func (o *AddOptions) GetArtifactMIMEType() string {
if o.ArtifactMIMEType == nil {
var z string
return z
}
return *o.ArtifactMIMEType
}
// WithAppend set field Append to given value
func (o *AddOptions) WithAppend(value bool) *AddOptions {
o.Append = &value
return o
}
// GetAppend returns value of field Append
func (o *AddOptions) GetAppend() bool {
if o.Append == nil {
var z bool
return z
}
return *o.Append
}
// WithFileMIMEType set field FileMIMEType to given value
func (o *AddOptions) WithFileMIMEType(value string) *AddOptions {
o.FileMIMEType = &value
return o
}
// GetFileMIMEType returns value of field FileMIMEType
func (o *AddOptions) GetFileMIMEType() string {
if o.FileMIMEType == nil {
var z string
return z
}
return *o.FileMIMEType
}

View File

@ -0,0 +1,63 @@
// Code generated by go generate; DO NOT EDIT.
package artifacts
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *ExtractOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *ExtractOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}
// WithTitle set field Title to given value
func (o *ExtractOptions) WithTitle(value string) *ExtractOptions {
o.Title = &value
return o
}
// GetTitle returns value of field Title
func (o *ExtractOptions) GetTitle() string {
if o.Title == nil {
var z string
return z
}
return *o.Title
}
// WithDigest set field Digest to given value
func (o *ExtractOptions) WithDigest(value string) *ExtractOptions {
o.Digest = &value
return o
}
// GetDigest returns value of field Digest
func (o *ExtractOptions) GetDigest() string {
if o.Digest == nil {
var z string
return z
}
return *o.Digest
}
// WithExcludeTitle set field ExcludeTitle to given value
func (o *ExtractOptions) WithExcludeTitle(value bool) *ExtractOptions {
o.ExcludeTitle = &value
return o
}
// GetExcludeTitle returns value of field ExcludeTitle
func (o *ExtractOptions) GetExcludeTitle() bool {
if o.ExcludeTitle == nil {
var z bool
return z
}
return *o.ExcludeTitle
}

View File

@ -0,0 +1,18 @@
// Code generated by go generate; DO NOT EDIT.
package artifacts
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *InspectOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *InspectOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}

View File

@ -0,0 +1,18 @@
// Code generated by go generate; DO NOT EDIT.
package artifacts
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *ListOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *ListOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}

View File

@ -0,0 +1,139 @@
// Code generated by go generate; DO NOT EDIT.
package artifacts
import (
"io"
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *PullOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *PullOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}
// WithAuthfile set field Authfile to given value
func (o *PullOptions) WithAuthfile(value string) *PullOptions {
o.Authfile = &value
return o
}
// GetAuthfile returns value of field Authfile
func (o *PullOptions) GetAuthfile() string {
if o.Authfile == nil {
var z string
return z
}
return *o.Authfile
}
// WithPassword set field Password to given value
func (o *PullOptions) WithPassword(value string) *PullOptions {
o.Password = &value
return o
}
// GetPassword returns value of field Password
func (o *PullOptions) GetPassword() string {
if o.Password == nil {
var z string
return z
}
return *o.Password
}
// WithProgressWriter set field ProgressWriter to given value
func (o *PullOptions) WithProgressWriter(value io.Writer) *PullOptions {
o.ProgressWriter = &value
return o
}
// GetProgressWriter returns value of field ProgressWriter
func (o *PullOptions) GetProgressWriter() io.Writer {
if o.ProgressWriter == nil {
var z io.Writer
return z
}
return *o.ProgressWriter
}
// WithQuiet set field Quiet to given value
func (o *PullOptions) WithQuiet(value bool) *PullOptions {
o.Quiet = &value
return o
}
// GetQuiet returns value of field Quiet
func (o *PullOptions) GetQuiet() bool {
if o.Quiet == nil {
var z bool
return z
}
return *o.Quiet
}
// WithRetry set field Retry to given value
func (o *PullOptions) WithRetry(value uint) *PullOptions {
o.Retry = &value
return o
}
// GetRetry returns value of field Retry
func (o *PullOptions) GetRetry() uint {
if o.Retry == nil {
var z uint
return z
}
return *o.Retry
}
// WithRetryDelay set field RetryDelay to given value
func (o *PullOptions) WithRetryDelay(value string) *PullOptions {
o.RetryDelay = &value
return o
}
// GetRetryDelay returns value of field RetryDelay
func (o *PullOptions) GetRetryDelay() string {
if o.RetryDelay == nil {
var z string
return z
}
return *o.RetryDelay
}
// WithTlsVerify set field TlsVerify to given value
func (o *PullOptions) WithTlsVerify(value bool) *PullOptions {
o.TlsVerify = &value
return o
}
// GetTlsVerify returns value of field TlsVerify
func (o *PullOptions) GetTlsVerify() bool {
if o.TlsVerify == nil {
var z bool
return z
}
return *o.TlsVerify
}
// WithUsername set field Username to given value
func (o *PullOptions) WithUsername(value string) *PullOptions {
o.Username = &value
return o
}
// GetUsername returns value of field Username
func (o *PullOptions) GetUsername() string {
if o.Username == nil {
var z string
return z
}
return *o.Username
}

View File

@ -0,0 +1,123 @@
// Code generated by go generate; DO NOT EDIT.
package artifacts
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *PushOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *PushOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}
// WithAuthfile set field Authfile to given value
func (o *PushOptions) WithAuthfile(value string) *PushOptions {
o.Authfile = &value
return o
}
// GetAuthfile returns value of field Authfile
func (o *PushOptions) GetAuthfile() string {
if o.Authfile == nil {
var z string
return z
}
return *o.Authfile
}
// WithPassword set field Password to given value
func (o *PushOptions) WithPassword(value string) *PushOptions {
o.Password = &value
return o
}
// GetPassword returns value of field Password
func (o *PushOptions) GetPassword() string {
if o.Password == nil {
var z string
return z
}
return *o.Password
}
// WithQuiet set field Quiet to given value
func (o *PushOptions) WithQuiet(value bool) *PushOptions {
o.Quiet = &value
return o
}
// GetQuiet returns value of field Quiet
func (o *PushOptions) GetQuiet() bool {
if o.Quiet == nil {
var z bool
return z
}
return *o.Quiet
}
// WithRetry set field Retry to given value
func (o *PushOptions) WithRetry(value uint) *PushOptions {
o.Retry = &value
return o
}
// GetRetry returns value of field Retry
func (o *PushOptions) GetRetry() uint {
if o.Retry == nil {
var z uint
return z
}
return *o.Retry
}
// WithRetryDelay set field RetryDelay to given value
func (o *PushOptions) WithRetryDelay(value string) *PushOptions {
o.RetryDelay = &value
return o
}
// GetRetryDelay returns value of field RetryDelay
func (o *PushOptions) GetRetryDelay() string {
if o.RetryDelay == nil {
var z string
return z
}
return *o.RetryDelay
}
// WithTlsVerify set field TlsVerify to given value
func (o *PushOptions) WithTlsVerify(value bool) *PushOptions {
o.TlsVerify = &value
return o
}
// GetTlsVerify returns value of field TlsVerify
func (o *PushOptions) GetTlsVerify() bool {
if o.TlsVerify == nil {
var z bool
return z
}
return *o.TlsVerify
}
// WithUsername set field Username to given value
func (o *PushOptions) WithUsername(value string) *PushOptions {
o.Username = &value
return o
}
// GetUsername returns value of field Username
func (o *PushOptions) GetUsername() string {
if o.Username == nil {
var z string
return z
}
return *o.Username
}

View File

@ -0,0 +1,33 @@
// Code generated by go generate; DO NOT EDIT.
package artifacts
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *RemoveOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *RemoveOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}
// WithAll set field All to given value
func (o *RemoveOptions) WithAll(value bool) *RemoveOptions {
o.All = &value
return o
}
// GetAll returns value of field All
func (o *RemoveOptions) GetAll() bool {
if o.All == nil {
var z bool
return z
}
return *o.All
}

View File

@ -5,18 +5,18 @@ import (
"github.com/containers/image/v5/types"
encconfig "github.com/containers/ocicrypt/config"
entityTypes "github.com/containers/podman/v5/pkg/domain/entities/types"
"github.com/containers/podman/v5/pkg/libartifact"
"github.com/opencontainers/go-digest"
entitiesTypes "github.com/containers/podman/v5/pkg/domain/entities/types"
)
type ArtifactAddOptions struct {
Annotations map[string]string
ArtifactType string
Append bool
FileType string
Annotations map[string]string
ArtifactMIMEType string
Append bool
FileMIMEType string
}
type ArtifactAddReport = entitiesTypes.ArtifactAddReport
type ArtifactExtractOptions struct {
// Title annotation value to extract only a single blob matching that name.
// Conflicts with Digest. Optional.
@ -24,13 +24,13 @@ type ArtifactExtractOptions struct {
// Digest of the blob to extract.
// Conflicts with Title. Optional.
Digest string
// ExcludeTitle option allows single blobs to be exported
// with their title/filename empty. Optional.
// Default: False
ExcludeTitle bool
}
type ArtifactBlob struct {
BlobReader io.Reader
BlobFilePath string
FileName string
}
type ArtifactBlob = entitiesTypes.ArtifactBlob
type ArtifactInspectOptions struct {
// Note: Remote is not currently implemented but will be used for
@ -38,8 +38,9 @@ type ArtifactInspectOptions struct {
Remote bool
}
type ArtifactListOptions struct {
}
type ArtifactListOptions struct{}
type ArtifactListReport = entitiesTypes.ArtifactListReport
type ArtifactPullOptions struct {
// containers-auth.json(5) file to use when authenticating against
@ -79,6 +80,8 @@ type ArtifactPullOptions struct {
IdentityToken string `json:"identitytoken,omitempty"`
}
type ArtifactPullReport = entitiesTypes.ArtifactPullReport
type ArtifactPushOptions struct {
ImagePushOptions
CredentialsCLI string
@ -90,29 +93,13 @@ type ArtifactPushOptions struct {
TLSVerifyCLI bool // CLI only
}
type ArtifactPushReport = entitiesTypes.ArtifactPushReport
type ArtifactRemoveOptions struct {
// Remove all artifacts
All bool
}
type ArtifactPullReport struct {
ArtifactDigest *digest.Digest
}
type ArtifactRemoveReport = entitiesTypes.ArtifactRemoveReport
type ArtifactPushReport struct {
ArtifactDigest *digest.Digest
}
type ArtifactInspectReport = entityTypes.ArtifactInspectReport
type ArtifactListReport struct {
*libartifact.Artifact
}
type ArtifactAddReport struct {
ArtifactDigest *digest.Digest
}
type ArtifactRemoveReport struct {
ArtifactDigests []*digest.Digest
}
type ArtifactInspectReport = entitiesTypes.ArtifactInspectReport

View File

@ -10,9 +10,9 @@ import (
)
type ImageEngine interface { //nolint:interfacebloat
ArtifactAdd(ctx context.Context, name string, artifactBlobs []ArtifactBlob, opts *ArtifactAddOptions) (*ArtifactAddReport, error)
ArtifactExtract(ctx context.Context, name string, target string, opts *ArtifactExtractOptions) error
ArtifactExtractTarStream(ctx context.Context, w io.Writer, name string, opts *ArtifactExtractOptions) error
ArtifactAdd(ctx context.Context, name string, artifactBlobs []ArtifactBlob, opts ArtifactAddOptions) (*ArtifactAddReport, error)
ArtifactExtract(ctx context.Context, name string, target string, opts ArtifactExtractOptions) error
ArtifactExtractTarStream(ctx context.Context, w io.Writer, name string, opts ArtifactExtractOptions) 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)

View File

@ -1,8 +1,39 @@
package types
import "github.com/containers/podman/v5/pkg/libartifact"
import (
"io"
"github.com/containers/podman/v5/pkg/libartifact"
"github.com/opencontainers/go-digest"
)
type ArtifactInspectReport struct {
*libartifact.Artifact
Digest string
}
type ArtifactBlob struct {
BlobReader io.Reader
BlobFilePath string
FileName string
}
type ArtifactAddReport struct {
ArtifactDigest *digest.Digest
}
type ArtifactRemoveReport struct {
ArtifactDigests []*digest.Digest
}
type ArtifactListReport struct {
*libartifact.Artifact
}
type ArtifactPushReport struct {
ArtifactDigest *digest.Digest
}
type ArtifactPullReport struct {
ArtifactDigest *digest.Digest
}

View File

@ -197,21 +197,17 @@ func (ir *ImageEngine) ArtifactPush(ctx context.Context, name string, opts entit
}, nil
}
func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, artifactBlobs []entities.ArtifactBlob, opts *entities.ArtifactAddOptions) (*entities.ArtifactAddReport, error) {
func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, artifactBlobs []entities.ArtifactBlob, opts entities.ArtifactAddOptions) (*entities.ArtifactAddReport, error) {
artStore, err := ir.Libpod.ArtifactStore()
if err != nil {
return nil, err
}
if opts.Annotations == nil {
opts.Annotations = make(map[string]string)
}
addOptions := types.AddOptions{
Annotations: opts.Annotations,
ArtifactType: opts.ArtifactType,
Append: opts.Append,
FileType: opts.FileType,
Annotations: opts.Annotations,
ArtifactMIMEType: opts.ArtifactMIMEType,
Append: opts.Append,
FileMIMEType: opts.FileMIMEType,
}
artifactDigest, err := artStore.Add(ctx, name, artifactBlobs, &addOptions)
@ -223,35 +219,33 @@ func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, artifactBlo
}, nil
}
func (ir *ImageEngine) ArtifactExtract(ctx context.Context, name string, target string, opts *entities.ArtifactExtractOptions) error {
func (ir *ImageEngine) ArtifactExtract(ctx context.Context, name string, target string, opts entities.ArtifactExtractOptions) error {
artStore, err := ir.Libpod.ArtifactStore()
if err != nil {
return err
}
extractOpt := &types.ExtractOptions{
extractOpt := types.ExtractOptions{
FilterBlobOptions: types.FilterBlobOptions{
Digest: opts.Digest,
Title: opts.Title,
},
}
return artStore.Extract(ctx, name, target, extractOpt)
return artStore.Extract(ctx, name, target, &extractOpt)
}
func (ir *ImageEngine) ArtifactExtractTarStream(ctx context.Context, w io.Writer, name string, opts *entities.ArtifactExtractOptions) error {
if opts == nil {
opts = &entities.ArtifactExtractOptions{}
}
func (ir *ImageEngine) ArtifactExtractTarStream(ctx context.Context, w io.Writer, name string, opts entities.ArtifactExtractOptions) error {
artStore, err := ir.Libpod.ArtifactStore()
if err != nil {
return err
}
extractOpt := &types.ExtractOptions{
extractOpt := types.ExtractOptions{
FilterBlobOptions: types.FilterBlobOptions{
Digest: opts.Digest,
Title: opts.Title,
},
ExcludeTitle: opts.ExcludeTitle,
}
return artStore.ExtractTarStream(ctx, w, name, extractOpt)
return artStore.ExtractTarStream(ctx, w, name, &extractOpt)
}

View File

@ -2,42 +2,119 @@ package tunnel
import (
"context"
"errors"
"fmt"
"io"
"os"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v5/pkg/bindings/artifacts"
"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 (ir *ImageEngine) ArtifactExtract(_ context.Context, name string, target string, opts entities.ArtifactExtractOptions) error {
options := artifacts.ExtractOptions{
Digest: &opts.Digest,
Title: &opts.Title,
ExcludeTitle: &opts.ExcludeTitle,
}
func (ir *ImageEngine) ArtifactExtract(ctx context.Context, name string, target string, opts *entities.ArtifactExtractOptions) error {
return artifacts.Extract(ir.ClientCtx, name, target, &options)
}
func (ir *ImageEngine) ArtifactExtractTarStream(_ context.Context, w io.Writer, name string, opts entities.ArtifactExtractOptions) error {
return fmt.Errorf("not implemented")
}
func (ir *ImageEngine) ArtifactExtractTarStream(ctx context.Context, w io.Writer, name string, opts *entities.ArtifactExtractOptions) error {
return fmt.Errorf("not implemented")
func (ir *ImageEngine) ArtifactInspect(_ context.Context, name string, opts entities.ArtifactInspectOptions) (*entities.ArtifactInspectReport, error) {
return artifacts.Inspect(ir.ClientCtx, name, &artifacts.InspectOptions{})
}
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(_ context.Context, opts entities.ArtifactListOptions) ([]*entities.ArtifactListReport, error) {
return artifacts.List(ir.ClientCtx, &artifacts.ListOptions{})
}
func (ir *ImageEngine) ArtifactList(ctx context.Context, opts entities.ArtifactListOptions) ([]*entities.ArtifactListReport, error) {
return nil, fmt.Errorf("not implemented")
func (ir *ImageEngine) ArtifactPull(_ context.Context, name string, opts entities.ArtifactPullOptions) (*entities.ArtifactPullReport, error) {
options := artifacts.PullOptions{
Username: &opts.Username,
Password: &opts.Password,
Quiet: &opts.Quiet,
RetryDelay: &opts.RetryDelay,
Retry: opts.MaxRetries,
}
switch opts.InsecureSkipTLSVerify {
case types.OptionalBoolTrue:
options.WithTlsVerify(false)
case types.OptionalBoolFalse:
options.WithTlsVerify(true)
}
return artifacts.Pull(ir.ClientCtx, name, &options)
}
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(_ context.Context, name string, opts entities.ArtifactRemoveOptions) (*entities.ArtifactRemoveReport, error) {
if opts.All {
// Note: This will be added when artifacts remove all endpoint is implemented
return nil, fmt.Errorf("not implemented")
}
return artifacts.Remove(ir.ClientCtx, name, &artifacts.RemoveOptions{})
}
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(_ context.Context, name string, opts entities.ArtifactPushOptions) (*entities.ArtifactPushReport, error) {
options := artifacts.PushOptions{
Username: &opts.Username,
Password: &opts.Password,
Quiet: &opts.Quiet,
RetryDelay: &opts.RetryDelay,
Retry: opts.Retry,
}
switch opts.SkipTLSVerify {
case types.OptionalBoolTrue:
options.WithTlsVerify(false)
case types.OptionalBoolFalse:
options.WithTlsVerify(true)
}
return artifacts.Push(ir.ClientCtx, name, &options)
}
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(_ context.Context, name string, artifactBlob []entities.ArtifactBlob, opts entities.ArtifactAddOptions) (*entities.ArtifactAddReport, error) {
var artifactAddReport *entities.ArtifactAddReport
func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, artifactBlob []entities.ArtifactBlob, opts *entities.ArtifactAddOptions) (*entities.ArtifactAddReport, error) {
return nil, fmt.Errorf("not implemented")
options := artifacts.AddOptions{
Append: &opts.Append,
ArtifactMIMEType: &opts.ArtifactMIMEType,
FileMIMEType: &opts.FileMIMEType,
}
for k, v := range opts.Annotations {
options.Annotations = append(options.Annotations, k+"="+v)
}
for i, blob := range artifactBlob {
if i > 0 {
// When adding more than 1 blob, set append true after the first
options.WithAppend(true)
}
f, err := os.Open(blob.BlobFilePath)
if err != nil {
return nil, err
}
defer f.Close()
artifactAddReport, err = artifacts.Add(ir.ClientCtx, name, blob.FileName, f, &options)
if err != nil && i > 0 {
_, recoverErr := artifacts.Remove(ir.ClientCtx, name, &artifacts.RemoveOptions{})
if recoverErr != nil {
return nil, fmt.Errorf("failed to cleanup unfinished artifact add: %w", errors.Join(err, recoverErr))
}
return nil, err
}
if err != nil {
return nil, err
}
}
return artifactAddReport, nil
}

View File

@ -217,8 +217,8 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, artifactBlobs []en
return nil, ErrEmptyArtifactName
}
if options.Append && len(options.ArtifactType) > 0 {
return nil, errors.New("append option is not compatible with ArtifactType option")
if options.Append && len(options.ArtifactMIMEType) > 0 {
return nil, errors.New("append option is not compatible with type option")
}
// currently we don't allow override of the filename ; if a user requirement emerges,
@ -256,7 +256,7 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, artifactBlobs []en
artifactManifest = specV1.Manifest{
Versioned: specs.Versioned{SchemaVersion: ManifestSchemaVersion},
MediaType: specV1.MediaTypeImageManifest,
ArtifactType: options.ArtifactType,
ArtifactType: options.ArtifactMIMEType,
// TODO This should probably be configurable once the CLI is capable
Config: specV1.DescriptorEmptyJSON,
Layers: make([]specV1.Descriptor, 0),
@ -314,13 +314,13 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, artifactBlobs []en
annotations[specV1.AnnotationTitle] = artifactBlob.FileName
newLayer := specV1.Descriptor{
MediaType: options.FileType,
MediaType: options.FileMIMEType,
Annotations: annotations,
}
// If we did not receive an override for the layer's mediatype, use
// detection to determine it.
if options.FileType == "" {
if options.FileMIMEType == "" {
artifactBlob.BlobReader, newLayer.MediaType, err = determineBlobMIMEType(artifactBlob)
if err != nil {
return nil, err
@ -449,6 +449,7 @@ func (as ArtifactStore) BlobMountPaths(ctx context.Context, nameOrDigest string,
if err != nil {
return nil, err
}
path, err := layout.GetLocalBlobPath(ctx, imgSrc, digest)
if err != nil {
return nil, err
@ -466,6 +467,7 @@ func (as ArtifactStore) BlobMountPaths(ctx context.Context, nameOrDigest string,
if err != nil {
return nil, err
}
path, err := layout.GetLocalBlobPath(ctx, imgSrc, l.Digest)
if err != nil {
return nil, err
@ -525,6 +527,7 @@ func (as ArtifactStore) Extract(ctx context.Context, nameOrDigest string, target
if err != nil {
return err
}
return copyTrustedImageBlobToFile(ctx, imgSrc, digest, filepath.Join(target, filename))
}
@ -534,6 +537,7 @@ func (as ArtifactStore) Extract(ctx context.Context, nameOrDigest string, target
if err != nil {
return err
}
err = copyTrustedImageBlobToFile(ctx, imgSrc, l.Digest, filepath.Join(target, filename))
if err != nil {
return err
@ -555,9 +559,6 @@ func (as ArtifactStore) ExtractTarStream(ctx context.Context, w io.Writer, nameO
}
defer imgSrc.Close()
tw := tar.NewWriter(w)
defer tw.Close()
// Return early if only a single blob is requested via title or digest
if len(options.Digest) > 0 || len(options.Title) > 0 {
digest, err := findDigest(arty, &options.FilterBlobOptions)
@ -569,11 +570,17 @@ func (as ArtifactStore) ExtractTarStream(ctx context.Context, w io.Writer, nameO
// so we do not have to get the actual title annotation form the blob.
// Passing options.Title is enough because we know it is empty when digest
// is set as we only allow either one.
filename, err := generateArtifactBlobName(options.Title, digest)
if err != nil {
return err
var filename string
if !options.ExcludeTitle {
filename, err = generateArtifactBlobName(options.Title, digest)
if err != nil {
return err
}
}
tw := tar.NewWriter(w)
defer tw.Close()
err = copyTrustedImageBlobToTarStream(ctx, imgSrc, digest, filename, tw)
if err != nil {
return err
@ -582,13 +589,40 @@ func (as ArtifactStore) ExtractTarStream(ctx context.Context, w io.Writer, nameO
return nil
}
artifactBlobCount := len(arty.Manifest.Layers)
type blob struct {
name string
digest digest.Digest
}
blobs := make([]blob, 0, artifactBlobCount)
// Gather blob details and return error on any illegal names
for _, l := range arty.Manifest.Layers {
title := l.Annotations[specV1.AnnotationTitle]
filename, err := generateArtifactBlobName(title, l.Digest)
if err != nil {
return err
digest := l.Digest
var name string
if artifactBlobCount != 1 || !options.ExcludeTitle {
name, err = generateArtifactBlobName(title, digest)
if err != nil {
return err
}
}
err = copyTrustedImageBlobToTarStream(ctx, imgSrc, l.Digest, filename, tw)
blobs = append(blobs, blob{
name: name,
digest: digest,
})
}
// Wrap io.Writer in a tar.Writer
tw := tar.NewWriter(w)
defer tw.Close()
// Write each blob to tar.Writer then close
for _, b := range blobs {
err := copyTrustedImageBlobToTarStream(ctx, imgSrc, b.digest, b.name, tw)
if err != nil {
return err
}
@ -612,7 +646,7 @@ func generateArtifactBlobName(title string, digest digest.Digest) (string, error
// We must use os.IsPathSeparator() as on Windows it checks both "\\" and "/".
for i := 0; i < len(filename); i++ {
if os.IsPathSeparator(filename[i]) {
return "", fmt.Errorf("invalid name: %q cannot contain %c", filename, filename[i])
return "", fmt.Errorf("invalid name: %q cannot contain %c: %w", filename, filename[i], libartTypes.ErrArtifactBlobTitleInvalid)
}
}
return filename, nil
@ -733,9 +767,7 @@ func (as ArtifactStore) indexPath() string {
// 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
)
var al libartifact.ArtifactList
lrs, err := layout.List(as.storePath)
if err != nil {

View File

@ -6,13 +6,13 @@ type GetArtifactOptions struct{}
// AddOptions are additional descriptors of an artifact file
type AddOptions struct {
Annotations map[string]string `json:"annotations,omitempty"`
ArtifactType string `json:",omitempty"`
// append option is not compatible with ArtifactType option
Annotations map[string]string `json:"annotations,omitempty"`
ArtifactMIMEType string `json:",omitempty"`
// append option is not compatible with ArtifactMIMEType option
Append bool `json:",omitempty"`
// FileType describes the media type for the layer. It is an override
// for the standard detection
FileType string `json:",omitempty"`
FileMIMEType string `json:",omitempty"`
}
// FilterBlobOptions options used to filter for a single blob in an artifact
@ -27,6 +27,10 @@ type FilterBlobOptions struct {
type ExtractOptions struct {
FilterBlobOptions
// ExcludeTitle option allows single blobs to be exported
// with their title/filename empty. Optional.
// Default: False
ExcludeTitle bool
}
type BlobMountPathOptions struct {

View File

@ -5,8 +5,9 @@ import (
)
var (
ErrArtifactUnamed = errors.New("artifact is unnamed")
ErrArtifactNotExist = errors.New("artifact does not exist")
ErrArtifactAlreadyExists = errors.New("artifact already exists")
ErrArtifactFileExists = errors.New("file already exists in artifact")
ErrArtifactUnamed = errors.New("artifact is unnamed")
ErrArtifactNotExist = errors.New("artifact does not exist")
ErrArtifactAlreadyExists = errors.New("artifact already exists")
ErrArtifactFileExists = errors.New("file already exists in artifact")
ErrArtifactBlobTitleInvalid = errors.New("artifact blob title invalid")
)

View File

@ -218,7 +218,7 @@ class ArtifactTestCase(APITestCase):
# Assert return error response is json and contains correct message
self.assertEqual(
rjson["cause"],
"append option is not compatible with ArtifactType option",
"append option is not compatible with type option",
)
def test_add_with_append_to_missing_artifact_fails(self):
@ -354,7 +354,7 @@ class ArtifactTestCase(APITestCase):
# Assert correct response code
self.assertEqual(r.status_code, 200, r.text)
# Assert return error response is json and contains correct message
# Assert return response is json and contains correct message
self.assertIn("sha256:", rjson["ArtifactDigest"])
def test_pull_with_retry(self):
@ -397,7 +397,7 @@ class ArtifactTestCase(APITestCase):
# Assert return error response is json and contains correct message
self.assertEqual(
rjson["cause"],
"unauthorized",
"unauthorized: access to the requested resource is not authorized",
)
def test_pull_missing_fails(self):
@ -413,9 +413,9 @@ class ArtifactTestCase(APITestCase):
self.assertEqual(r.status_code, 404, r.text)
# Assert return error response is json and contains correct message
self.assertEqual(
rjson["cause"],
self.assertIn(
"manifest unknown",
rjson["cause"],
)
def test_remove(self):
@ -459,9 +459,9 @@ class ArtifactTestCase(APITestCase):
self.assertEqual(r.status_code, 401, r.text)
# Assert return error response is json and contains correct message
self.assertEqual(
self.assertIn(
"authentication required",
rjson["cause"],
"unauthorized",
)
def test_push_bad_param(self):

View File

@ -12,10 +12,6 @@ import (
)
var _ = Describe("Podman artifact mount", func() {
BeforeEach(func() {
SkipIfRemote("artifacts are not supported on the remote client yet due to being in development still")
})
It("podman artifact mount single blob", func() {
podmanTest.PodmanExitCleanly("artifact", "pull", ARTIFACT_SINGLE)

View File

@ -21,10 +21,6 @@ const (
)
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())
@ -67,7 +63,6 @@ var _ = Describe("Podman artifact", func() {
noHeaderOutput := noHeaderSession.OutputToStringArray()
Expect(noHeaderOutput).To(HaveLen(2))
Expect(noHeaderOutput).ToNot(ContainElement("REPOSITORY"))
})
It("podman artifact simple add", func() {
@ -133,7 +128,11 @@ var _ = Describe("Podman artifact", func() {
retrySession := podmanTest.Podman([]string{"artifact", "pull", "--retry", "1", "--retry-delay", "100ms", "127.0.0.1/mybadimagename"})
retrySession.WaitWithDefaultTimeout()
Expect(retrySession).Should(ExitWithError(125, "connect: connection refused"))
Expect(retrySession.ErrorToString()).To(ContainSubstring("retrying in 100ms ..."))
// TODO: This can be removed once Artifact API supports streaming
if !IsRemote() {
Expect(retrySession.ErrorToString()).To(ContainSubstring("retrying in 100ms ..."))
}
artifact1File, err := createArtifactFile(1024)
Expect(err).ToNot(HaveOccurred())
@ -202,12 +201,15 @@ var _ = Describe("Podman artifact", func() {
multipleArgs.WaitWithDefaultTimeout()
Expect(multipleArgs).Should(ExitWithError(125, "Error: too many arguments: only accepts one artifact name or digest"))
// Remove all
podmanTest.PodmanExitCleanly("artifact", "rm", "-a")
// TODO: This should be removed once Artifact API remove endpoint supports the "all" flag
if !IsRemote() {
// Remove all
podmanTest.PodmanExitCleanly("artifact", "rm", "-a")
// There should be no artifacts in the store
rmAll := podmanTest.PodmanExitCleanly("artifact", "ls", "--noheading")
Expect(rmAll.OutputToString()).To(BeEmpty())
// There should be no artifacts in the store
rmAll := podmanTest.PodmanExitCleanly("artifact", "ls", "--noheading")
Expect(rmAll.OutputToString()).To(BeEmpty())
}
})
It("podman artifact inspect with full or partial digest", func() {
@ -220,7 +222,6 @@ var _ = Describe("Podman artifact", func() {
podmanTest.PodmanExitCleanly("artifact", "inspect", artifactDigest)
podmanTest.PodmanExitCleanly("artifact", "inspect", artifactDigest[:12])
})
It("podman artifact extract single", func() {
@ -535,7 +536,7 @@ var _ = Describe("Podman artifact", func() {
failSession := podmanTest.Podman([]string{"artifact", "add", "--type", artifactType, "--append", artifact1Name, artifact3File})
failSession.WaitWithDefaultTimeout()
Expect(failSession).Should(ExitWithError(125, "Error: append option is not compatible with ArtifactType option"))
Expect(failSession).Should(ExitWithError(125, "Error: append option is not compatible with type option"))
})
})