Merge pull request #21653 from nalind/fun-with-artifacts

podman manifest add: support creating artifact manifest on the fly
This commit is contained in:
openshift-merge-bot[bot] 2024-02-29 19:04:03 +00:00 committed by GitHub
commit b681209efe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1674 additions and 207 deletions

View File

@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"os"
"strings"
"github.com/containers/common/pkg/auth"
"github.com/containers/common/pkg/completion"
@ -12,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

View File

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

View File

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

View File

@ -4,4 +4,4 @@
####> are applicable to all of those.
#### **--annotation**=*annotation=value*
Set an annotation on the entry for the image.
Set an annotation on the entry for the specified image or artifact.

View File

@ -2,7 +2,7 @@
####> podman manifest add, manifest annotate
####> If file is edited, make sure the changes
####> are applicable to all of those.
#### **--features**
#### **--features**=*feature*
Specify the features list which the list or index records as requirements for
the image. This option is rarely used.

View File

@ -2,7 +2,7 @@
####> podman manifest add, manifest annotate
####> If file is edited, make sure the changes
####> are applicable to all of those.
#### **--os-version**
#### **--os-version**=*version*
Specify the OS version which the list or index records as a requirement for the
image. This option is rarely used.

View File

@ -1,16 +1,18 @@
% podman-manifest-add 1
## NAME
podman\-manifest\-add - Add an image to a manifest list or image index
podman\-manifest\-add - Add an image or artifact to a manifest list or image index
## SYNOPSIS
**podman manifest add** [*options*] *listnameorindexname* [*transport*]:*imagename*
**podman manifest add** [*options*] *listnameorindexname* [*transport*]:*imagename* *imageorartifactname* [...]
## DESCRIPTION
Adds the specified image to the specified manifest list or image index.
Adds the specified image to the specified manifest list or image index, or
creates an artifact manifest and adds it to the specified image index.
## RETURN VALUE
The list image's ID.
## OPTIONS
@ -24,13 +26,62 @@ from such a list or index is added to the list or index. Combining
@@option annotation.manifest
#### **--arch**
#### **--arch**=*architecture*
Override the architecture which the list or index records as a requirement for
the image. If *imageName* refers to a manifest list or image index, the
architecture information is retrieved from it. Otherwise, it is
retrieved from the image's configuration information.
#### **--artifact**
Create an artifact manifest and add it to the image index. Arguments after the
index name will be interpreted as file names rather than as image references.
In most scenarios, the **--artifact-type** option should also be specified.
#### **--artifact-config**=*path*
When creating an artifact manifest and adding it to the image index, use the
specified file's contents as the configuration blob in the artifact manifest.
In most scenarios, leaving the default value, which signifies an empty
configuration, unchanged, is the preferred option.
#### **--artifact-config-type**=*type*
When creating an artifact manifest and adding it to the image index, use the
specified MIME type as the `mediaType` associated with the configuration blob
in the artifact manifest. In most scenarios, leaving the default value, which
signifies either an empty configuration or the standard OCI configuration type,
unchanged, is the preferred option.
#### **--artifact-exclude-titles**
When creating an artifact manifest and adding it to the image index, do not
set "org.opencontainers.image.title" annotations equal to the file's basename
for each file added to the artifact manifest. Tools which retrieve artifacts
from a registry may use these values to choose names for files when saving
artifacts to disk, so this option is not recommended unless it is required
for interoperability with a particular registry.
#### **--artifact-layer-type**=*type*
When creating an artifact manifest and adding it to the image index, use the
specified MIME type as the `mediaType` associated with the files' contents. If
not specified, guesses based on either the files names or their contents will
be made and used, but the option should be specified if certainty is needed.
#### **--artifact-subject**=*imageName*
When creating an artifact manifest and adding it to the image index, set the
*subject* field in the artifact manifest to mark the artifact manifest as being
associated with the specified image in some way. An artifact manifest can only
be associated with, at most, one subject.
#### **--artifact-type**=*type*
When creating an artifact manifest, use the specified MIME type as the
manifest's `artifactType` value instead of the less informative default value.
@@option authfile
@@option cert-dir
@ -39,7 +90,7 @@ retrieved from the image's configuration information.
@@option features
#### **--os**
#### **--os**=*OS*
Override the OS which the list or index records as a requirement for the image.
If *imagename* refers to a manifest list or image index, the OS information

