Merge pull request #26789 from mheon/560_rc2_backports

Backports for Podman v5.6.0-RC2
This commit is contained in:
openshift-merge-bot[bot] 2025-08-08 14:41:34 +00:00 • committed by GitHub
commit 419a031fcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1321 additions and 196 deletions

View file

@ -11,6 +11,7 @@
- The `--mount` option to `podman create` and `podman run` now supports `dest=` as a valid alias for `destination=`.
- The `podman kube play` command can now restrict container execution to specific CPU cores and specific memory nodes using the `io.podman.annotations.cpuset/$ctrname` and `io.podman.annotations.memory-nodes/$ctrname` annotations ([#26172](https://github.com/containers/podman/issues/26172)).
- The `podman kube play` command now supports the `lifecycle.stopSignal` field in Pod YAML, allowing the signal used to stop containers to be specified ([#25389](https://github.com/containers/podman/issues/25389)).
- The `podman artifact` suite of commands for interacting with OCI artifacts is now available in the remote Podman client and the bindings for the REST API.
- The `podman volume import` and `podman volume export` commands are now available in the remote Podman client ([#26049](https://github.com/containers/podman/issues/26409)).
- The `--build-context` option to `podman build` is now supported by the remote Podman client ([#23433](https://github.com/containers/podman/issues/23433)).
- The `podman volume create` command now accepts two new options, `--uid` and `--gid`, to set the UID and GID the volume will be created with.
@ -22,6 +23,7 @@
- A new command, `podman buildx inspect`, has been added to improve Docker compatibility ([#13014](https://github.com/containers/podman/issues/13014)).
### Changes
- The `podman artifact` suite of commands for interacting with OCI artifacts is now considered stable.
- For users running `podman machine` VMs using the `libkrun` provider on an M3 or newer host running macOS 15+, nested virtualization is enabled by default.
- When creating `podman machine` VMs on Windows using the WSL v2 provider, images are now pulled as artifacts from `quay.io/podman/machine-os`, matching the behavior of other VM providers.
- Signal forwarding done by the `--sig-proxy` option to `podman run` and `podman attach` is now more robust to races and no longer forwards the `SIGSTOP` signal.
@ -58,7 +60,11 @@
- A full set of API endpoints for interacting with artifacts has been added, including inspecting artifacts (`GET /libpod/artifacts/{name}/json`), listing all artifacts (`GET /libpod/artifacts/json`), pulling an artifact (`POST /libpod/artifacts/pull`), removing an artifact (`DELETE /libpod/artifacts/{name}`), adding an artifact (or appending to an existing artifact) from a tar file in the request body (`POST /libpod/artifacts/add`), pushing an artifact to a registry (`/libpod/artifacts/{name}/push`), and retrieving the contents of an artifact (`GET /libpod/artifacts/{name}/extract`).
- The Compat Create endpoint for Containers now accepts a new parameter, `HostConfig.CgroupnsMode`, to specify the cgroup namespace mode of the created container.
- The Compat Create endpoint for Containers now respects the `base_hosts_file` option in `containers.conf`.
- The Compat Info endpoint now returns a new field, `DefaultAddressPools`.
- The Compat System Info endpoint now returns a new field, `DefaultAddressPools`.
- The Compat System DF endpoint has removed the deprecated `BuilderSize` field.
- The Compat Ping endpoint now sets `Builder-Version` to `1` to match Docker installs that do not include BuildKit.
- The Compat List endpoint for Images now returns the `shared-size` field unconditionally, even if the `shared-size` query parameter was not set to true. If not requested through query parameter, it is set to `-1`. This improves Docker API compatibility.
- The Compat Inspect endpoint for Images now no longer returns the deprecated `VirtualSize` field when Docker API version 1.44 and up is requested.
- Fixed a bug where the Compat Delete API for Containers would remove running containers when the `FORCE` parameter was set to true; Docker only removes stopped containers ([#25871](https://github.com/containers/podman/issues/25871)).
- Fixed a bug where the Compat List and Compat Inspect endpoints for Containers returned container status using Podman statuses instead of converting to Docker-compatible statuses ([#17728](https://github.com/containers/podman/issues/17728)).
- Fixed a bug where healthchecks that exceeded their timeout were not properly terminated; they now receive SIGTERM, then SIGKILL after a delay, if their timeout is exceeded ([#26086](https://github.com/containers/podman/pull/26086)).
@ -67,10 +73,10 @@
### Misc
- Quadlet now no longer uses container/pod ID files when stopping containers, but instead passes the name of the container/pod directly to `podman stop`/`podman pod stop`.
- When building Podman via Makefile, it will now attempt to dynamically link sqlite3 if the library and header are installed locally. This and other optimizations should result in a significant reduction in binary size relative to Podman 5.5.x. Packagers can use the `libsqlite3` build tag to force this behavior when not using the Makefile to build.
- Updated Buildah to v1.41.0
- Updated the containers/common library to v0.64.0
- Updated the containers/storage library to v1.59.0
- Updated the containers/image library to v5.36.0
- Updated Buildah to v1.41.1
- Updated the containers/common library to v0.64.1
- Updated the containers/storage library to v1.59.1
- Updated the containers/image library to v5.36.1
## 5.5.2
### Security

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

@ -19,6 +19,7 @@ import (
"github.com/containers/podman/v5/libpod"
"github.com/containers/podman/v5/pkg/api/handlers"
"github.com/containers/podman/v5/pkg/api/handlers/utils"
"github.com/containers/podman/v5/pkg/api/handlers/utils/apiutil"
api "github.com/containers/podman/v5/pkg/api/types"
"github.com/containers/podman/v5/pkg/auth"
"github.com/containers/podman/v5/pkg/domain/entities"
@ -340,7 +341,7 @@ func GetImage(w http.ResponseWriter, r *http.Request) {
utils.Error(w, http.StatusNotFound, fmt.Errorf("failed to find image %s: %s", name, errMsg))
return
}
inspect, err := imageDataToImageInspect(r.Context(), newImage)
inspect, err := imageDataToImageInspect(r.Context(), newImage, r)
if err != nil {
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to convert ImageData to ImageInspect '%s': %w", name, err))
return
@ -348,7 +349,7 @@ func GetImage(w http.ResponseWriter, r *http.Request) {
utils.WriteResponse(w, http.StatusOK, inspect)
}
func imageDataToImageInspect(ctx context.Context, l *libimage.Image) (*handlers.ImageInspect, error) {
func imageDataToImageInspect(ctx context.Context, l *libimage.Image, r *http.Request) (*handlers.ImageInspect, error) {
options := &libimage.InspectOptions{WithParent: true, WithSize: true}
info, err := l.Inspect(ctx, options)
if err != nil {
@ -407,8 +408,13 @@ func imageDataToImageInspect(ctx context.Context, l *libimage.Image) (*handlers.
RootFS: rootfs,
Size: info.Size,
Variant: "",
VirtualSize: info.VirtualSize,
}
if _, err := apiutil.SupportedVersion(r, "<1.44.0"); err == nil {
//nolint:staticcheck // Deprecated field
dockerImageInspect.VirtualSize = info.VirtualSize
}
return &handlers.ImageInspect{InspectResponse: dockerImageInspect}, nil
}
@ -416,9 +422,10 @@ func GetImages(w http.ResponseWriter, r *http.Request) {
decoder := utils.GetDecoder(r)
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
query := struct {
All bool
Digests bool
Filter string // Docker 1.24 compatibility
All bool
Digests bool
Filter string // Docker 1.24 compatibility
SharedSize bool `schema:"shared-size"` // Docker 1.42 compatibility
}{
// This is where you can override the golang default value for one of fields
}
@ -468,6 +475,26 @@ func GetImages(w http.ResponseWriter, r *http.Request) {
// docker adds sha256: in front of the ID
for _, s := range summaries {
s.ID = "sha256:" + s.ID
// Ensure RepoTags and RepoDigests are empty arrays instead of null for Docker compatibility
// as per https://docs.docker.com/reference/api/engine/version-history/#v143-api-changes
// Relates to https://issues.redhat.com/browse/RUN-2699
if s.RepoTags == nil {
s.RepoTags = []string{}
}
if s.RepoDigests == nil {
s.RepoDigests = []string{}
}
// Docker 1.42 sets SharedSize to -1 if ont passed explicitly
if !query.SharedSize {
s.SharedSize = -1
}
// VirtualSize is deprecated in version 1.43 and removed in version 1.44
// See https://docs.docker.com/reference/api/engine/version-history/#v143-api-changes
if _, err := apiutil.SupportedVersion(r, "<1.44.0"); err == nil {
s.VirtualSize = s.Size
} else {
s.VirtualSize = 0
}
}
}
utils.WriteResponse(w, http.StatusOK, summaries)

View file

@ -18,7 +18,8 @@ import (
func Ping(w http.ResponseWriter, r *http.Request) {
// Note: API-Version and Libpod-API-Version are set in handler_api.go
w.Header().Set("BuildKit-Version", "")
w.Header().Set("Builder-Version", "")
// Docker uses Builder-Version 1 for classic builder and 2 for BuildKit
w.Header().Set("Builder-Version", "1")
w.Header().Set("Docker-Experimental", "true")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("OSType", runtime.GOOS)

View file

@ -9,6 +9,7 @@ import (
"github.com/containers/podman/v5/libpod"
"github.com/containers/podman/v5/pkg/api/handlers"
"github.com/containers/podman/v5/pkg/api/handlers/utils"
"github.com/containers/podman/v5/pkg/api/handlers/utils/apiutil"
api "github.com/containers/podman/v5/pkg/api/types"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/domain/infra/abi"
@ -41,8 +42,12 @@ func GetDiskUsage(w http.ResponseWriter, r *http.Request) {
RepoTags: []string{o.Tag},
SharedSize: o.SharedSize,
Size: o.Size,
VirtualSize: o.Size - o.UniqueSize,
}
if _, err := apiutil.SupportedVersion(r, "<1.44.0"); err == nil {
t.VirtualSize = o.Size - o.UniqueSize //nolint:staticcheck // Deprecated field
}
imgs[i] = &t
}
@ -91,11 +96,13 @@ func GetDiskUsage(w http.ResponseWriter, r *http.Request) {
}
utils.WriteResponse(w, http.StatusOK, handlers.DiskUsage{DiskUsage: docker.DiskUsage{
LayersSize: df.ImagesSize,
Images: imgs,
Containers: ctnrs,
Volumes: vols,
BuildCache: []*build.CacheRecord{},
BuilderSize: 0,
// BuilderSize was explicitly omitted since Docker deprecated its in ver 1.42
// and suggests to use BuildCache.
// https://docs.docker.com/reference/api/engine/version-history/#v142-api-changes
LayersSize: df.ImagesSize,
Images: imgs,
Containers: ctnrs,
Volumes: vols,
BuildCache: []*build.CacheRecord{},
}})
}

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

@ -16,7 +16,7 @@ type ImageSummary struct {
Created int64
Size int64
SharedSize int
VirtualSize int64
VirtualSize int64 `json:",omitempty"`
Labels map[string]string
Containers int
ReadOnly bool `json:",omitempty"`

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

@ -42,6 +42,14 @@ t GET images/json?filter=$IMAGE 200 \
length=1 \
.[0].Names[0]=$IMAGE
# Test VirtualSize field is present in API v1.43 (backward compatibility)
t GET /v1.43/images/json 200 \
.[0].VirtualSize~[0-9]\\+
# Test VirtualSize field is no longer present in API v1.44+ (deprecated since API v1.43)
t GET /v1.44/images/json 200 \
.[0].VirtualSize=null
# Negative test case
t GET images/json?filter=nonesuch 200 length=0
@ -50,6 +58,14 @@ t GET images/$iid/json 200 \
.Id=sha256:$iid \
.RepoTags[0]=$IMAGE
# Test VirtualSize field is present in API v1.43 for single image inspect (backward compatibility)
t GET /v1.43/images/$iid/json 200 \
.VirtualSize~[0-9]\\+
# Test VirtualSize field is no longer present in API v1.44+ for single image inspect (deprecated since API v1.43)
t GET /v1.44/images/$iid/json 200 \
.VirtualSize=null
t POST "images/create?fromImage=alpine" 200 .error~null .status~".*Download complete.*"
t POST "libpod/images/pull?reference=alpine&compatMode=true" 200 .error~null .status~".*Download complete.*"
@ -440,4 +456,26 @@ t GET "libpod/events?stream=false&since=$START" 200 \
.Actor.Attributes.name="localhost:5000/idonotexist" \
.Actor.Attributes.error~".*connection refused"
# test empty RepoTags and RepoDigests is an empty array
IIDFILE=$(mktemp)
podman image build --iidfile $IIDFILE -<<EOF
FROM $IMAGE
RUN :
EOF
t GET images/json 200 \
.[1].RepoTags=[] \
.[1].RepoDigests=[] \
.[1].Id=$(< $IIDFILE)
podman rmi -f $(< $IIDFILE)
rm -f $IIDFILE
# check that SharedSize returns -1 for compat api if not set
# and 0 (or the correct shared size) if set
t GET images/json 200 \
.[0].SharedSize=-1
t GET images/json?shared-size=true 200 \
.[0].SharedSize=0
# vim: filetype=sh

View file

@ -38,6 +38,27 @@ cid=$(jq -r '.Id' <<<"$output")
t GET system/df 200 '.LayersSize=12180391'
t GET libpod/system/df 200 '.ImagesSize=12180391'
# VirtualSize was computed (somehow) in v1.43 so we need to
# build an image to test that the value is returned
# in API <= v1.43.
IIDFILE=$(mktemp)
podman build --iidfile $IIDFILE -<< EOF
FROM $IMAGE
RUN :
EOF
# Test VirtualSize field is present in API v1.43 for system/df (backward compatibility)
t GET /v1.43/system/df 200 \
.Images[0].Size~[0-9]\\+ \
.Images[0].VirtualSize~[0-9]\\+
# Test VirtualSize field is no longer present in API v1.44+ for system/df (deprecated since API v1.43)
t GET /v1.44/system/df 200 \
.Images[0].Size~[0-9]\\+ \
.Images[0].VirtualSize=null
podman rmi -f $(< $IIDFILE)
# Verify that one container references the volume
t GET system/df 200 '.Volumes[0].UsageData.RefCount=1'
@ -91,3 +112,5 @@ t POST 'libpod/system/prune?volumes=true&filters={"label":["testlabel1"]}' param
t POST 'libpod/system/prune?volumes=true' params='' 200 .VolumePruneReports[0].Id=foo1
# TODO add other system prune tests for pods / images
# vim: filetype=sh

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