podman manifest add: support creating artifact manifest on the fly

Add a --artifact flag to `podman manifest add` which can be used to
create an artifact manifest for one or more files and attach it to a
manifest list.  Corresponding --artifact-type, --artifact-config-type,
--artifact-config, --artifact-layer-type, --artifact-subject, and
--artifact-exclude-titles options can be used to fine-tune the fields in
the artifact manifest that don't refer to the files themselves.

Add a --index option to `podman manifest annotate` that will cause
values passed to the --annotation flag to be applied to the manifest
list as a whole instead of to an entry in the list.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
This commit is contained in:
Nalin Dahyabhai 2024-02-14 10:03:07 -05:00
parent 2bbed8f200
commit f168b3c115
19 changed files with 1578 additions and 181 deletions

View File

@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"os"
"strings"
"github.com/containers/common/pkg/auth"
"github.com/containers/common/pkg/completion"
@ -12,25 +14,30 @@ import (
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/util"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
)
// manifestAddOptsWrapper wraps entities.ManifestAddOptions and prevents leaking
// CLI-only fields into the API types.
// manifestAddOptsWrapper wraps entities.ManifestAddOptions and prevents
// leaking CLI-only fields into the API types.
type manifestAddOptsWrapper struct {
entities.ManifestAddOptions
artifactOptions entities.ManifestAddArtifactOptions
TLSVerifyCLI bool // CLI only
Insecure bool // CLI only
CredentialsCLI string
tlsVerifyCLI bool // CLI only
insecure bool // CLI only
credentialsCLI string // CLI only
artifact bool // CLI only
artifactConfigFile string // CLI only
artifactType string // CLI only
}
var (
manifestAddOpts = manifestAddOptsWrapper{}
addCmd = &cobra.Command{
Use: "add [options] LIST IMAGE [IMAGE...]",
Short: "Add images to a manifest list or image index",
Long: "Adds an image to a manifest list or image index.",
Use: "add [options] LIST IMAGEORARTIFACT [IMAGEORARTIFACT...]",
Short: "Add images or artifacts to a manifest list or image index",
Long: "Adds an image or artifact to a manifest list or image index.",
RunE: add,
Args: cobra.MinimumNArgs(2),
ValidArgsFunction: common.AutocompleteImages,
@ -55,6 +62,32 @@ func init() {
flags.StringVar(&manifestAddOpts.Arch, archFlagName, "", "override the `architecture` of the specified image")
_ = addCmd.RegisterFlagCompletionFunc(archFlagName, completion.AutocompleteArch)
artifactFlagName := "artifact"
flags.BoolVar(&manifestAddOpts.artifact, artifactFlagName, false, "add all arguments as artifact files rather than as images")
artifactExcludeTitlesFlagName := "artifact-exclude-titles"
flags.BoolVar(&manifestAddOpts.artifactOptions.ExcludeTitles, artifactExcludeTitlesFlagName, false, fmt.Sprintf(`refrain from setting %q annotations on "layers"`, imgspecv1.AnnotationTitle))
artifactTypeFlagName := "artifact-type"
flags.StringVar(&manifestAddOpts.artifactType, artifactTypeFlagName, "", "override the artifactType value")
_ = addCmd.RegisterFlagCompletionFunc(artifactTypeFlagName, completion.AutocompleteNone)
artifactConfigFlagName := "artifact-config"
flags.StringVar(&manifestAddOpts.artifactConfigFile, artifactConfigFlagName, "", "artifact configuration file")
_ = addCmd.RegisterFlagCompletionFunc(artifactConfigFlagName, completion.AutocompleteNone)
artifactConfigTypeFlagName := "artifact-config-type"
flags.StringVar(&manifestAddOpts.artifactOptions.ConfigType, artifactConfigTypeFlagName, "", "artifact configuration media type")
_ = addCmd.RegisterFlagCompletionFunc(artifactConfigTypeFlagName, completion.AutocompleteNone)
artifactLayerTypeFlagName := "artifact-layer-type"
flags.StringVar(&manifestAddOpts.artifactOptions.LayerType, artifactLayerTypeFlagName, "", "artifact layer media type")
_ = addCmd.RegisterFlagCompletionFunc(artifactLayerTypeFlagName, completion.AutocompleteNone)
artifactSubjectFlagName := "artifact-subject"
flags.StringVar(&manifestAddOpts.IndexSubject, artifactSubjectFlagName, "", "artifact subject reference")
_ = addCmd.RegisterFlagCompletionFunc(artifactSubjectFlagName, completion.AutocompleteNone)
authfileFlagName := "authfile"
flags.StringVar(&manifestAddOpts.Authfile, authfileFlagName, auth.GetDefaultAuthFile(), "path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override")
_ = addCmd.RegisterFlagCompletionFunc(authfileFlagName, completion.AutocompleteDefault)
@ -64,7 +97,7 @@ func init() {
_ = addCmd.RegisterFlagCompletionFunc(certDirFlagName, completion.AutocompleteDefault)
credsFlagName := "creds"
flags.StringVar(&manifestAddOpts.CredentialsCLI, credsFlagName, "", "use `[username[:password]]` for accessing the registry")
flags.StringVar(&manifestAddOpts.credentialsCLI, credsFlagName, "", "use `[username[:password]]` for accessing the registry")
_ = addCmd.RegisterFlagCompletionFunc(credsFlagName, completion.AutocompleteNone)
featuresFlagName := "features"
@ -79,9 +112,9 @@ func init() {
flags.StringVar(&manifestAddOpts.OSVersion, osVersionFlagName, "", "override the OS `version` of the specified image")
_ = addCmd.RegisterFlagCompletionFunc(osVersionFlagName, completion.AutocompleteNone)
flags.BoolVar(&manifestAddOpts.Insecure, "insecure", false, "neither require HTTPS nor verify certificates when accessing the registry")
flags.BoolVar(&manifestAddOpts.insecure, "insecure", false, "neither require HTTPS nor verify certificates when accessing the registry")
_ = flags.MarkHidden("insecure")
flags.BoolVar(&manifestAddOpts.TLSVerifyCLI, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry")
flags.BoolVar(&manifestAddOpts.tlsVerifyCLI, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry")
variantFlagName := "variant"
flags.StringVar(&manifestAddOpts.Variant, variantFlagName, "", "override the `Variant` of the specified image")
@ -99,8 +132,8 @@ func add(cmd *cobra.Command, args []string) error {
}
}
if manifestAddOpts.CredentialsCLI != "" {
creds, err := util.ParseRegistryCreds(manifestAddOpts.CredentialsCLI)
if manifestAddOpts.credentialsCLI != "" {
creds, err := util.ParseRegistryCreds(manifestAddOpts.credentialsCLI)
if err != nil {
return err
}
@ -113,19 +146,54 @@ func add(cmd *cobra.Command, args []string) error {
// which is important to implement a sane way of dealing with defaults of
// boolean CLI flags.
if cmd.Flags().Changed("tls-verify") {
manifestAddOpts.SkipTLSVerify = types.NewOptionalBool(!manifestAddOpts.TLSVerifyCLI)
manifestAddOpts.SkipTLSVerify = types.NewOptionalBool(!manifestAddOpts.tlsVerifyCLI)
}
if cmd.Flags().Changed("insecure") {
if manifestAddOpts.SkipTLSVerify != types.OptionalBoolUndefined {
return errors.New("--insecure may not be used with --tls-verify")
}
manifestAddOpts.SkipTLSVerify = types.NewOptionalBool(manifestAddOpts.Insecure)
manifestAddOpts.SkipTLSVerify = types.NewOptionalBool(manifestAddOpts.insecure)
}
listID, err := registry.ImageEngine().ManifestAdd(context.Background(), args[0], args[1:], manifestAddOpts.ManifestAddOptions)
if !manifestAddOpts.artifact {
var changedArtifactFlags []string
for _, artifactOption := range []string{"artifact-type", "artifact-config", "artifact-config-type", "artifact-layer-type", "artifact-subject", "artifact-exclude-titles"} {
if cmd.Flags().Changed(artifactOption) {
changedArtifactFlags = append(changedArtifactFlags, "--"+artifactOption)
}
}
switch {
case len(changedArtifactFlags) == 1:
return fmt.Errorf("%s requires --artifact", changedArtifactFlags[0])
case len(changedArtifactFlags) > 1:
return fmt.Errorf("%s require --artifact", strings.Join(changedArtifactFlags, "/"))
}
}
var listID string
var err error
if manifestAddOpts.artifact {
if cmd.Flags().Changed("artifact-type") {
manifestAddOpts.artifactOptions.Type = &manifestAddOpts.artifactType
}
if manifestAddOpts.artifactConfigFile != "" {
configBytes, err := os.ReadFile(manifestAddOpts.artifactConfigFile)
if err != nil {
return fmt.Errorf("%v", err)
}
manifestAddOpts.artifactOptions.Config = string(configBytes)
}
manifestAddOpts.artifactOptions.ManifestAnnotateOptions = manifestAddOpts.ManifestAnnotateOptions
listID, err = registry.ImageEngine().ManifestAddArtifact(context.Background(), args[0], args[1:], manifestAddOpts.artifactOptions)
if err != nil {
return err
}
} else {
listID, err = registry.ImageEngine().ManifestAdd(context.Background(), args[0], args[1:], manifestAddOpts.ManifestAddOptions)
if err != nil {
return err
}
}
fmt.Println(listID)
return nil
}

View File

@ -1,7 +1,9 @@
package manifest
import (
"errors"
"fmt"
"strings"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
@ -10,14 +12,22 @@ import (
"github.com/spf13/cobra"
)
// manifestAnnotateOptsWrapper wraps entities.ManifestAnnotateOptions and
// prevents us from having to add CLI-only fields to the API types.
type manifestAnnotateOptsWrapper struct {
entities.ManifestAnnotateOptions
annotations []string
index bool
}
var (
manifestAnnotateOpts = entities.ManifestAnnotateOptions{}
manifestAnnotateOpts = manifestAnnotateOptsWrapper{}
annotateCmd = &cobra.Command{
Use: "annotate [options] LIST IMAGE",
Use: "annotate [options] LIST IMAGEORARTIFACT",
Short: "Add or update information about an entry in a manifest list or image index",
Long: "Adds or updates information about an entry in a manifest list or image index.",
RunE: annotate,
Args: cobra.ExactArgs(2),
Args: cobra.RangeArgs(1, 2),
Example: `podman manifest annotate --annotation left=right mylist:v1.11 sha256:15352d97781ffdf357bf3459c037be3efac4133dc9070c2dce7eca7c05c3e736`,
ValidArgsFunction: common.AutocompleteImages,
}
@ -31,36 +41,85 @@ func init() {
flags := annotateCmd.Flags()
annotationFlagName := "annotation"
flags.StringArrayVar(&manifestAnnotateOpts.Annotation, annotationFlagName, nil, "set an `annotation` for the specified image")
flags.StringArrayVar(&manifestAnnotateOpts.annotations, annotationFlagName, nil, "set an `annotation` for the specified image or artifact")
_ = annotateCmd.RegisterFlagCompletionFunc(annotationFlagName, completion.AutocompleteNone)
archFlagName := "arch"
flags.StringVar(&manifestAnnotateOpts.Arch, archFlagName, "", "override the `architecture` of the specified image")
flags.StringVar(&manifestAnnotateOpts.Arch, archFlagName, "", "override the `architecture` of the specified image or artifact")
_ = annotateCmd.RegisterFlagCompletionFunc(archFlagName, completion.AutocompleteArch)
featuresFlagName := "features"
flags.StringSliceVar(&manifestAnnotateOpts.Features, featuresFlagName, nil, "override the `features` of the specified image")
flags.StringSliceVar(&manifestAnnotateOpts.Features, featuresFlagName, nil, "override the `features` of the specified image or artifact")
_ = annotateCmd.RegisterFlagCompletionFunc(featuresFlagName, completion.AutocompleteNone)
indexFlagName := "index"
flags.BoolVar(&manifestAnnotateOpts.index, indexFlagName, false, "apply --"+annotationFlagName+" values to the image index itself")
osFlagName := "os"
flags.StringVar(&manifestAnnotateOpts.OS, osFlagName, "", "override the `OS` of the specified image")
flags.StringVar(&manifestAnnotateOpts.OS, osFlagName, "", "override the `OS` of the specified image or artifact")
_ = annotateCmd.RegisterFlagCompletionFunc(osFlagName, completion.AutocompleteOS)
osFeaturesFlagName := "os-features"
flags.StringSliceVar(&manifestAnnotateOpts.OSFeatures, osFeaturesFlagName, nil, "override the OS `features` of the specified image")
flags.StringSliceVar(&manifestAnnotateOpts.OSFeatures, osFeaturesFlagName, nil, "override the OS `features` of the specified image or artifact")
_ = annotateCmd.RegisterFlagCompletionFunc(osFeaturesFlagName, completion.AutocompleteNone)
osVersionFlagName := "os-version"
flags.StringVar(&manifestAnnotateOpts.OSVersion, osVersionFlagName, "", "override the OS `version` of the specified image")
flags.StringVar(&manifestAnnotateOpts.OSVersion, osVersionFlagName, "", "override the OS `version` of the specified image or artifact")
_ = annotateCmd.RegisterFlagCompletionFunc(osVersionFlagName, completion.AutocompleteNone)
variantFlagName := "variant"
flags.StringVar(&manifestAnnotateOpts.Variant, variantFlagName, "", "override the `Variant` of the specified image")
flags.StringVar(&manifestAnnotateOpts.Variant, variantFlagName, "", "override the `Variant` of the specified image or artifact")
_ = annotateCmd.RegisterFlagCompletionFunc(variantFlagName, completion.AutocompleteNone)
subjectFlagName := "subject"
flags.StringVar(&manifestAnnotateOpts.IndexSubject, subjectFlagName, "", "set the `subject` to which the image index refers")
_ = annotateCmd.RegisterFlagCompletionFunc(subjectFlagName, completion.AutocompleteNone)
}
func annotate(cmd *cobra.Command, args []string) error {
id, err := registry.ImageEngine().ManifestAnnotate(registry.Context(), args[0], args[1], manifestAnnotateOpts)
var listImageSpec, instanceSpec string
switch len(args) {
case 1:
listImageSpec = args[0]
if listImageSpec == "" {
return fmt.Errorf(`invalid image name "%s"`, args[0])
}
if !manifestAnnotateOpts.index {
return errors.New(`expected an instance digest, image name, or artifact name`)
}
case 2:
listImageSpec = args[0]
if listImageSpec == "" {
return fmt.Errorf(`invalid image name "%s"`, args[0])
}
if manifestAnnotateOpts.index {
return fmt.Errorf(`did not expect image or artifact name "%s" when modifying the entire index`, args[1])
}
instanceSpec = args[1]
if instanceSpec == "" {
return fmt.Errorf(`invalid instance digest, image name, or artifact name "%s"`, instanceSpec)
}
default:
return errors.New("expected either a list name and --index or a list name and an image digest or image name or artifact name")
}
opts := manifestAnnotateOpts.ManifestAnnotateOptions
var annotations map[string]string
for _, annotation := range manifestAnnotateOpts.annotations {
k, v, parsed := strings.Cut(annotation, "=")
if !parsed {
return fmt.Errorf("expected --annotation %q to be in key=value format", annotation)
}
if annotations == nil {
annotations = make(map[string]string)
}
annotations[k] = v
}
if manifestAnnotateOpts.index {
opts.IndexAnnotations = annotations
} else {
opts.Annotations = annotations
}
id, err := registry.ImageEngine().ManifestAnnotate(registry.Context(), args[0], args[1], opts)
if err != nil {
return err
}

View File

@ -3,7 +3,9 @@ package manifest
import (
"errors"
"fmt"
"strings"
"github.com/containers/common/pkg/completion"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
@ -15,8 +17,8 @@ import (
// CLI-only fields into the API types.
type manifestCreateOptsWrapper struct {
entities.ManifestCreateOptions
TLSVerifyCLI, Insecure bool // CLI only
annotations []string // CLI only
tlsVerifyCLI, insecure bool // CLI only
}
var (
@ -43,9 +45,11 @@ func init() {
flags := createCmd.Flags()
flags.BoolVar(&manifestCreateOpts.All, "all", false, "add all of the lists' images if the images to add are lists")
flags.BoolVarP(&manifestCreateOpts.Amend, "amend", "a", false, "modify an existing list if one with the desired name already exists")
flags.BoolVar(&manifestCreateOpts.Insecure, "insecure", false, "neither require HTTPS nor verify certificates when accessing the registry")
flags.BoolVar(&manifestCreateOpts.insecure, "insecure", false, "neither require HTTPS nor verify certificates when accessing the registry")
flags.StringArrayVar(&manifestCreateOpts.annotations, "annotation", nil, "set annotations on the new list")
_ = createCmd.RegisterFlagCompletionFunc("annotation", completion.AutocompleteNone)
_ = flags.MarkHidden("insecure")
flags.BoolVar(&manifestCreateOpts.TLSVerifyCLI, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry")
flags.BoolVar(&manifestCreateOpts.tlsVerifyCLI, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry")
}
func create(cmd *cobra.Command, args []string) error {
@ -54,13 +58,23 @@ func create(cmd *cobra.Command, args []string) error {
// which is important to implement a sane way of dealing with defaults of
// boolean CLI flags.
if cmd.Flags().Changed("tls-verify") {
manifestCreateOpts.SkipTLSVerify = types.NewOptionalBool(!manifestCreateOpts.TLSVerifyCLI)
manifestCreateOpts.SkipTLSVerify = types.NewOptionalBool(!manifestCreateOpts.tlsVerifyCLI)
}
if cmd.Flags().Changed("insecure") {
if manifestCreateOpts.SkipTLSVerify != types.OptionalBoolUndefined {
return errors.New("--insecure may not be used with --tls-verify")
}
manifestCreateOpts.SkipTLSVerify = types.NewOptionalBool(manifestCreateOpts.Insecure)
manifestCreateOpts.SkipTLSVerify = types.NewOptionalBool(manifestCreateOpts.insecure)
}
for _, annotation := range manifestCreateOpts.annotations {
k, v, parsed := strings.Cut(annotation, "=")
if !parsed {
return fmt.Errorf("expected --annotation %q to be in key=value format", annotation)
}
if manifestCreateOpts.Annotations == nil {
manifestCreateOpts.Annotations = make(map[string]string)
}
manifestCreateOpts.Annotations[k] = v
}
imageID, err := registry.ImageEngine().ManifestCreate(registry.Context(), args[0], args[1:], manifestCreateOpts.ManifestCreateOptions)

View File

@ -8,8 +8,12 @@ import (
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/containers/common/libimage/define"
"github.com/containers/image/v5/docker/reference"
@ -23,12 +27,12 @@ import (
"github.com/containers/podman/v5/pkg/channel"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/domain/infra/abi"
envLib "github.com/containers/podman/v5/pkg/env"
"github.com/containers/podman/v5/pkg/errorhandling"
"github.com/gorilla/mux"
"github.com/gorilla/schema"
"github.com/opencontainers/go-digest"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
)
func ManifestCreate(w http.ResponseWriter, r *http.Request) {
@ -39,6 +43,8 @@ func ManifestCreate(w http.ResponseWriter, r *http.Request) {
Images []string `schema:"images"`
All bool `schema:"all"`
Amend bool `schema:"amend"`
Annotation []string `schema:"annotation"`
Annotations map[string]string `schema:"annotations"`
}{
// Add defaults here once needed.
}
@ -73,7 +79,21 @@ func ManifestCreate(w http.ResponseWriter, r *http.Request) {
imageEngine := abi.ImageEngine{Libpod: runtime}
createOptions := entities.ManifestCreateOptions{All: query.All, Amend: query.Amend}
annotations := maps.Clone(query.Annotations)
for _, annotation := range query.Annotation {
k, v, ok := strings.Cut(annotation, "=")
if !ok {
utils.Error(w, http.StatusBadRequest,
fmt.Errorf("invalid annotation %s", annotation))
return
}
if annotations == nil {
annotations = make(map[string]string)
}
annotations[k] = v
}
createOptions := entities.ManifestCreateOptions{All: query.All, Amend: query.Amend, Annotations: annotations}
manID, err := imageEngine.ManifestCreate(r.Context(), query.Name, query.Images, createOptions)
if err != nil {
utils.InternalServerError(w, err)
@ -99,26 +119,29 @@ func ManifestCreate(w http.ResponseWriter, r *http.Request) {
body := new(entities.ManifestModifyOptions)
if err := json.Unmarshal(buffer, body); err != nil {
utils.InternalServerError(w, fmt.Errorf("Decode(): %w", err))
utils.InternalServerError(w, fmt.Errorf("decoding modifications in request: %w", err))
return
}
// gather all images for manifest list
var images []string
if len(query.Images) > 0 {
images = query.Images
if len(body.IndexAnnotation) != 0 || len(body.IndexAnnotations) != 0 || body.IndexSubject != "" {
manifestAnnotateOptions := entities.ManifestAnnotateOptions{
IndexAnnotation: body.IndexAnnotation,
IndexAnnotations: body.IndexAnnotations,
IndexSubject: body.IndexSubject,
}
if len(body.Images) > 0 {
images = body.Images
}
id, err := imageEngine.ManifestAdd(r.Context(), query.Name, images, body.ManifestAddOptions)
if err != nil {
if _, err := imageEngine.ManifestAnnotate(r.Context(), manID, "", manifestAnnotateOptions); err != nil {
utils.InternalServerError(w, err)
return
}
}
if len(body.Images) > 0 {
if _, err := imageEngine.ManifestAdd(r.Context(), manID, body.Images, body.ManifestAddOptions); err != nil {
utils.InternalServerError(w, err)
return
}
}
utils.WriteResponse(w, status, entities.IDResponse{ID: id})
utils.WriteResponse(w, status, entities.IDResponse{ID: manID})
}
// ManifestExists return true if manifest list exists.
@ -194,7 +217,7 @@ func ManifestAddV3(w http.ResponseWriter, r *http.Request) {
TLSVerify bool `schema:"tlsVerify"`
}{}
if err := json.NewDecoder(r.Body).Decode(&query); err != nil {
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("Decode(): %w", err))
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("decoding AddV3 query: %w", err))
return
}
@ -471,33 +494,137 @@ func ManifestModify(w http.ResponseWriter, r *http.Request) {
imageEngine := abi.ImageEngine{Libpod: runtime}
body := new(entities.ManifestModifyOptions)
multireader, err := r.MultipartReader()
if err != nil {
multireader = nil
// not multipart - request is just encoded JSON, nothing else
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("Decode(): %w", err))
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("decoding modify request: %w", err))
return
}
} else {
// multipart - request is encoded JSON in the first part, each artifact is its own part
bodyPart, err := multireader.NextPart()
if err != nil {
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("reading first part of multipart request: %w", err))
return
}
err = json.NewDecoder(bodyPart).Decode(body)
bodyPart.Close()
if err != nil {
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("decoding modify request in multipart request: %w", err))
return
}
}
name := utils.GetName(r)
if _, err := runtime.LibimageRuntime().LookupManifestList(name); err != nil {
manifestList, err := runtime.LibimageRuntime().LookupManifestList(name)
if err != nil {
utils.Error(w, http.StatusNotFound, err)
return
}
annotationsFromAnnotationSlice := func(annotation []string) map[string]string {
annotations := make(map[string]string)
for _, annotationSpec := range annotation {
key, val, hasVal := strings.Cut(annotationSpec, "=")
if !hasVal {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("no value given for annotation %q", key))
return nil
}
annotations[key] = val
}
return annotations
}
if len(body.ManifestAddOptions.Annotation) != 0 {
if len(body.ManifestAddOptions.Annotations) != 0 {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("can not set both Annotation and Annotations"))
return
}
annotations := make(map[string]string)
for _, annotationSpec := range body.ManifestAddOptions.Annotation {
key, val, hasVal := strings.Cut(annotationSpec, "=")
if !hasVal {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("no value given for annotation %q", key))
body.ManifestAddOptions.Annotations = annotationsFromAnnotationSlice(body.ManifestAddOptions.Annotation)
body.ManifestAddOptions.Annotation = nil
}
if len(body.ManifestAddOptions.IndexAnnotation) != 0 {
if len(body.ManifestAddOptions.IndexAnnotations) != 0 {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("can not set both IndexAnnotation and IndexAnnotations"))
return
}
annotations[key] = val
body.ManifestAddOptions.IndexAnnotations = annotationsFromAnnotationSlice(body.ManifestAddOptions.IndexAnnotation)
body.ManifestAddOptions.IndexAnnotation = nil
}
body.ManifestAddOptions.Annotations = envLib.Join(body.ManifestAddOptions.Annotations, annotations)
body.ManifestAddOptions.Annotation = nil
var artifactExtractionError error
var artifactExtraction sync.WaitGroup
if multireader != nil {
// If the data was multipart, then save items from it into a
// directory that will be removed along with this list,
// whenever that happens.
artifactExtraction.Add(1)
go func() {
defer artifactExtraction.Done()
storageConfig := runtime.StorageConfig()
// FIXME: knowing that this is the location of the
// per-image-record-stuff directory is a little too
// "inside storage"
fileDir, err := os.MkdirTemp(filepath.Join(runtime.GraphRoot(), storageConfig.GraphDriverName+"-images", manifestList.ID()), "")
if err != nil {
artifactExtractionError = err
return
}
// We'll be building a list of the names of files we
// received as part of the request and setting it in
// the request body before we're done.
var contentFiles []string
part, err := multireader.NextPart()
if err != nil {
artifactExtractionError = err
return
}
for part != nil {
partName := part.FormName()
if filename := part.FileName(); filename != "" {
partName = filename
}
if partName != "" {
partName = path.Base(partName)
}
// Write the file in a scope that lets us close it as quickly
// as possible.
if err = func() error {
defer part.Close()
var f *os.File
// Create the file.
if partName != "" {
// Try to use the supplied name.
f, err = os.OpenFile(filepath.Join(fileDir, partName), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
} else {
// No supplied name means they don't care.
f, err = os.CreateTemp(fileDir, "upload")
}
if err != nil {
return err
}
defer f.Close()
// Write the file's contents.
if _, err = io.Copy(f, part); err != nil {
return err
}
contentFiles = append(contentFiles, f.Name())
return nil
}(); err != nil {
break
}
part, err = multireader.NextPart()
}
// If we stowed all of the uploaded files without issue, we're all good.
if err != nil && !errors.Is(err, io.EOF) {
artifactExtractionError = err
return
}
// Save the list of files that we created.
body.ArtifactFiles = contentFiles
}()
}
if tlsVerify, ok := r.URL.Query()["tlsVerify"]; ok {
@ -527,17 +654,50 @@ func ManifestModify(w http.ResponseWriter, r *http.Request) {
body.ManifestAddOptions.CertDir = sys.DockerCertPath
}
var report entities.ManifestModifyReport
report := entities.ManifestModifyReport{ID: manifestList.ID()}
switch {
case strings.EqualFold("update", body.Operation):
if len(body.Images) > 0 {
id, err := imageEngine.ManifestAdd(r.Context(), name, body.Images, body.ManifestAddOptions)
if err != nil {
report.Errors = append(report.Errors, err)
break
}
report = entities.ManifestModifyReport{
ID: id,
Images: body.Images,
report.ID = id
report.Images = body.Images
}
if multireader != nil {
// Wait for the extraction goroutine to finish
// processing the message in the request body, so that
// we know whether or not everything looked alright.
artifactExtraction.Wait()
if artifactExtractionError != nil {
report.Errors = append(report.Errors, artifactExtractionError)
artifactExtractionError = nil
break
}
// Reconstruct a ManifestAddArtifactOptions from the corresponding
// fields in the entities.ManifestModifyOptions that we decoded
// the request struct into and then supplemented with the files list.
// We waited until after the extraction goroutine finished to ensure
// that we'd pick up its changes to the ArtifactFiles list.
manifestAddArtifactOptions := entities.ManifestAddArtifactOptions{
Type: body.ArtifactType,
LayerType: body.ArtifactLayerType,
ConfigType: body.ArtifactConfigType,
Config: body.ArtifactConfig,
ExcludeTitles: body.ArtifactExcludeTitles,
Annotations: body.ArtifactAnnotations,
Subject: body.ArtifactSubject,
Files: body.ArtifactFiles,
}
id, err := imageEngine.ManifestAddArtifact(r.Context(), name, body.ArtifactFiles, manifestAddArtifactOptions)
if err != nil {
report.Errors = append(report.Errors, err)
break
}
report.ID = id
report.Files = body.ArtifactFiles
}
case strings.EqualFold("remove", body.Operation):
for _, image := range body.Images {
@ -550,15 +710,7 @@ func ManifestModify(w http.ResponseWriter, r *http.Request) {
report.Images = append(report.Images, image)
}
case strings.EqualFold("annotate", body.Operation):
options := entities.ManifestAnnotateOptions{
Annotations: body.Annotations,
Arch: body.Arch,
Features: body.Features,
OS: body.OS,
OSFeatures: body.OSFeatures,
OSVersion: body.OSVersion,
Variant: body.Variant,
}
options := body.ManifestAnnotateOptions
for _, image := range body.Images {
id, err := imageEngine.ManifestAnnotate(r.Context(), name, image, options)
if err != nil {
@ -573,6 +725,13 @@ func ManifestModify(w http.ResponseWriter, r *http.Request) {
return
}
// In case something weird happened, don't just let the goroutine go; make the
// client at least wait for it.
artifactExtraction.Wait()
if artifactExtractionError != nil {
report.Errors = append(report.Errors, artifactExtractionError)
}
statusCode := http.StatusOK
switch {
case len(report.Errors) > 0 && len(report.Images) > 0:

View File

@ -6,10 +6,14 @@ import (
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/textproto"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/containers/common/libimage/define"
"github.com/containers/image/v5/manifest"
@ -21,6 +25,7 @@ import (
"github.com/containers/podman/v5/pkg/errorhandling"
dockerAPI "github.com/docker/docker/api/types"
jsoniter "github.com/json-iterator/go"
"golang.org/x/exp/slices"
)
// Create creates a manifest for the given name. Optional images to be associated with
@ -160,7 +165,7 @@ func Add(ctx context.Context, name string, options *AddOptions) (string, error)
Features: options.Features,
Images: options.Images,
OS: options.OS,
OSFeatures: nil,
OSFeatures: options.OSFeatures,
OSVersion: options.OSVersion,
Variant: options.Variant,
Username: options.Username,
@ -172,6 +177,37 @@ func Add(ctx context.Context, name string, options *AddOptions) (string, error)
return Modify(ctx, name, options.Images, &optionsv4)
}
// AddArtifact creates an artifact manifest and adds it to a given manifest
// list. Additional options for the manifest can also be specified. The ID of
// the new manifest list is returned as a string
func AddArtifact(ctx context.Context, name string, options *AddArtifactOptions) (string, error) {
if options == nil {
options = new(AddArtifactOptions)
}
optionsv4 := ModifyOptions{
Annotations: options.Annotation,
Arch: options.Arch,
Features: options.Features,
OS: options.OS,
OSFeatures: options.OSFeatures,
OSVersion: options.OSVersion,
Variant: options.Variant,
ArtifactType: options.Type,
ArtifactConfigType: options.ConfigType,
ArtifactLayerType: options.LayerType,
ArtifactConfig: options.Config,
ArtifactExcludeTitles: options.ExcludeTitles,
ArtifactSubject: options.Subject,
ArtifactAnnotations: options.Annotations,
}
if len(options.Files) > 0 {
optionsv4.WithArtifactFiles(options.Files)
}
optionsv4.WithOperation("update")
return Modify(ctx, name, nil, &optionsv4)
}
// Remove deletes a manifest entry from a manifest list. Both name and the digest to be
// removed are mandatory inputs. The ID of the new manifest list is returned as a string.
func Remove(ctx context.Context, name, digest string, _ *RemoveOptions) (string, error) {
@ -284,6 +320,16 @@ func Modify(ctx context.Context, name string, images []string, options *ModifyOp
}
options.WithImages(images)
var artifactFiles, artifactBaseNames []string
if options.ArtifactFiles != nil && len(*options.ArtifactFiles) > 0 {
artifactFiles = slices.Clone(*options.ArtifactFiles)
artifactBaseNames = make([]string, 0, len(artifactFiles))
for _, filename := range artifactFiles {
artifactBaseNames = append(artifactBaseNames, filepath.Base(filename))
}
options.ArtifactFiles = &artifactBaseNames
}
conn, err := bindings.GetClient(ctx)
if err != nil {
return "", err
@ -292,12 +338,81 @@ func Modify(ctx context.Context, name string, images []string, options *ModifyOp
if err != nil {
return "", err
}
reader := strings.NewReader(opts)
reader := io.Reader(strings.NewReader(opts))
if options.Body != nil {
reader = io.MultiReader(reader, *options.Body)
}
var artifactContentType string
var artifactWriterGroup sync.WaitGroup
var artifactWriterError error
if len(artifactFiles) > 0 {
// get ready to upload the passed-in files
bodyReader, bodyWriter := io.Pipe()
defer bodyReader.Close()
requestBodyReader := reader
reader = bodyReader
// upload the files in another goroutine
writer := multipart.NewWriter(bodyWriter)
artifactContentType = writer.FormDataContentType()
artifactWriterGroup.Add(1)
go func() {
defer bodyWriter.Close()
defer writer.Close()
// start with the body we would have uploaded if we weren't
// attaching artifacts
headers := textproto.MIMEHeader{
"Content-Type": []string{"application/json"},
}
requestPartWriter, err := writer.CreatePart(headers)
if err != nil {
artifactWriterError = fmt.Errorf("creating form part for request: %v", err)
return
}
if _, err := io.Copy(requestPartWriter, requestBodyReader); err != nil {
artifactWriterError = fmt.Errorf("uploading request as form part: %v", err)
return
}
// now walk the list of files we're attaching
for _, file := range artifactFiles {
if err := func() error {
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close()
fileBase := filepath.Base(file)
formFile, err := writer.CreateFormFile(fileBase, fileBase)
if err != nil {
return err
}
st, err := f.Stat()
if err != nil {
return err
}
// upload the file contents
n, err := io.Copy(formFile, f)
if err != nil {
return fmt.Errorf("uploading contents of artifact file %s: %w", filepath.Base(file), err)
}
if n != st.Size() {
return fmt.Errorf("short write while uploading contents of artifact file %s: %d != %d", filepath.Base(file), n, st.Size())
}
return nil
}(); err != nil {
artifactWriterError = err
break
}
}
}()
}
header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, options.GetUsername(), options.GetPassword())
if err != nil {
return "", err
}
if artifactContentType != "" {
header["Content-Type"] = []string{artifactContentType}
}
params, err := options.ToParams()
if err != nil {
@ -315,6 +430,11 @@ func Modify(ctx context.Context, name string, images []string, options *ModifyOp
}
defer response.Body.Close()
artifactWriterGroup.Wait()
if artifactWriterError != nil {
return "", fmt.Errorf("uploading artifacts: %w", err)
}
data, err := io.ReadAll(response.Body)
if err != nil {
return "", fmt.Errorf("unable to process API response: %w", err)

View File

@ -1,5 +1,7 @@
package manifests
import "io"
// InspectOptions are optional options for inspecting manifests
//
//go:generate go run ../generator/generator.go InspectOptions
@ -17,6 +19,7 @@ type InspectOptions struct {
type CreateOptions struct {
All *bool
Amend *bool
Annotation map[string]string
}
// ExistsOptions are optional options for checking
@ -31,19 +34,44 @@ type ExistsOptions struct {
//go:generate go run ../generator/generator.go AddOptions
type AddOptions struct {
All *bool
Annotation map[string]string
Arch *string
Features []string
Images []string
OS *string
OSVersion *string
OSFeatures []string
Variant *string
Images []string
Authfile *string
Password *string
Username *string
SkipTLSVerify *bool `schema:"-"`
}
// AddArtifactOptions are optional options for adding artifact manifests
//
//go:generate go run ../generator/generator.go AddArtifactOptions
type AddArtifactOptions struct {
Annotation map[string]string
Arch *string
Features []string
OS *string
OSVersion *string
OSFeatures []string
Variant *string
Type **string `json:"artifact_type,omitempty"`
ConfigType *string `json:"artifact_config_type,omitempty"`
Config *string `json:"artifact_config,omitempty"`
LayerType *string `json:"artifact_layer_type,omitempty"`
ExcludeTitles *bool `json:"artifact_exclude_titles,omitempty"`
Subject *string `json:"artifact_subject,omitempty"`
Annotations map[string]string `json:"artifact_annotations,omitempty"`
Files []string `json:"artifact_files,omitempty"`
}
// RemoveOptions are optional options for removing manifest lists
//
//go:generate go run ../generator/generator.go RemoveOptions
@ -58,18 +86,28 @@ type ModifyOptions struct {
// efficiently perform each update on a manifest list.
Operation *string
All *bool // All when true, operate on all images in a manifest list that may be included in Images
Annotations map[string]string // Annotations to add to manifest list
Annotations map[string]string // Annotations to add to the entries for Images in the manifest list
Arch *string // Arch overrides the architecture for the image
Features []string // Feature list for the image
Images []string // Images is an optional list of images to add/remove to/from manifest list depending on operation
OS *string // OS overrides the operating system for the image
// OS features for the image
OSFeatures []string `json:"os_features" schema:"os_features"`
// OSVersion overrides the operating system for the image
OSVersion *string `json:"os_version" schema:"os_version"`
Variant *string // Variant overrides the operating system variant for the image
OSFeatures []string `json:"os_features" schema:"os_features"` // OSFeatures overrides the OS features for the image
OSVersion *string `json:"os_version" schema:"os_version"` // OSVersion overrides the operating system version for the image
Variant *string // Variant overrides the architecture variant for the image
Images []string // Images is an optional list of images to add/remove to/from manifest list depending on operation
Authfile *string
Password *string
Username *string
SkipTLSVerify *bool `schema:"-"`
ArtifactType **string `json:"artifact_type"` // the ArtifactType in an artifact manifest being created
ArtifactConfigType *string `json:"artifact_config_type"` // the config.MediaType in an artifact manifest being created
ArtifactConfig *string `json:"artifact_config"` // the config.Data in an artifact manifest being created
ArtifactLayerType *string `json:"artifact_layer_type"` // the MediaType for each layer in an artifact manifest being created
ArtifactExcludeTitles *bool `json:"artifact_exclude_titles"` // whether or not to include title annotations for each layer in an artifact manifest being created
ArtifactSubject *string `json:"artifact_subject"` // subject to set in an artifact manifest being created
ArtifactAnnotations map[string]string `json:"artifact_annotations"` // annotations to add to an artifact manifest being created
ArtifactFiles *[]string `json:"artifact_files"` // an optional list of files to add to a new artifact manifest in the manifest list
Body *io.Reader `json:"-" schema:"-"`
}

View File

@ -77,21 +77,6 @@ func (o *AddOptions) GetFeatures() []string {
return o.Features
}
// WithImages set field Images to given value
func (o *AddOptions) WithImages(value []string) *AddOptions {
o.Images = value
return o
}
// GetImages returns value of field Images
func (o *AddOptions) GetImages() []string {
if o.Images == nil {
var z []string
return z
}
return o.Images
}
// WithOS set field OS to given value
func (o *AddOptions) WithOS(value string) *AddOptions {
o.OS = &value
@ -122,6 +107,21 @@ func (o *AddOptions) GetOSVersion() string {
return *o.OSVersion
}
// WithOSFeatures set field OSFeatures to given value
func (o *AddOptions) WithOSFeatures(value []string) *AddOptions {
o.OSFeatures = value
return o
}
// GetOSFeatures returns value of field OSFeatures
func (o *AddOptions) GetOSFeatures() []string {
if o.OSFeatures == nil {
var z []string
return z
}
return o.OSFeatures
}
// WithVariant set field Variant to given value
func (o *AddOptions) WithVariant(value string) *AddOptions {
o.Variant = &value
@ -137,6 +137,21 @@ func (o *AddOptions) GetVariant() string {
return *o.Variant
}
// WithImages set field Images to given value
func (o *AddOptions) WithImages(value []string) *AddOptions {
o.Images = value
return o
}
// GetImages returns value of field Images
func (o *AddOptions) GetImages() []string {
if o.Images == nil {
var z []string
return z
}
return o.Images
}
// WithAuthfile set field Authfile to given value
func (o *AddOptions) WithAuthfile(value string) *AddOptions {
o.Authfile = &value

View File

@ -0,0 +1,243 @@
// Code generated by go generate; DO NOT EDIT.
package manifests
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *AddArtifactOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *AddArtifactOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}
// WithAnnotation set field Annotation to given value
func (o *AddArtifactOptions) WithAnnotation(value map[string]string) *AddArtifactOptions {
o.Annotation = value
return o
}
// GetAnnotation returns value of field Annotation
func (o *AddArtifactOptions) GetAnnotation() map[string]string {
if o.Annotation == nil {
var z map[string]string
return z
}
return o.Annotation
}
// WithArch set field Arch to given value
func (o *AddArtifactOptions) WithArch(value string) *AddArtifactOptions {
o.Arch = &value
return o
}
// GetArch returns value of field Arch
func (o *AddArtifactOptions) GetArch() string {
if o.Arch == nil {
var z string
return z
}
return *o.Arch
}
// WithFeatures set field Features to given value
func (o *AddArtifactOptions) WithFeatures(value []string) *AddArtifactOptions {
o.Features = value
return o
}
// GetFeatures returns value of field Features
func (o *AddArtifactOptions) GetFeatures() []string {
if o.Features == nil {
var z []string
return z
}
return o.Features
}
// WithOS set field OS to given value
func (o *AddArtifactOptions) WithOS(value string) *AddArtifactOptions {
o.OS = &value
return o
}
// GetOS returns value of field OS
func (o *AddArtifactOptions) GetOS() string {
if o.OS == nil {
var z string
return z
}
return *o.OS
}
// WithOSVersion set field OSVersion to given value
func (o *AddArtifactOptions) WithOSVersion(value string) *AddArtifactOptions {
o.OSVersion = &value
return o
}
// GetOSVersion returns value of field OSVersion
func (o *AddArtifactOptions) GetOSVersion() string {
if o.OSVersion == nil {
var z string
return z
}
return *o.OSVersion
}
// WithOSFeatures set field OSFeatures to given value
func (o *AddArtifactOptions) WithOSFeatures(value []string) *AddArtifactOptions {
o.OSFeatures = value
return o
}
// GetOSFeatures returns value of field OSFeatures
func (o *AddArtifactOptions) GetOSFeatures() []string {
if o.OSFeatures == nil {
var z []string
return z
}
return o.OSFeatures
}
// WithVariant set field Variant to given value
func (o *AddArtifactOptions) WithVariant(value string) *AddArtifactOptions {
o.Variant = &value
return o
}
// GetVariant returns value of field Variant
func (o *AddArtifactOptions) GetVariant() string {
if o.Variant == nil {
var z string
return z
}
return *o.Variant
}
// WithType set field Type to given value
func (o *AddArtifactOptions) WithType(value *string) *AddArtifactOptions {
o.Type = &value
return o
}
// GetType returns value of field Type
func (o *AddArtifactOptions) GetType() *string {
if o.Type == nil {
var z *string
return z
}
return *o.Type
}
// WithConfigType set field ConfigType to given value
func (o *AddArtifactOptions) WithConfigType(value string) *AddArtifactOptions {
o.ConfigType = &value
return o
}
// GetConfigType returns value of field ConfigType
func (o *AddArtifactOptions) GetConfigType() string {
if o.ConfigType == nil {
var z string
return z
}
return *o.ConfigType
}
// WithConfig set field Config to given value
func (o *AddArtifactOptions) WithConfig(value string) *AddArtifactOptions {
o.Config = &value
return o
}
// GetConfig returns value of field Config
func (o *AddArtifactOptions) GetConfig() string {
if o.Config == nil {
var z string
return z
}
return *o.Config
}
// WithLayerType set field LayerType to given value
func (o *AddArtifactOptions) WithLayerType(value string) *AddArtifactOptions {
o.LayerType = &value
return o
}
// GetLayerType returns value of field LayerType
func (o *AddArtifactOptions) GetLayerType() string {
if o.LayerType == nil {
var z string
return z
}
return *o.LayerType
}
// WithExcludeTitles set field ExcludeTitles to given value
func (o *AddArtifactOptions) WithExcludeTitles(value bool) *AddArtifactOptions {
o.ExcludeTitles = &value
return o
}
// GetExcludeTitles returns value of field ExcludeTitles
func (o *AddArtifactOptions) GetExcludeTitles() bool {
if o.ExcludeTitles == nil {
var z bool
return z
}
return *o.ExcludeTitles
}
// WithSubject set field Subject to given value
func (o *AddArtifactOptions) WithSubject(value string) *AddArtifactOptions {
o.Subject = &value
return o
}
// GetSubject returns value of field Subject
func (o *AddArtifactOptions) GetSubject() string {
if o.Subject == nil {
var z string
return z
}
return *o.Subject
}
// WithAnnotations set field Annotations to given value
func (o *AddArtifactOptions) WithAnnotations(value map[string]string) *AddArtifactOptions {
o.Annotations = value
return o
}
// GetAnnotations returns value of field Annotations
func (o *AddArtifactOptions) GetAnnotations() map[string]string {
if o.Annotations == nil {
var z map[string]string
return z
}
return o.Annotations
}
// WithFiles set field Files to given value
func (o *AddArtifactOptions) WithFiles(value []string) *AddArtifactOptions {
o.Files = value
return o
}
// GetFiles returns value of field Files
func (o *AddArtifactOptions) GetFiles() []string {
if o.Files == nil {
var z []string
return z
}
return o.Files
}

View File

@ -46,3 +46,18 @@ func (o *CreateOptions) GetAmend() bool {
}
return *o.Amend
}
// WithAnnotation set field Annotation to given value
func (o *CreateOptions) WithAnnotation(value map[string]string) *CreateOptions {
o.Annotation = value
return o
}
// GetAnnotation returns value of field Annotation
func (o *CreateOptions) GetAnnotation() map[string]string {
if o.Annotation == nil {
var z map[string]string
return z
}
return o.Annotation
}

View File

@ -2,6 +2,7 @@
package manifests
import (
"io"
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
@ -47,13 +48,13 @@ func (o *ModifyOptions) GetAll() bool {
return *o.All
}
// WithAnnotations set annotations to add to manifest list
// WithAnnotations set annotations to add to the entries for Images in the manifest list
func (o *ModifyOptions) WithAnnotations(value map[string]string) *ModifyOptions {
o.Annotations = value
return o
}
// GetAnnotations returns value of annotations to add to manifest list
// GetAnnotations returns value of annotations to add to the entries for Images in the manifest list
func (o *ModifyOptions) GetAnnotations() map[string]string {
if o.Annotations == nil {
var z map[string]string
@ -92,21 +93,6 @@ func (o *ModifyOptions) GetFeatures() []string {
return o.Features
}
// WithImages set images is an optional list of images to add/remove to/from manifest list depending on operation
func (o *ModifyOptions) WithImages(value []string) *ModifyOptions {
o.Images = value
return o
}
// GetImages returns value of images is an optional list of images to add/remove to/from manifest list depending on operation
func (o *ModifyOptions) GetImages() []string {
if o.Images == nil {
var z []string
return z
}
return o.Images
}
// WithOS set oS overrides the operating system for the image
func (o *ModifyOptions) WithOS(value string) *ModifyOptions {
o.OS = &value
@ -122,13 +108,13 @@ func (o *ModifyOptions) GetOS() string {
return *o.OS
}
// WithOSFeatures set field OSFeatures to given value
// WithOSFeatures set oSFeatures overrides the OS features for the image
func (o *ModifyOptions) WithOSFeatures(value []string) *ModifyOptions {
o.OSFeatures = value
return o
}
// GetOSFeatures returns value of field OSFeatures
// GetOSFeatures returns value of oSFeatures overrides the OS features for the image
func (o *ModifyOptions) GetOSFeatures() []string {
if o.OSFeatures == nil {
var z []string
@ -137,13 +123,13 @@ func (o *ModifyOptions) GetOSFeatures() []string {
return o.OSFeatures
}
// WithOSVersion set field OSVersion to given value
// WithOSVersion set oSVersion overrides the operating system version for the image
func (o *ModifyOptions) WithOSVersion(value string) *ModifyOptions {
o.OSVersion = &value
return o
}
// GetOSVersion returns value of field OSVersion
// GetOSVersion returns value of oSVersion overrides the operating system version for the image
func (o *ModifyOptions) GetOSVersion() string {
if o.OSVersion == nil {
var z string
@ -152,13 +138,13 @@ func (o *ModifyOptions) GetOSVersion() string {
return *o.OSVersion
}
// WithVariant set variant overrides the operating system variant for the image
// WithVariant set variant overrides the architecture variant for the image
func (o *ModifyOptions) WithVariant(value string) *ModifyOptions {
o.Variant = &value
return o
}
// GetVariant returns value of variant overrides the operating system variant for the image
// GetVariant returns value of variant overrides the architecture variant for the image
func (o *ModifyOptions) GetVariant() string {
if o.Variant == nil {
var z string
@ -167,6 +153,21 @@ func (o *ModifyOptions) GetVariant() string {
return *o.Variant
}
// WithImages set images is an optional list of images to add/remove to/from manifest list depending on operation
func (o *ModifyOptions) WithImages(value []string) *ModifyOptions {
o.Images = value
return o
}
// GetImages returns value of images is an optional list of images to add/remove to/from manifest list depending on operation
func (o *ModifyOptions) GetImages() []string {
if o.Images == nil {
var z []string
return z
}
return o.Images
}
// WithAuthfile set field Authfile to given value
func (o *ModifyOptions) WithAuthfile(value string) *ModifyOptions {
o.Authfile = &value
@ -226,3 +227,138 @@ func (o *ModifyOptions) GetSkipTLSVerify() bool {
}
return *o.SkipTLSVerify
}
// WithArtifactType set the ArtifactType in an artifact manifest being created
func (o *ModifyOptions) WithArtifactType(value *string) *ModifyOptions {
o.ArtifactType = &value
return o
}
// GetArtifactType returns value of the ArtifactType in an artifact manifest being created
func (o *ModifyOptions) GetArtifactType() *string {
if o.ArtifactType == nil {
var z *string
return z
}
return *o.ArtifactType
}
// WithArtifactConfigType set the config.MediaType in an artifact manifest being created
func (o *ModifyOptions) WithArtifactConfigType(value string) *ModifyOptions {
o.ArtifactConfigType = &value
return o
}
// GetArtifactConfigType returns value of the config.MediaType in an artifact manifest being created
func (o *ModifyOptions) GetArtifactConfigType() string {
if o.ArtifactConfigType == nil {
var z string
return z
}
return *o.ArtifactConfigType
}
// WithArtifactConfig set the config.Data in an artifact manifest being created
func (o *ModifyOptions) WithArtifactConfig(value string) *ModifyOptions {
o.ArtifactConfig = &value
return o
}
// GetArtifactConfig returns value of the config.Data in an artifact manifest being created
func (o *ModifyOptions) GetArtifactConfig() string {
if o.ArtifactConfig == nil {
var z string
return z
}
return *o.ArtifactConfig
}
// WithArtifactLayerType set the MediaType for each layer in an artifact manifest being created
func (o *ModifyOptions) WithArtifactLayerType(value string) *ModifyOptions {
o.ArtifactLayerType = &value
return o
}
// GetArtifactLayerType returns value of the MediaType for each layer in an artifact manifest being created
func (o *ModifyOptions) GetArtifactLayerType() string {
if o.ArtifactLayerType == nil {
var z string
return z
}
return *o.ArtifactLayerType
}
// WithArtifactExcludeTitles set whether or not to include title annotations for each layer in an artifact manifest being created
func (o *ModifyOptions) WithArtifactExcludeTitles(value bool) *ModifyOptions {
o.ArtifactExcludeTitles = &value
return o
}
// GetArtifactExcludeTitles returns value of whether or not to include title annotations for each layer in an artifact manifest being created
func (o *ModifyOptions) GetArtifactExcludeTitles() bool {
if o.ArtifactExcludeTitles == nil {
var z bool
return z
}
return *o.ArtifactExcludeTitles
}
// WithArtifactSubject set subject to set in an artifact manifest being created
func (o *ModifyOptions) WithArtifactSubject(value string) *ModifyOptions {
o.ArtifactSubject = &value
return o
}
// GetArtifactSubject returns value of subject to set in an artifact manifest being created
func (o *ModifyOptions) GetArtifactSubject() string {
if o.ArtifactSubject == nil {
var z string
return z
}
return *o.ArtifactSubject
}
// WithArtifactAnnotations set annotations to add to an artifact manifest being created
func (o *ModifyOptions) WithArtifactAnnotations(value map[string]string) *ModifyOptions {
o.ArtifactAnnotations = value
return o
}
// GetArtifactAnnotations returns value of annotations to add to an artifact manifest being created
func (o *ModifyOptions) GetArtifactAnnotations() map[string]string {
if o.ArtifactAnnotations == nil {
var z map[string]string
return z
}
return o.ArtifactAnnotations
}
// WithArtifactFiles set an optional list of files to add to a new artifact manifest in the manifest list
func (o *ModifyOptions) WithArtifactFiles(value []string) *ModifyOptions {
o.ArtifactFiles = &value
return o
}
// GetArtifactFiles returns value of an optional list of files to add to a new artifact manifest in the manifest list
func (o *ModifyOptions) GetArtifactFiles() []string {
if o.ArtifactFiles == nil {
var z []string
return z
}
return *o.ArtifactFiles
}
// WithBody set field Body to given value
func (o *ModifyOptions) WithBody(value io.Reader) *ModifyOptions {
o.Body = &value
return o
}
// GetBody returns value of field Body
func (o *ModifyOptions) GetBody() io.Reader {
if o.Body == nil {
var z io.Reader
return z
}
return *o.Body
}

View File

@ -36,6 +36,7 @@ type ImageEngine interface { //nolint:interfacebloat
ManifestExists(ctx context.Context, name string) (*BoolReport, error)
ManifestInspect(ctx context.Context, name string, opts ManifestInspectOptions) ([]byte, error)
ManifestAdd(ctx context.Context, listName string, imageNames []string, opts ManifestAddOptions) (string, error)
ManifestAddArtifact(ctx context.Context, name string, files []string, opts ManifestAddArtifactOptions) (string, error)
ManifestAnnotate(ctx context.Context, names, image string, opts ManifestAnnotateOptions) (string, error)
ManifestRemoveDigest(ctx context.Context, names, image string) (string, error)
ManifestRm(ctx context.Context, names []string) (*ImageRemoveReport, []error)

View File

@ -5,7 +5,7 @@ import (
entitiesTypes "github.com/containers/podman/v5/pkg/domain/entities/types"
)
// ManifestCreateOptions provides model for creating manifest
// ManifestCreateOptions provides model for creating manifest list or image index
type ManifestCreateOptions struct {
// True when adding lists to include all images
All bool `schema:"all"`
@ -13,6 +13,8 @@ type ManifestCreateOptions struct {
Amend bool `schema:"amend"`
// Should TLS registry certificate be verified?
SkipTLSVerify types.OptionalBool `json:"-" schema:"-"`
// Annotations to set on the list, which forces it to be OCI format
Annotations map[string]string `json:"annotations" schema:"annotations"`
}
// ManifestInspectOptions provides model for inspecting manifest
@ -40,28 +42,51 @@ type ManifestAddOptions struct {
SkipTLSVerify types.OptionalBool `json:"-" schema:"-"`
// Username to authenticate to registry when pushing manifest list
Username string `json:"-" schema:"-"`
// Images is an optional list of images to add to manifest list
// Images is an optional list of image references to add to manifest list
Images []string `json:"images" schema:"images"`
}
// ManifestAddArtifactOptions provides the model for creating artifact manifests
// for files and adding those manifests to a manifest list
//
// swagger:model
type ManifestAddArtifactOptions struct {
ManifestAnnotateOptions
// Note to future maintainers: keep these fields synchronized with ManifestModifyOptions!
Type *string `json:"artifact_type" schema:"artifact_type"`
LayerType string `json:"artifact_layer_type" schema:"artifact_layer_type"`
ConfigType string `json:"artifact_config_type" schema:"artifact_config_type"`
Config string `json:"artifact_config" schema:"artifact_config"`
ExcludeTitles bool `json:"artifact_exclude_titles" schema:"artifact_exclude_titles"`
Annotations map[string]string `json:"artifact_annotations" schema:"artifact_annotations"`
Subject string `json:"artifact_subject" schema:"artifact_subject"`
Files []string `json:"artifact_files" schema:"-"`
}
// ManifestAnnotateOptions provides model for annotating manifest list
type ManifestAnnotateOptions struct {
// Annotation to add to manifest list
// Annotation to add to the item in the manifest list
Annotation []string `json:"annotation" schema:"annotation"`
// Annotations to add to manifest list by a map which is preferred over Annotation
// Annotations to add to the item in the manifest list by a map which is preferred over Annotation
Annotations map[string]string `json:"annotations" schema:"annotations"`
// Arch overrides the architecture for the image
// Arch overrides the architecture for the item in the manifest list
Arch string `json:"arch" schema:"arch"`
// Feature list for the image
// Feature list for the item in the manifest list
Features []string `json:"features" schema:"features"`
// OS overrides the operating system for the image
// OS overrides the operating system for the item in the manifest list
OS string `json:"os" schema:"os"`
// OS features for the image
// OS features for the item in the manifest list
OSFeatures []string `json:"os_features" schema:"os_features"`
// OSVersion overrides the operating system for the image
// OSVersion overrides the operating system for the item in the manifest list
OSVersion string `json:"os_version" schema:"os_version"`
// Variant for the image
// Variant for the item in the manifest list
Variant string `json:"variant" schema:"variant"`
// IndexAnnotation is a slice of key=value annotations to add to the manifest list itself
IndexAnnotation []string `json:"index_annotation" schema:"annotation"`
// IndexAnnotations is a map of key:value annotations to add to the manifest list itself, by a map which is preferred over IndexAnnotation
IndexAnnotations map[string]string `json:"index_annotations" schema:"annotations"`
// IndexSubject is a subject value to set in the manifest list itself
IndexSubject string `json:"subject" schema:"subject"`
}
// ManifestModifyOptions provides the model for mutating a manifest
@ -77,6 +102,18 @@ type ManifestModifyOptions struct {
Operation string `json:"operation" schema:"operation"` // Valid values: update, remove, annotate
ManifestAddOptions
ManifestRemoveOptions
// The following are all of the fields from ManifestAddArtifactOptions.
// We can't just embed the whole structure because it embeds a
// ManifestAnnotateOptions, which would conflict with the one that
// ManifestAddOptions embeds.
ArtifactType *string `json:"artifact_type" schema:"artifact_type"`
ArtifactLayerType string `json:"artifact_layer_type" schema:"artifact_layer_type"`
ArtifactConfigType string `json:"artifact_config_type" schema:"artifact_config_type"`
ArtifactConfig string `json:"artifact_config" schema:"artifact_config"`
ArtifactExcludeTitles bool `json:"artifact_exclude_titles" schema:"artifact_exclude_titles"`
ArtifactAnnotations map[string]string `json:"artifact_annotations" schema:"artifact_annotations"`
ArtifactSubject string `json:"artifact_subject" schema:"artifact_subject"`
ArtifactFiles []string `json:"artifact_files" schema:"-"`
}
// ManifestPushReport provides the model for the pushed manifest

View File

@ -14,8 +14,10 @@ type ManifestPushReport struct {
type ManifestModifyReport struct {
// Manifest List ID
ID string `json:"Id"`
// Images to removed from manifest list, otherwise not provided.
// Images added to or removed from manifest list, otherwise not provided.
Images []string `json:"images,omitempty" schema:"images"`
// Files added to manifest list, otherwise not provided.
Files []string `json:"files,omitempty" schema:"files"`
// Errors associated with operation
Errors []error `json:"errors,omitempty"`
}

View File

@ -6,12 +6,14 @@ import (
"encoding/json"
"fmt"
"os"
"path"
"strings"
"errors"
"github.com/containers/common/libimage"
cp "github.com/containers/image/v5/copy"
"github.com/containers/image/v5/docker"
"github.com/containers/image/v5/manifest"
"github.com/containers/image/v5/pkg/compression"
"github.com/containers/image/v5/pkg/shortnames"
@ -24,6 +26,7 @@ import (
"github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
)
// ManifestCreate implements logic for creating manifest lists via ImageEngine
@ -45,6 +48,14 @@ func (ir *ImageEngine) ManifestCreate(ctx context.Context, name string, images [
}
}
annotateOptions := &libimage.ManifestListAnnotateOptions{}
if len(opts.Annotations) != 0 {
annotateOptions.IndexAnnotations = opts.Annotations
if err := manifestList.AnnotateInstance("", annotateOptions); err != nil {
return "", err
}
}
addOptions := &libimage.ManifestListAddOptions{All: opts.All}
for _, image := range images {
if _, err := manifestList.Add(ctx, image, addOptions); err != nil {
@ -214,6 +225,13 @@ func (ir *ImageEngine) ManifestAdd(ctx context.Context, name string, images []st
Password: opts.Password,
}
images = slices.Clone(images)
for _, image := range opts.Images {
if !slices.Contains(images, image) {
images = append(images, image)
}
}
for _, image := range images {
instanceDigest, err := manifestList.Add(ctx, image, addOptions)
if err != nil {
@ -226,6 +244,7 @@ func (ir *ImageEngine) ManifestAdd(ctx context.Context, name string, images []st
OS: opts.OS,
OSVersion: opts.OSVersion,
Variant: opts.Variant,
Subject: opts.IndexSubject,
}
if len(opts.Annotation) != 0 {
annotations := make(map[string]string)
@ -247,13 +266,26 @@ func (ir *ImageEngine) ManifestAdd(ctx context.Context, name string, images []st
return manifestList.ID(), nil
}
// ManifestAnnotate updates an entry of the manifest list
func (ir *ImageEngine) ManifestAnnotate(ctx context.Context, name, image string, opts entities.ManifestAnnotateOptions) (string, error) {
instanceDigest, err := digest.Parse(image)
if err != nil {
return "", fmt.Errorf(`invalid image digest "%s": %v`, image, err)
func mergeAnnotations(preferred map[string]string, aux []string) (map[string]string, error) {
if len(aux) != 0 {
auxAnnotations := make(map[string]string)
for _, annotationSpec := range aux {
key, val, hasVal := strings.Cut(annotationSpec, "=")
if !hasVal {
return nil, fmt.Errorf("no value given for annotation %q", key)
}
auxAnnotations[key] = val
}
if preferred == nil {
preferred = make(map[string]string)
}
preferred = envLib.Join(auxAnnotations, preferred)
}
return preferred, nil
}
// ManifestAnnotate updates an entry of the manifest list
func (ir *ImageEngine) ManifestAnnotate(ctx context.Context, name, image string, opts entities.ManifestAnnotateOptions) (string, error) {
manifestList, err := ir.Libpod.LibimageRuntime().LookupManifestList(name)
if err != nil {
return "", err
@ -265,19 +297,56 @@ func (ir *ImageEngine) ManifestAnnotate(ctx context.Context, name, image string,
OS: opts.OS,
OSVersion: opts.OSVersion,
Variant: opts.Variant,
Subject: opts.IndexSubject,
}
if annotateOptions.Annotations, err = mergeAnnotations(opts.Annotations, opts.Annotation); err != nil {
return "", err
}
if annotateOptions.IndexAnnotations, err = mergeAnnotations(opts.IndexAnnotations, opts.IndexAnnotation); err != nil {
return "", err
}
var instanceDigest digest.Digest
if image == "" {
if len(opts.Annotations) != 0 {
return "", errors.New("setting annotation on an item in a manifest list requires an instance digest")
}
if len(opts.Annotation) != 0 {
annotations := make(map[string]string)
for _, annotationSpec := range opts.Annotation {
key, val, hasVal := strings.Cut(annotationSpec, "=")
if !hasVal {
return "", fmt.Errorf("no value given for annotation %q", key)
return "", errors.New("setting annotation on an item in a manifest list requires an instance digest")
}
annotations[key] = val
if opts.Arch != "" {
return "", errors.New("setting architecture on an item in a manifest list requires an instance digest")
}
if len(opts.Features) != 0 {
return "", errors.New("setting features on an item in a manifest list requires an instance digest")
}
if opts.OS != "" {
return "", errors.New("setting OS on an item in a manifest list requires an instance digest")
}
if len(opts.OSFeatures) != 0 {
return "", errors.New("setting OS features on an item in a manifest list requires an instance digest")
}
if opts.OSVersion != "" {
return "", errors.New("setting OS version on an item in a manifest list requires an instance digest")
}
if opts.Variant != "" {
return "", errors.New("setting variant on an item in a manifest list requires an instance digest")
}
} else {
if len(opts.IndexAnnotations) != 0 {
return "", errors.New("setting index-wide annotation in a manifest list requires no instance digest")
}
if len(opts.IndexAnnotation) != 0 {
return "", errors.New("setting index-wide annotation in a manifest list requires no instance digest")
}
if len(opts.IndexSubject) != 0 {
return "", errors.New("setting subject for a manifest list requires no instance digest")
}
instanceDigest, err = ir.digestFromDigestOrManifestListMember(ctx, manifestList, image)
if err != nil {
return "", fmt.Errorf("finding instance for %q: %w", image, err)
}
opts.Annotations = envLib.Join(opts.Annotations, annotations)
}
annotateOptions.Annotations = opts.Annotations
if err := manifestList.AnnotateInstance(instanceDigest, annotateOptions); err != nil {
return "", err
@ -286,6 +355,108 @@ func (ir *ImageEngine) ManifestAnnotate(ctx context.Context, name, image string,
return manifestList.ID(), nil
}
// ManifestAddArtifact creates artifact manifest for files and adds them to the manifest list
func (ir *ImageEngine) ManifestAddArtifact(ctx context.Context, name string, files []string, opts entities.ManifestAddArtifactOptions) (string, error) {
if len(files) < 1 {
return "", errors.New("manifest add artifact requires at least one file")
}
manifestList, err := ir.Libpod.LibimageRuntime().LookupManifestList(name)
if err != nil {
return "", err
}
files = slices.Clone(files)
for _, file := range opts.Files {
if !slices.Contains(files, file) {
files = append(files, file)
}
}
addArtifactOptions := &libimage.ManifestListAddArtifactOptions{
Type: opts.Type,
ConfigType: opts.ConfigType,
Config: opts.Config,
LayerType: opts.LayerType,
ExcludeTitles: opts.ExcludeTitles,
Annotations: opts.Annotations,
Subject: opts.Subject,
}
instanceDigest, err := manifestList.AddArtifact(ctx, addArtifactOptions, files...)
if err != nil {
return "", err
}
annotateOptions := &libimage.ManifestListAnnotateOptions{
Architecture: opts.Arch,
Features: opts.Features,
OS: opts.OS,
OSVersion: opts.OSVersion,
Variant: opts.Variant,
Subject: opts.IndexSubject,
}
if annotateOptions.Annotations, err = mergeAnnotations(opts.Annotations, opts.Annotation); err != nil {
return "", err
}
if annotateOptions.IndexAnnotations, err = mergeAnnotations(opts.IndexAnnotations, opts.IndexAnnotation); err != nil {
return "", err
}
if err := manifestList.AnnotateInstance(instanceDigest, annotateOptions); err != nil {
return "", err
}
return manifestList.ID(), nil
}
func (ir *ImageEngine) digestFromDigestOrManifestListMember(ctx context.Context, list *libimage.ManifestList, name string) (digest.Digest, error) {
instanceDigest, err := digest.Parse(name)
if err == nil {
return instanceDigest, nil
}
listData, inspectErr := list.Inspect()
if inspectErr != nil {
return "", fmt.Errorf(`inspecting list "%s" for instance list: %v`, list.ID(), err)
}
// maybe the name is a file name we previously attached as part of an artifact manifest
for _, descriptor := range listData.Manifests {
if slices.Contains(descriptor.Files, path.Base(name)) || slices.Contains(descriptor.Files, name) {
return descriptor.Digest, nil
}
}
// maybe it's the name of an image we added to the list?
ref, err := alltransports.ParseImageName(name)
if err != nil {
withDocker := fmt.Sprintf("%s://%s", docker.Transport.Name(), name)
ref, err = alltransports.ParseImageName(withDocker)
if err != nil {
image, _, err := ir.Libpod.LibimageRuntime().LookupImage(name, &libimage.LookupImageOptions{ManifestList: true})
if err != nil {
return "", fmt.Errorf("locating image named %q to check if it's in the manifest list: %w", name, err)
}
if ref, err = image.StorageReference(); err != nil {
return "", fmt.Errorf("reading image reference %q to check if it's in the manifest list: %w", name, err)
}
}
}
// read the manifest of this image
src, err := ref.NewImageSource(ctx, ir.Libpod.SystemContext())
if err != nil {
return "", fmt.Errorf("reading local image %q to check if it's in the manifest list: %w", name, err)
}
defer src.Close()
manifestBytes, _, err := src.GetManifest(ctx, nil)
if err != nil {
return "", fmt.Errorf("locating image named %q to check if it's in the manifest list: %w", name, err)
}
refDigest, err := manifest.Digest(manifestBytes)
if err != nil {
return "", fmt.Errorf("digesting manifest of local image %q: %w", name, err)
}
return refDigest, nil
}
// ManifestRemoveDigest removes specified digest from the specified manifest list
func (ir *ImageEngine) ManifestRemoveDigest(ctx context.Context, name, image string) (string, error) {
instanceDigest, err := digest.Parse(image)

View File

@ -11,11 +11,12 @@ import (
"github.com/containers/podman/v5/pkg/bindings/manifests"
"github.com/containers/podman/v5/pkg/domain/entities"
envLib "github.com/containers/podman/v5/pkg/env"
"golang.org/x/exp/slices"
)
// ManifestCreate implements manifest create via ImageEngine
func (ir *ImageEngine) ManifestCreate(ctx context.Context, name string, images []string, opts entities.ManifestCreateOptions) (string, error) {
options := new(manifests.CreateOptions).WithAll(opts.All).WithAmend(opts.Amend)
options := new(manifests.CreateOptions).WithAll(opts.All).WithAmend(opts.Amend).WithAnnotation(opts.Annotations)
imageID, err := manifests.Create(ir.ClientCtx, name, images, options)
if err != nil {
return imageID, fmt.Errorf("creating manifest: %w", err)
@ -57,6 +58,13 @@ func (ir *ImageEngine) ManifestInspect(ctx context.Context, name string, opts en
// ManifestAdd adds images to the manifest list
func (ir *ImageEngine) ManifestAdd(_ context.Context, name string, imageNames []string, opts entities.ManifestAddOptions) (string, error) {
imageNames = slices.Clone(imageNames)
for _, image := range opts.Images {
if !slices.Contains(imageNames, image) {
imageNames = append(imageNames, image)
}
}
options := new(manifests.AddOptions).WithAll(opts.All).WithArch(opts.Arch).WithVariant(opts.Variant)
options.WithFeatures(opts.Features).WithImages(imageNames).WithOS(opts.OS).WithOSVersion(opts.OSVersion)
options.WithUsername(opts.Username).WithPassword(opts.Password).WithAuthfile(opts.Authfile)
@ -89,6 +97,39 @@ func (ir *ImageEngine) ManifestAdd(_ context.Context, name string, imageNames []
return id, nil
}
// ManifestAddArtifact creates artifact manifests and adds them to the manifest list
func (ir *ImageEngine) ManifestAddArtifact(_ context.Context, name string, files []string, opts entities.ManifestAddArtifactOptions) (string, error) {
files = slices.Clone(files)
for _, file := range opts.Files {
if !slices.Contains(files, file) {
files = append(files, file)
}
}
options := new(manifests.AddArtifactOptions).WithArch(opts.Arch).WithVariant(opts.Variant)
options.WithFeatures(opts.Features).WithOS(opts.OS).WithOSVersion(opts.OSVersion).WithOSFeatures(opts.OSFeatures)
if len(opts.Annotation) != 0 {
annotations := make(map[string]string)
for _, annotationSpec := range opts.Annotation {
key, val, hasVal := strings.Cut(annotationSpec, "=")
if !hasVal {
return "", fmt.Errorf("no value given for annotation %q", key)
}
annotations[key] = val
}
options.WithAnnotation(annotations)
}
options.WithType(opts.Type).WithConfigType(opts.ConfigType).WithLayerType(opts.LayerType)
options.WithConfig(opts.Config)
options.WithExcludeTitles(opts.ExcludeTitles).WithSubject(opts.Subject)
options.WithAnnotations(opts.Annotations)
options.WithFiles(files)
id, err := manifests.AddArtifact(ir.ClientCtx, name, options)
if err != nil {
return id, fmt.Errorf("adding to manifest list %s: %w", name, err)
}
return id, nil
}
// ManifestAnnotate updates an entry of the manifest list
func (ir *ImageEngine) ManifestAnnotate(ctx context.Context, name, images string, opts entities.ManifestAnnotateOptions) (string, error) {
options := new(manifests.ModifyOptions).WithArch(opts.Arch).WithVariant(opts.Variant)

View File

@ -3,6 +3,7 @@
# Tests for manifest list endpoints
start_registry
export REGISTRY_PORT
# Creates the manifest list
t POST /v3.4.0/libpod/manifests/create?name=abc 200 \
@ -28,7 +29,7 @@ RUN >file2
EOF
)
# manifest add --anotation tests
# manifest add --annotation tests
t POST /v3.4.0/libpod/manifests/$id_abc/add images="[\"containers-storage:$id_abc_image\"]" 200
t PUT /v4.0.0/libpod/manifests/$id_xyz operation='update' images="[\"containers-storage:$id_xyz_image\"]" annotations="{\"foo\":\"bar\"}" annotation="[\"hoge=fuga\"]" 400 \
.cause='can not set both Annotation and Annotations'
@ -66,5 +67,130 @@ t POST "/v4.0.0/libpod/manifests/xyz:latest/registry/localhost:$REGISTRY_PORT%2F
t DELETE /v4.0.0/libpod/manifests/$id_abc 200
t DELETE /v4.0.0/libpod/manifests/$id_xyz 200
# manifest add --artifact tests
truncate -s 20M $WORKDIR/zeroes
function test_artifacts_with_args() {
# these values, ideally, are local to our caller
local args="$artifact_annotations $artifact_config $artifact_config_type $artifact_exclude_titles $artifact_layer_type $artifact_type"
t POST /v5.0.0/libpod/manifests/artifacts 201
local id_artifacts=$(jq -r '.Id' <<<"$output")
t PUT /v5.0.0/libpod/manifests/$id_artifacts operation='update' $args --form=listed.txt="oh yeah" --form=zeroes=@"$WORKDIR/zeroes" 200
t POST "/v5.0.0/libpod/manifests/artifacts:latest/registry/localhost:$REGISTRY_PORT%2Fartifacts:latest?tlsVerify=false&all=true" 200
local index=$(skopeo inspect --raw --tls-verify=false docker://localhost:$REGISTRY_PORT/artifacts:latest)
# jq <<<"$index"
local digest=$(jq -r '.manifests[0].digest' <<<"$index")
local artifact=$(skopeo inspect --raw --tls-verify=false docker://localhost:$REGISTRY_PORT/artifacts@${digest})
# jq <<<"$artifact"
local expect_type
case ${artifact_type} in
artifact_type=*)
expect_type=${artifact_type#artifact_type=}
expect_type=${expect_type:-null};;
*)
expect_type=application/vnd.unknown.artifact.v1;;
esac
is $(jq -r '.artifactType'<<<"$artifact") "${expect_type}" "artifactType in artifact manifest with artifact_type arg \"${artifact_type}\""
is $(jq -r '.manifests[0].artifactType'<<<"$index") "${expect_type}" "artifactType in image index with artifact_type arg \"${artifact_type}\""
local expect_annotations
case ${artifact_annotations} in
artifact_annotations=*)
expect_annotations=$(jq -r '.foo' <<<"${artifact_annotations#artifact_annotations=}");;
*)
expect_annotations=null;;
esac
is $(jq -r '.annotations["foo"]'<<<"$artifact") "$expect_annotations" "\"foo\" annotation in artifact manifest with artifact_annotations arg \"${artifact_annotations}\""
local expect_config_size
case ${artifact_config} in
artifact_config=*)
expect_config_size=$(wc -c <<<"${artifact_config#artifact_config=}")
expect_config_size=$((expect_config_size-1))
if [[ $expect_config_size -eq 0 ]]; then
expect_config_size=2
fi ;;
*)
expect_config_size=2;;
esac
is $(jq -r '.config.size'<<<"$artifact") "$expect_config_size" "size of config blob in artifact manifest with artifact_config arg \"${artifact_config}\""
local expect_config_type
case ${artifact_config_type} in
artifact_config_type=*)
expect_config_type=${artifact_config_type#artifact_config_type=}
if [[ -z "$expect_config_type" ]] ; then
if [[ -n "${artifact_config#artifact_config=}" ]] ; then
expect_config_type=application/vnd.oci.image.config.v1+json
else
expect_config_type=application/vnd.oci.empty.v1+json
fi
fi;;
*)
if [[ -n "${artifact_config#artifact_config=}" ]] ; then
expect_config_type=application/vnd.oci.image.config.v1+json
else
expect_config_type=application/vnd.oci.empty.v1+json
fi;;
esac
is $(jq -r '.config.mediaType'<<<"$artifact") "$expect_config_type" "mediaType of config blob in artifact manifest with artifact_config_type arg \"${artifact_config_type}\" and artifact_config arg \"${artifact_config}\""
local expect_first_layer_type expect_second_layer_type
case ${artifact_layer_type} in
artifact_layer_type=*)
expect_first_layer_type=${artifact_layer_type#artifact_layer_type=}
expect_first_layer_type=${expect_first_layer_type:-text/plain}
expect_second_layer_type=${artifact_layer_type#artifact_layer_type=}
expect_second_layer_type=${expect_second_layer_type:-application/octet-stream};;
*)
expect_first_layer_type=text/plain;
expect_second_layer_type=application/octet-stream;;
esac
is $(jq -r '.layers[0].mediaType'<<<"$artifact") "$expect_first_layer_type" "mediaType of listed.txt layer in artifact manifest with artifact_layer_type arg \"${artifact_layer_type}\""
is $(jq -r '.layers[1].mediaType'<<<"$artifact") "$expect_second_layer_type" "mediaType of zeroes layer in artifact manifest with artifact_layer_type arg \"${artifact_layer_type}\""
local expect_first_title expect_second_title
case ${artifact_exclude_titles} in
artifact_exclude_titles=true)
expect_first_title=null;
expect_second_title=null;;
*)
expect_first_title=listed.txt;
expect_second_title=zeroes;;
esac
is $(jq -r '.layers[0].annotations["org.opencontainers.image.title"]'<<<"$artifact") "$expect_first_title" "org.opencontainers.image.title annotation on listed.txt layer in artifact manifest with artifact_exclude_titles arg \"${artifact_exclude_titles}\""
is $(jq -r '.layers[1].annotations["org.opencontainers.image.title"]'<<<"$artifact") "$expect_second_title" "org.opencontainers.image.title annotation on zeroes layer in artifact manifest with artifact_exclude_titles arg \"${artifact_exclude_titles}\""
t DELETE /v5.0.0/libpod/manifests/$id_artifacts 200
}
function test_artifacts() {
local artifact_annotations
local artifact_config
local artifact_config_type
local artifact_exclude_titles
local artifact_layer_type
local artifact_type
for artifact_annotations in "" artifact_annotations='{"foo":"bar"}' ; do
test_artifacts_with_args
done
for artifact_config in "" artifact_config= artifact_config="{}"; do
for artifact_config_type in "" artifact_config_type= artifact_config_type=text/plain ; do
test_artifacts_with_args
done
done
for artifact_exclude_titles in "" artifact_exclude_titles=true ; do
test_artifacts_with_args
done
for artifact_layer_type in "" artifact_layer_type= artifact_layer_type=text/plain artifact_layer_type=application/octet-stream ; do
test_artifacts_with_args
done
for artifact_type in "" artifact_type= artifact_type=text/plain artifact_type=application/octet-stream ; do
test_artifacts_with_args
done
}
test_artifacts
podman rmi -a
stop_registry

View File

@ -33,7 +33,7 @@ wait "${child_pid}"
APIV2_TEST_EXPECT_TIMEOUT=2 t POST "containers/${CTR}/wait?condition=next-exit" 999
like "$(<$WORKDIR/curl.headers.out)" ".*HTTP.* 200 OK.*" \
"Received headers from /wait"
if [[ -e $WORKDIR/curl.result.out ]]; then
if [[ -s $WORKDIR/curl.result.out ]]; then
_show_ok 0 "UNEXPECTED: curl on /wait returned results"
fi

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
# Usage: test-apiv2 [PORT]
# Usage: test-apiv2 testglob
#
# DEVELOPER NOTE: you almost certainly don't need to play in here. See README.
#
@ -238,17 +238,17 @@ function jsonify() {
function t() {
local method=$1; shift
local path=$1; shift
local -a curl_args
local -a curl_args form_args
local content_type="application/json"
local testname="$method $path"
# POST and PUT requests may be followed by one or more key=value pairs.
# Slurp the command line until we see a 3-digit status code.
if [[ $method = "POST" || $method == "PUT" || $method = "DELETE" ]]; then
if [[ $method = "POST" || $method == "PUT" || $method == "DELETE" ]]; then
local -a post_args
if [[ $method = "POST" ]]; then
if [[ $method == "POST" ]]; then
function _add_curl_args() { curl_args+=(--data-binary @$1); }
else
function _add_curl_args() { curl_args+=(--upload-file $1); }
@ -260,6 +260,10 @@ function t() {
# --disable makes curl not lookup the curlrc file, it shouldn't affect the tests in any way.
-) curl_args+=(--disable);
shift;;
--form=*) form_args+=(--form);
form_args+=("${arg#--form=}");
content_type="multipart/form-data";
shift;;
*=*) post_args+=("$arg");
shift;;
*.json) _add_curl_args $arg;
@ -276,9 +280,12 @@ function t() {
*) die "Internal error: invalid POST arg '$arg'" ;;
esac
done
if [[ -z "$curl_args" ]]; then
if [[ -z "${curl_args[*]}" && -z "${form_args[*]}" ]]; then
curl_args=(-d $(jsonify ${post_args[*]}))
testname="$testname [${curl_args[*]}]"
elif [[ -z "${curl_args[*]}" ]]; then
curl_args=(--form request.json=$(jsonify ${post_args[*]}) "${form_args[@]}")
testname="$testname [${curl_args[*]} ${form_args[*]}]"
fi
fi
@ -319,14 +326,29 @@ function t() {
echo "-------------------------------------------------------------" >>$LOG
echo "\$ $testname" >>$LOG
rm -f $WORKDIR/curl.*
# -s = silent, but --write-out 'format' gives us important response data
# The --write-out 'format' gives us important response data.
# The hairy "{ ...;rc=$?; } || :" lets us capture curl's exit code and
# give a helpful diagnostic if it fails.
{ response=$(curl -s -X $method "${curl_args[@]}" \
: > $WORKDIR/curl.result.out
: > $WORKDIR/curl.result.err
{ response=$(curl -X $method "${curl_args[@]}" \
-H "Content-type: $content_type" \
--dump-header $WORKDIR/curl.headers.out \
-v --stderr $WORKDIR/curl.result.err \
--write-out '%{http_code}^%{content_type}^%{time_total}' \
-o $WORKDIR/curl.result.out "$url"); rc=$?; } || :
if [ -n "$PODMAN_TESTS_DUMP_TRACES" ]; then
# Dump the results we got back, exactly as we got them back.
echo "\$ begin stdout" >>$LOG
test -s $WORKDIR/curl.result.out && od -t x1c $WORKDIR/curl.result.out 2>&1 >>$LOG
echo "\$ end stdout" >>$LOG
echo "\$ begin stderr" >>$LOG
test -s $WORKDIR/curl.result.err && cat $WORKDIR/curl.result.err >>$LOG
echo "\$ end stderr" >>$LOG
echo "\$ begin response code^content_type^time_total" >>$LOG
od -t x1c <<< "$response" >>$LOG
echo "\$ end response" >>$LOG
fi
# Special case: this means we *expect and want* a timeout
if [[ -n "$APIV2_TEST_EXPECT_TIMEOUT" ]]; then

View File

@ -150,4 +150,134 @@ EOF
run_podman image prune -f
}
function manifestListAddArtifactOnce() {
echo listFlags="$listFlags"
echo platformFlags="$platformFlags"
echo typeFlag="$typeFlag"
echo layerTypeFlag="$layerTypeFlag"
echo configTypeFlag="$configTypeFlag"
echo configFlag="$configFlag"
echo titleFlag="$titleFlag"
local index artifact firstdigest seconddigest config configSize defaulttype filetitle requested expected actual
run_podman manifest create $listFlags $list
run_podman manifest add $list ${platformFlags} --artifact ${typeFlag} ${layerTypeFlag} ${configTypeFlag} ${configFlag} ${titleFlag} ${PODMAN_TMPDIR}/listed.txt
run_podman manifest add $list ${platformFlags} --artifact ${typeFlag} ${layerTypeFlag} ${configTypeFlag} ${configFlag} ${titleFlag} ${PODMAN_TMPDIR}/zeroes
run_podman manifest inspect $list
run_podman tag $list localhost:${PODMAN_LOGIN_REGISTRY_PORT}/test
run_podman manifest push --tls-verify=false localhost:${PODMAN_LOGIN_REGISTRY_PORT}/test
run skopeo inspect --tls-verify=false --raw docker://localhost:${PODMAN_LOGIN_REGISTRY_PORT}/test
assert $status -eq 0
echo "$output"
index="$output"
if [[ -n "$listFlags" ]] ; then
assert $(jq -r '.annotations["global"]' <<<"$index") == local
fi
if [[ -n "$platformFlags" ]] ; then
assert $(jq -r '.manifests[1].platform.os' <<<"$index") == linux
assert $(jq -r '.manifests[1].platform.architecture' <<<"$index") == amd64
fi
if [[ -n "$typeFlag" ]] ; then
actual=$(jq -r '.manifests[0].artifactType' <<<"$index")
assert "${actual#null}" == "${typeFlag#--artifact-type=}"
actual=$(jq -r '.manifests[1].artifactType' <<<"$index")
assert "${actual#null}" == "${typeFlag#--artifact-type=}"
fi
firstdigest=$(jq -r '.manifests[0].digest' <<<"$index")
seconddigest=$(jq -r '.manifests[1].digest' <<<"$index")
for digest in $firstdigest $seconddigest ; do
case $digest in
$firstdigest)
filetitle=listed.txt
defaulttype=text/plain
;;
$seconddigest)
filetitle=zeroes
defaulttype=application/octet-stream
;;
*)
false
;;
esac
run skopeo inspect --raw --tls-verify=false docker://localhost:${PODMAN_LOGIN_REGISTRY_PORT}/test@${digest}
assert $status -eq 0
echo "$output"
artifact="$output"
if [[ -n "$typeFlag" ]] ; then
actual=$(jq -r '.artifactType' <<<"$artifact")
assert "${actual#null}" == "${typeFlag#--artifact-type=}"
else
actual=$(jq -r '.artifactType' <<<"$artifact")
assert "${actual}" == application/vnd.unknown.artifact.v1
fi
if [ -n "$layerTypeFlag" ] ; then
actual=$(jq -r '.layers[0].mediaType' <<<"$artifact")
assert "${actual}" == "${layerTypeFlag#--artifact-layer-type=}"
else
actual=$(jq -r '.layers[0].mediaType' <<<"$artifact")
assert "${actual}" == "$defaulttype"
fi
requested=${configTypeFlag#--artifact-config-type=}
actual=$(jq -r '.config.mediaType' <<<"$artifact")
if test -n "$requested" ; then
assert "$actual" == "$requested"
else
config=${configFlag#--artifact-config=}
if [ -z "$config" ] ; then
expected=application/vnd.oci.empty.v1+json
else
configSize=$(wc -c <"$config")
if [ $configSize -gt 0 ] ; then
expected=application/vnd.oci.image.config.v1+json
else
expected=application/vnd.oci.empty.v1+json
fi
fi
assert "$actual" == "$expected"
fi
if test -n "$titleFlag" ; then
assert $(jq -r '.layers[0].annotations["org.opencontainers.image.title"]' <<<"$artifact") == null
else
assert $(jq -r '.layers[0].annotations["org.opencontainers.image.title"]' <<<"$artifact") == $filetitle
fi
done
run_podman rmi $list localhost:${PODMAN_LOGIN_REGISTRY_PORT}/test
}
@test "manifest list --add --artifact" {
# Build a list and add some files to it, making sure to exercise and verify
# every flag available.
skip_if_remote "running a local registry doesn't work with podman-remote"
start_registry
run_podman login --tls-verify=false \
--username ${PODMAN_LOGIN_USER} \
--password-stdin \
--authfile=$authfile \
localhost:${PODMAN_LOGIN_REGISTRY_PORT} <<<"${PODMAN_LOGIN_PASS}"
local list="test:1.0"
truncate -s 20M ${PODMAN_TMPDIR}/zeroes
echo oh yeah > ${PODMAN_TMPDIR}/listed.txt
echo '{}' > ${PODMAN_TMPDIR}/minimum-config.json
local listFlags platformFlags typeFlag configTypeFlag configFlag layerTypeFlag titleFlag
for listFlags in "" "--annotation global=local" ; do
manifestListAddArtifactOnce
done
for platformFlags in "" "--os=linux --arch=amd64" ; do
manifestListAddArtifactOnce
done
for typeFlag in "" --artifact-type="" --artifact-type=application/octet-stream --artifact-type=text/plain ; do
manifestListAddArtifactOnce
done
for configTypeFlag in "" --artifact-config-type=application/octet-stream --artifact-config-type=text/plain ; do
for configFlag in "" --artifact-config= --artifact-config=${PODMAN_TMPDIR}/minimum-config.json ; do
manifestListAddArtifactOnce
done
done
for layerTypeFlag in "" --artifact-layer-type=application/octet-stream --artifact-layer-type=text/plain ; do
manifestListAddArtifactOnce
done
for titleFlag in "" "--artifact-exclude-titles" ; do
manifestListAddArtifactOnce
done
stop_registry
}
# vim: filetype=sh