View File

@ -1,41 +1,53 @@
% podman-manifest-annotate 1
## NAME
podman\-manifest\-annotate - Add or update information about an entry in a manifest list or image index
podman\-manifest\-annotate - Add and update information about an image or artifact in a manifest list or image index
## SYNOPSIS
**podman manifest annotate** [*options*] *listnameorindexname* *imagemanifestdigest*
**podman manifest annotate** [*options*] *listnameorindexname* *imagemanifestdigestorimageorartifactname*
## DESCRIPTION
Adds or updates information about an image included in a manifest list or image index.
Adds or updates information about an image or artifact included in a manifest list or image index.
## OPTIONS
@@option annotation.manifest
If **--index** is also specified, sets the annotation on the entire image index.
#### **--arch**
#### **--arch**=*architecture*
Override the architecture which the list or index records as a requirement for
the image. This is usually automatically retrieved from the image's
configuration information, so it is rarely necessary to use this option.
@@option features
#### **--os**
#### **--index**
Treats arguments to the **--annotation** option as annotation values to be set
on the image index itself rather than on an entry in the image index. Implied
for **--subject**.
#### **--os**=*OS*
Override the OS which the list or index records as a requirement for the image.
This is usually automatically retrieved from the image's configuration
information, so it is rarely necessary to use this option.
#### **--os-features**
#### **--os-features**=*feature*
Specify the OS features list which the list or index records as requirements
for the image. This option is rarely used.
@@option os-version
#### **--subject**=*imageName*
Set the *subject* field in the image index to mark the image index as being
associated with the specified image in some way. An image index can only be
associated with, at most, one subject.
@@option variant.manifest
## EXAMPLE

View File

@ -28,6 +28,10 @@ If a manifest list named *listnameorindexname* already exists, modify the
preexisting list instead of exiting with an error. The contents of
*listnameorindexname* are not modified if no *imagename*s are given.
#### **--annotation**=*value*
Set an annotation on the newly-created image index.
@@option tls-verify
## EXAMPLES

View File

@ -13,16 +13,16 @@ The `podman manifest` command provides subcommands which can be used to:
## SUBCOMMANDS
| Command | Man Page | Description |
| -------- | ------------------------------------------------------------ | --------------------------------------------------------------------------- |
| add | [podman-manifest-add(1)](podman-manifest-add.1.md) | Add an image to a manifest list or image index. |
| annotate | [podman-manifest-annotate(1)](podman-manifest-annotate.1.md) | Add or update information about an entry in a manifest list or image index. |
| create | [podman-manifest-create(1)](podman-manifest-create.1.md) | Create a manifest list or image index. |
| exists | [podman-manifest-exists(1)](podman-manifest-exists.1.md) | Check if the given manifest list exists in local storage |
| inspect | [podman-manifest-inspect(1)](podman-manifest-inspect.1.md) | Display a manifest list or image index. |
| push | [podman-manifest-push(1)](podman-manifest-push.1.md) | Push a manifest list or image index to a registry. |
| remove | [podman-manifest-remove(1)](podman-manifest-remove.1.md) | Remove an image from a manifest list or image index. |
| rm | [podman-manifest-rm(1)](podman-manifest-rm.1.md) | Remove manifest list or image index from local storage. |
| Command | Man Page | Description |
| -------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
| add | [podman-manifest-add(1)](podman-manifest-add.1.md) | Add an image or artifact to a manifest list or image index. |
| annotate | [podman-manifest-annotate(1)](podman-manifest-annotate.1.md) | Add and update information about an image or artifact in a manifest list or image index. |
| create | [podman-manifest-create(1)](podman-manifest-create.1.md) | Create a manifest list or image index. |
| exists | [podman-manifest-exists(1)](podman-manifest-exists.1.md) | Check if the given manifest list exists in local storage |
| inspect | [podman-manifest-inspect(1)](podman-manifest-inspect.1.md) | Display a manifest list or image index. |
| push | [podman-manifest-push(1)](podman-manifest-push.1.md) | Push a manifest list or image index to a registry. |
| remove | [podman-manifest-remove(1)](podman-manifest-remove.1.md) | Remove an image from a manifest list or image index. |
| rm | [podman-manifest-rm(1)](podman-manifest-rm.1.md) | Remove manifest list or image index from local storage. |
## EXAMPLES

View File

@ -8,8 +8,12 @@ import (
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/containers/common/libimage/define"
"github.com/containers/image/v5/docker/reference"
@ -23,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:

View File

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

View File

@ -1,5 +1,7 @@
package manifests
import "io"
// InspectOptions are optional options for inspecting manifests
//
//go:generate go run ../generator/generator.go InspectOptions
@ -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:"-"`
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,12 +6,14 @@ import (
"encoding/json"
"fmt"
"os"
"path"
"strings"
"errors"
"github.com/containers/common/libimage"
cp "github.com/containers/image/v5/copy"
"github.com/containers/image/v5/docker"
"github.com/containers/image/v5/manifest"
"github.com/containers/image/v5/pkg/compression"
"github.com/containers/image/v5/pkg/shortnames"
@ -24,6 +26,7 @@ import (
"github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
)
// ManifestCreate implements logic for creating manifest lists via ImageEngine
@ -45,6 +48,14 @@ func (ir *ImageEngine) ManifestCreate(ctx context.Context, name string, images [
}
}
annotateOptions := &libimage.ManifestListAnnotateOptions{}
if len(opts.Annotations) != 0 {
annotateOptions.IndexAnnotations = opts.Annotations
if err := manifestList.AnnotateInstance("", annotateOptions); err != nil {
return "", err
}
}
addOptions := &libimage.ManifestListAddOptions{All: opts.All}
for _, image := range images {
if _, err := manifestList.Add(ctx, image, addOptions); err != nil {
@ -214,6 +225,13 @@ func (ir *ImageEngine) ManifestAdd(ctx context.Context, name string, images []st
Password: opts.Password,
}
images = slices.Clone(images)
for _, image := range opts.Images {
if !slices.Contains(images, image) {
images = append(images, image)
}
}
for _, image := range images {
instanceDigest, err := manifestList.Add(ctx, image, addOptions)
if err != nil {
@ -226,6 +244,7 @@ func (ir *ImageEngine) ManifestAdd(ctx context.Context, name string, images []st
OS: opts.OS,
OSVersion: opts.OSVersion,
Variant: opts.Variant,
Subject: opts.IndexSubject,
}
if len(opts.Annotation) != 0 {
annotations := make(map[string]string)
@ -247,13 +266,26 @@ func (ir *ImageEngine) ManifestAdd(ctx context.Context, name string, images []st
return manifestList.ID(), nil
}
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)

View File

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

5
pkg/env/env.go vendored
View File

@ -8,6 +8,8 @@ import (
"fmt"
"os"
"strings"
"golang.org/x/exp/maps"
)
const whiteSpaces = " \t"
@ -50,8 +52,9 @@ func Map(slice []string) map[string]string {
// Join joins the two environment maps with override overriding base.
func Join(base map[string]string, override map[string]string) map[string]string {
if len(base) == 0 {
return override
return maps.Clone(override)
}
base = maps.Clone(base)
for k, v := range override {
base[k] = v
}

View File

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

View File

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

View File

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

View File

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