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:
parent
2bbed8f200
commit
f168b3c115
|
@ -4,6 +4,8 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/common/pkg/auth"
|
||||
"github.com/containers/common/pkg/completion"
|
||||
|
@ -12,30 +14,35 @@ 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,
|
||||
Example: `podman manifest add mylist:v1.11 image:v1.11-amd64
|
||||
podman manifest add mylist:v1.11 transport:imageName`,
|
||||
podman manifest add mylist:v1.11 transport:imageName`,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -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,18 +146,53 @@ 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 err != nil {
|
||||
return err
|
||||
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,22 +27,24 @@ 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) {
|
||||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
||||
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
|
||||
query := struct {
|
||||
Name string `schema:"name"`
|
||||
Images []string `schema:"images"`
|
||||
All bool `schema:"all"`
|
||||
Amend bool `schema:"amend"`
|
||||
Name string `schema:"name"`
|
||||
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 _, err := imageEngine.ManifestAnnotate(r.Context(), manID, "", manifestAnnotateOptions); err != nil {
|
||||
utils.InternalServerError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(body.Images) > 0 {
|
||||
images = body.Images
|
||||
if _, err := imageEngine.ManifestAdd(r.Context(), manID, body.Images, body.ManifestAddOptions); err != nil {
|
||||
utils.InternalServerError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
id, err := imageEngine.ManifestAdd(r.Context(), query.Name, images, body.ManifestAddOptions)
|
||||
if 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)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("Decode(): %w", err))
|
||||
return
|
||||
|
||||
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("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
|
||||
}
|
||||
body.ManifestAddOptions.IndexAnnotations = annotationsFromAnnotationSlice(body.ManifestAddOptions.IndexAnnotation)
|
||||
body.ManifestAddOptions.IndexAnnotation = 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
|
||||
}
|
||||
annotations[key] = val
|
||||
}
|
||||
body.ManifestAddOptions.Annotations = envLib.Join(body.ManifestAddOptions.Annotations, annotations)
|
||||
body.ManifestAddOptions.Annotation = nil
|
||||
// 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):
|
||||
id, err := imageEngine.ManifestAdd(r.Context(), name, body.Images, body.ManifestAddOptions)
|
||||
if err != nil {
|
||||
report.Errors = append(report.Errors, err)
|
||||
break
|
||||
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.ID = id
|
||||
report.Images = body.Images
|
||||
}
|
||||
report = entities.ManifestModifyReport{
|
||||
ID: id,
|
||||
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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package manifests
|
||||
|
||||
import "io"
|
||||
|
||||
// InspectOptions are optional options for inspecting manifests
|
||||
//
|
||||
//go:generate go run ../generator/generator.go InspectOptions
|
||||
|
@ -15,8 +17,9 @@ type InspectOptions struct {
|
|||
//
|
||||
//go:generate go run ../generator/generator.go CreateOptions
|
||||
type CreateOptions struct {
|
||||
All *bool
|
||||
Amend *bool
|
||||
All *bool
|
||||
Amend *bool
|
||||
Annotation map[string]string
|
||||
}
|
||||
|
||||
// ExistsOptions are optional options for checking
|
||||
|
@ -30,20 +33,45 @@ type ExistsOptions struct {
|
|||
//
|
||||
//go:generate go run ../generator/generator.go AddOptions
|
||||
type AddOptions struct {
|
||||
All *bool
|
||||
Annotation map[string]string
|
||||
Arch *string
|
||||
Features []string
|
||||
All *bool
|
||||
|
||||
Annotation map[string]string
|
||||
Arch *string
|
||||
Features []string
|
||||
OS *string
|
||||
OSVersion *string
|
||||
OSFeatures []string
|
||||
Variant *string
|
||||
|
||||
Images []string
|
||||
OS *string
|
||||
OSVersion *string
|
||||
Variant *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
|
||||
|
@ -55,21 +83,31 @@ type RemoveOptions struct {
|
|||
//go:generate go run ../generator/generator.go ModifyOptions
|
||||
type ModifyOptions struct {
|
||||
// Operation values are "update", "remove" and "annotate". This allows the service to
|
||||
// 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
|
||||
// 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 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:"-"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
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) {
|
||||
instanceDigest, err := digest.Parse(image)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(`invalid image digest "%s": %v`, image, err)
|
||||
}
|
||||
|
||||
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 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
|
||||
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 {
|
||||
return "", errors.New("setting annotation on an item in a manifest list requires an instance digest")
|
||||
}
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue