diff --git a/cmd/buildah/manifest.go b/cmd/buildah/manifest.go index d2220d47f..fc2e44bac 100644 --- a/cmd/buildah/manifest.go +++ b/cmd/buildah/manifest.go @@ -26,6 +26,7 @@ import ( "github.com/hashicorp/go-multierror" digest "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -33,12 +34,17 @@ import ( type manifestCreateOpts struct { os, arch string all, tlsVerify, insecure, amend bool + annotations []string } type manifestAddOpts struct { authfile, certDir, creds, os, arch, variant, osVersion string features, osFeatures, annotations []string tlsVerify, insecure, all bool + artifact, artifactExcludeTitles bool + artifactType, artifactLayerType string + artifactConfigType, artifactConfigFile string + artifactSubject string } type manifestRemoveOpts struct{} @@ -46,6 +52,8 @@ type manifestRemoveOpts struct{} type manifestAnnotateOpts struct { os, arch, variant, osVersion string features, osFeatures, annotations []string + index bool + subject string } type manifestInspectOpts struct { @@ -57,9 +65,9 @@ func init() { var ( manifestDescription = "\n Creates, modifies, and pushes manifest lists and image indexes." manifestCreateDescription = "\n Creates manifest lists and image indexes." - manifestAddDescription = "\n Adds an image to a manifest list or image index." - manifestRemoveDescription = "\n Removes an image from a manifest list or image index." - manifestAnnotateDescription = "\n Adds or updates information about an entry in a manifest list or image index." + manifestAddDescription = "\n Adds an image or artifact to a manifest list or image index." + manifestRemoveDescription = "\n Removes an image or artifact from a manifest list or image index." + manifestAnnotateDescription = "\n Adds or updates information about an image index or an entry in a manifest list or image index." manifestInspectDescription = "\n Display the contents of a manifest list or image index." manifestPushDescription = "\n Pushes manifest lists and image indexes to registries." manifestRmDescription = "\n Remove one or more manifest lists from local storage." @@ -103,6 +111,7 @@ func init() { flags := manifestCreateCommand.Flags() flags.BoolVar(&manifestCreateOpts.all, "all", false, "add all of the lists' images if the images to add are lists") flags.BoolVar(&manifestCreateOpts.amend, "amend", false, "modify an existing list if one with the desired name already exists") + flags.StringSliceVar(&manifestCreateOpts.annotations, "annotation", nil, "set an `annotation` for the image index") flags.StringVar(&manifestCreateOpts.os, "os", "", "if any of the specified images is a list, choose the one for `os`") if err := flags.MarkHidden("os"); err != nil { panic(fmt.Sprintf("error marking --os as hidden: %v", err)) @@ -121,17 +130,25 @@ func init() { manifestAddCommand := &cobra.Command{ Use: "add", - Short: "Add images to a manifest list or image index", + Short: "Add an image or artifact to a manifest list or image index", Long: manifestAddDescription, RunE: func(cmd *cobra.Command, args []string) error { return manifestAddCmd(cmd, args, manifestAddOpts) }, Example: `buildah manifest add mylist:v1.11 image:v1.11-amd64 - buildah manifest add mylist:v1.11 transport:imageName`, + buildah manifest add mylist:v1.11 transport:imageName + buildah manifest add --artifact --artifact-type text/plain mylist:v1.11 ./somefile.txt ./somefile.png`, Args: cobra.MinimumNArgs(2), } manifestAddCommand.SetUsageTemplate(UsageTemplate()) flags = manifestAddCommand.Flags() + flags.BoolVar(&manifestAddOpts.artifact, "artifact", false, "treat the argument as a filename and add it as an artifact") + flags.StringVar(&manifestAddOpts.artifactType, "artifact-type", "", "artifact manifest media type") + flags.StringVar(&manifestAddOpts.artifactConfigType, "artifact-config-type", imgspecv1.DescriptorEmptyJSON.MediaType, "artifact config media type") + flags.StringVar(&manifestAddOpts.artifactConfigFile, "artifact-config", "", "artifact config file") + flags.StringVar(&manifestAddOpts.artifactLayerType, "artifact-layer-type", "", "artifact layer media type") + flags.BoolVar(&manifestAddOpts.artifactExcludeTitles, "artifact-exclude-titles", false, fmt.Sprintf(`refrain from setting %q annotations on "layers"`, v1.AnnotationTitle)) + flags.StringVar(&manifestAddOpts.artifactSubject, "artifact-subject", "", "artifact subject reference") flags.StringVar(&manifestAddOpts.authfile, "authfile", auth.GetDefaultAuthFile(), "path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") flags.StringVar(&manifestAddOpts.certDir, "cert-dir", "", "use certificates at the specified path to access the registry") flags.StringVar(&manifestAddOpts.creds, "creds", "", "use `[username[:password]]` for accessing the registry") @@ -141,7 +158,7 @@ func init() { flags.StringVar(&manifestAddOpts.osVersion, "os-version", "", "override the OS `version` of the specified image") flags.StringSliceVar(&manifestAddOpts.features, "features", nil, "override the `features` of the specified image") flags.StringSliceVar(&manifestAddOpts.osFeatures, "os-features", nil, "override the OS `features` of the specified image") - flags.StringSliceVar(&manifestAddOpts.annotations, "annotation", nil, "set an `annotation` for the specified image") + flags.StringSliceVar(&manifestAddOpts.annotations, "annotation", nil, "set an `annotation` for the specified image or artifact") flags.BoolVar(&manifestAddOpts.insecure, "insecure", false, "neither require HTTPS nor verify certificates when accessing the registry. TLS verification cannot be used when talking to an insecure registry.") if err := flags.MarkHidden("insecure"); err != nil { panic(fmt.Sprintf("error marking insecure as hidden: %v", err)) @@ -185,16 +202,18 @@ func init() { return manifestAnnotateCmd(cmd, args, manifestAnnotateOpts) }, Example: `buildah manifest annotate --annotation left=right mylist:v1.11 image:v1.11-amd64`, - Args: cobra.MinimumNArgs(2), + Args: cobra.RangeArgs(1, 2), } flags = manifestAnnotateCommand.Flags() flags.StringVar(&manifestAnnotateOpts.os, "os", "", "override the `OS` of the specified image") flags.StringVar(&manifestAnnotateOpts.arch, "arch", "", "override the `Architecture` of the specified image") + flags.BoolVar(&manifestAnnotateOpts.index, "index", false, "set annotations or artifact type for the index itself instead of for an entry in the index") flags.StringVar(&manifestAnnotateOpts.variant, "variant", "", "override the `Variant` of the specified image") flags.StringVar(&manifestAnnotateOpts.osVersion, "os-version", "", "override the os `version` of the specified image") flags.StringSliceVar(&manifestAnnotateOpts.features, "features", nil, "override the `features` of the specified image") flags.StringSliceVar(&manifestAnnotateOpts.osFeatures, "os-features", nil, "override the os `features` of the specified image") flags.StringSliceVar(&manifestAnnotateOpts.annotations, "annotation", nil, "set an `annotation` for the specified image") + flags.StringVar(&manifestAnnotateOpts.subject, "subject", "", "set a subject for the image index") manifestAnnotateCommand.SetUsageTemplate(UsageTemplate()) manifestCommand.AddCommand(manifestAnnotateCommand) @@ -327,7 +346,7 @@ func manifestCreateCmd(c *cobra.Command, args []string, opts manifestCreateOpts) if err != nil { return fmt.Errorf("encountered while expanding image name %q: %w", listImageSpec, err) } - if manifestListID, err = list.SaveToImage(store, "", names, manifest.DockerV2ListMediaType); err != nil { + if manifestListID, err = list.SaveToImage(store, "", names, ""); err != nil { if errors.Is(err, storage.ErrDuplicateName) && opts.amend { for _, name := range names { manifestList, err := runtime.LookupManifestList(listImageSpec) @@ -361,6 +380,20 @@ func manifestCreateCmd(c *cobra.Command, args []string, opts manifestCreateOpts) return err } + if len(opts.annotations) != 0 { + annotations := make(map[string]string) + for _, annotationSpec := range opts.annotations { + k, v, ok := strings.Cut(annotationSpec, "=") + if !ok { + return fmt.Errorf(`no "=" found in annotation %q`, annotationSpec) + } + annotations[k] = v + } + if err := list.SetAnnotations(nil, annotations); err != nil { + return err + } + } + for _, imageSpec := range imageSpecs { ref, err := alltransports.ParseImageName(imageSpec) if err != nil { @@ -376,13 +409,12 @@ func manifestCreateCmd(c *cobra.Command, args []string, opts manifestCreateOpts) // Found local image so use that. ref = refLocal } - _, err = list.Add(getContext(), systemContext, ref, opts.all) - if err != nil { + if _, err = list.Add(getContext(), systemContext, ref, opts.all); err != nil { return err } } - imageID, err := list.SaveToImage(store, manifestListID, names, manifest.DockerV2ListMediaType) + imageID, err := list.SaveToImage(store, manifestListID, names, "") if err == nil { fmt.Printf("%s\n", imageID) } @@ -396,20 +428,29 @@ func manifestAddCmd(c *cobra.Command, args []string, opts manifestAddOpts) error listImageSpec := "" imageSpec := "" + artifactSpec := []string{} switch len(args) { case 0, 1: - return errors.New("At least a list image and an image to add must be specified") + return errors.New("At least a list image and an image or artifact to add must be specified") case 2: listImageSpec = args[0] if listImageSpec == "" { return fmt.Errorf(`Invalid image name "%s"`, args[0]) } - imageSpec = args[1] - if imageSpec == "" { - return fmt.Errorf(`Invalid image name "%s"`, args[1]) + if opts.artifact { + artifactSpec = args[1:] + } else { + imageSpec = args[1] + if imageSpec == "" { + return fmt.Errorf(`Invalid image name "%s"`, args[1]) + } } default: - return errors.New("At least two arguments are necessary: list and image to add to list") + if opts.artifact { + artifactSpec = args[1:] + } else { + return errors.New("Too many arguments: expected list and image add to list") + } } store, err := getStore(c) @@ -442,79 +483,129 @@ func manifestAddCmd(c *cobra.Command, args []string, opts manifestAddOpts) error if err != nil { return err } - ref, err := alltransports.ParseImageName(imageSpec) - if err != nil { - if ref, err = alltransports.ParseImageName(util.DefaultTransport + imageSpec); err != nil { - // check if the local image exists - if ref, _, err = util.FindImage(store, "", systemContext, imageSpec); err != nil { + + var instanceDigest digest.Digest + if opts.artifact { + var subjectRef types.ImageReference + if opts.artifactSubject != "" { + if subjectRef, err = alltransports.ParseImageName(opts.artifactSubject); err != nil { + if subjectRef, err = alltransports.ParseImageName(util.DefaultTransport + opts.artifactSubject); err != nil { + if subjectRef, _, err = util.FindImage(store, "", systemContext, opts.artifactSubject); err != nil { + logrus.Errorf("Error while trying to parse artifact subject %q: %v", opts.artifactSubject, err) + return err + } + } + } + } + var artifactType *string + if c.Flags().Changed("artifact-type") { + artifactType = &opts.artifactType + } + var artifactLayerType *string + if c.Flags().Changed("artifact-layer-type") { + artifactLayerType = &opts.artifactLayerType + } + options := manifests.AddArtifactOptions{ + ManifestArtifactType: artifactType, + LayerMediaType: artifactLayerType, + SubjectReference: subjectRef, + } + if opts.artifactConfigType != "" { + tmp := imgspecv1.DescriptorEmptyJSON + tmp.MediaType = opts.artifactConfigType + options.ConfigDescriptor = &tmp + } + if opts.artifactConfigFile != "" { + if options.ConfigDescriptor == nil { + tmp := imgspecv1.DescriptorEmptyJSON + if opts.artifactConfigType == "" { + tmp.MediaType = imgspecv1.MediaTypeImageConfig + } + options.ConfigDescriptor = &tmp + } + options.ConfigDescriptor.Size = -1 + options.ConfigFile = opts.artifactConfigFile + } + options.ExcludeTitles = opts.artifactExcludeTitles + instanceDigest, err = list.AddArtifact(getContext(), systemContext, options, artifactSpec...) + if err != nil { + logrus.Errorf("Error while trying to add artifact %q to image index: %v", artifactSpec, err) + return err + } + } else { + var ref types.ImageReference + if ref, err = alltransports.ParseImageName(imageSpec); err != nil { + if ref, err = alltransports.ParseImageName(util.DefaultTransport + imageSpec); err != nil { + if ref, _, err = util.FindImage(store, "", systemContext, imageSpec); err != nil { + return err + } + } + } + + instanceDigest, err = list.Add(getContext(), systemContext, ref, opts.all) + if err != nil { + var storeErr error + // Retry without a custom system context. A user may want to add + // a custom platform (see #3511). + if ref, _, storeErr = util.FindImage(store, "", nil, imageSpec); storeErr != nil { + logrus.Errorf("Error while trying to find image on local storage: %v", storeErr) + return err + } + instanceDigest, storeErr = list.Add(getContext(), systemContext, ref, opts.all) + if storeErr != nil { + logrus.Errorf("Error while trying to add on manifest list: %v", storeErr) return err } } } - digest, err := list.Add(getContext(), systemContext, ref, opts.all) - if err != nil { - var storeErr error - // Retry without a custom system context. A user may want to add - // a custom platform (see #3511). - if ref, _, storeErr = util.FindImage(store, "", nil, imageSpec); storeErr != nil { - logrus.Errorf("Error while trying to find image on local storage: %v", storeErr) - return err - } - digest, storeErr = list.Add(getContext(), systemContext, ref, opts.all) - if storeErr != nil { - logrus.Errorf("Error while trying to add on manifest list: %v", storeErr) - return err - } - } - if opts.os != "" { - if err := list.SetOS(digest, opts.os); err != nil { + if err := list.SetOS(instanceDigest, opts.os); err != nil { return err } } if opts.osVersion != "" { - if err := list.SetOSVersion(digest, opts.osVersion); err != nil { + if err := list.SetOSVersion(instanceDigest, opts.osVersion); err != nil { return err } } if len(opts.osFeatures) != 0 { - if err := list.SetOSFeatures(digest, opts.osFeatures); err != nil { + if err := list.SetOSFeatures(instanceDigest, opts.osFeatures); err != nil { return err } } if opts.arch != "" { - if err := list.SetArchitecture(digest, opts.arch); err != nil { + if err := list.SetArchitecture(instanceDigest, opts.arch); err != nil { return err } } if opts.variant != "" { - if err := list.SetVariant(digest, opts.variant); err != nil { + if err := list.SetVariant(instanceDigest, opts.variant); err != nil { return err } } if len(opts.features) != 0 { - if err := list.SetFeatures(digest, opts.features); err != nil { + if err := list.SetFeatures(instanceDigest, opts.features); err != nil { return err } } if len(opts.annotations) != 0 { annotations := make(map[string]string) for _, annotationSpec := range opts.annotations { - spec := strings.SplitN(annotationSpec, "=", 2) - if len(spec) != 2 { - return fmt.Errorf("no value given for annotation %q", spec[0]) + k, v, ok := strings.Cut(annotationSpec, "=") + if !ok { + return fmt.Errorf(`no "=" found in annotation %q`, annotationSpec) } - annotations[spec[0]] = spec[1] + annotations[k] = v } - if err := list.SetAnnotations(&digest, annotations); err != nil { + if err := list.SetAnnotations(&instanceDigest, annotations); err != nil { return err } } updatedListID, err := list.SaveToImage(store, manifestList.ID(), nil, "") if err == nil { - fmt.Printf("%s: %s\n", updatedListID, digest.String()) + fmt.Printf("%s: %s\n", updatedListID, instanceDigest.String()) } return err @@ -523,6 +614,7 @@ func manifestAddCmd(c *cobra.Command, args []string, opts manifestAddOpts) error func manifestRemoveCmd(c *cobra.Command, args []string, opts manifestRemoveOpts) error { listImageSpec := "" var instanceDigest digest.Digest + var instanceSpec string switch len(args) { case 0, 1: return errors.New("At least a list image and one or more instance digests must be specified") @@ -531,15 +623,10 @@ func manifestRemoveCmd(c *cobra.Command, args []string, opts manifestRemoveOpts) if listImageSpec == "" { return fmt.Errorf(`Invalid image name "%s"`, args[0]) } - instanceSpec := args[1] + instanceSpec = args[1] if instanceSpec == "" { return fmt.Errorf(`Invalid instance "%s"`, args[1]) } - d, err := digest.Parse(instanceSpec) - if err != nil { - return fmt.Errorf(`Invalid instance "%s": %v`, args[1], err) - } - instanceDigest = d default: return errors.New("At least two arguments are necessary: list and digest of instance to remove from list") } @@ -562,7 +649,36 @@ func manifestRemoveCmd(c *cobra.Command, args []string, opts manifestRemoveOpts) if err != nil { return err } - + _, list, err := manifests.LoadFromImage(store, manifestList.ID()) + if err != nil { + return err + } + d, err := list.InstanceByFile(instanceSpec) + if err != nil { + instanceRef, err := alltransports.ParseImageName(instanceSpec) + if err != nil { + if instanceRef, err = alltransports.ParseImageName(util.DefaultTransport + instanceSpec); err != nil { + if instanceRef, _, err = util.FindImage(store, "", systemContext, instanceSpec); err != nil { + return fmt.Errorf(`Invalid instance "%s": %v`, instanceSpec, err) + } + } + } + ctx := getContext() + instanceImg, err := instanceRef.NewImageSource(ctx, systemContext) + if err != nil { + return fmt.Errorf("Reading image instance: %w", err) + } + defer instanceImg.Close() + manifestBytes, _, err := instanceImg.GetManifest(ctx, nil) + if err != nil { + return fmt.Errorf("Reading image instance manifest: %w", err) + } + d, err = manifest.Digest(manifestBytes) + if err != nil { + return fmt.Errorf("Digesting image instance manifest: %w", err) + } + } + instanceDigest = d if err := manifestList.RemoveInstance(instanceDigest); err != nil { return err } @@ -611,7 +727,11 @@ func manifestRmCmd(c *cobra.Command, args []string) error { func manifestAnnotateCmd(c *cobra.Command, args []string, opts manifestAnnotateOpts) error { listImageSpec := "" - imageSpec := "" + instanceSpec := "" + if opts.subject != "" { + // this option is always only working at the index level + opts.index = true + } switch len(args) { case 0: return errors.New("At least a list image must be specified") @@ -620,17 +740,23 @@ func manifestAnnotateCmd(c *cobra.Command, args []string, opts manifestAnnotateO if listImageSpec == "" { return fmt.Errorf(`Invalid image name "%s"`, args[0]) } + if !opts.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]) } - imageSpec = args[1] - if imageSpec == "" { - return fmt.Errorf(`Invalid image name "%s"`, args[1]) + if opts.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("At least two arguments are necessary: list and image to add to list") + return errors.New("Expected either a list name and --index or a list name and an image digest or image name or artifact name") } store, err := getStore(c) @@ -664,75 +790,148 @@ func manifestAnnotateCmd(c *cobra.Command, args []string, opts manifestAnnotateO return err } - digest, err := digest.Parse(imageSpec) - if err != nil { - ctx := getContext() - ref, _, err := util.FindImage(store, "", systemContext, imageSpec) + var instance digest.Digest + if !opts.index { + d, err := list.InstanceByFile(instanceSpec) if err != nil { - return err - } - img, err := ref.NewImageSource(ctx, systemContext) - if err != nil { - return err - } - defer img.Close() - manifestBytes, _, err := img.GetManifest(ctx, nil) - if err != nil { - return err - } - digest, err = manifest.Digest(manifestBytes) - if err != nil { - return err + instanceRef, err := alltransports.ParseImageName(instanceSpec) + if err != nil { + if instanceRef, err = alltransports.ParseImageName(util.DefaultTransport + instanceSpec); err != nil { + // check if the local image exists + if instanceRef, _, err = util.FindImage(store, "", systemContext, instanceSpec); err != nil { + return fmt.Errorf(`Invalid instance "%s": %v`, instanceSpec, err) + } + } + } + ctx := getContext() + instanceImg, err := instanceRef.NewImageSource(ctx, systemContext) + if err != nil { + return fmt.Errorf("Reading image instance: %w", err) + } + defer instanceImg.Close() + manifestBytes, _, err := instanceImg.GetManifest(ctx, nil) + if err != nil { + return fmt.Errorf("Reading image instance manifest: %w", err) + } + d, err = manifest.Digest(manifestBytes) + if err != nil { + return fmt.Errorf("Digesting image instance manifest: %w", err) + } } + instance = d } if opts.os != "" { - if err := list.SetOS(digest, opts.os); err != nil { + if opts.index { + return fmt.Errorf("--index is not compatible with --os") + } + if err := list.SetOS(instance, opts.os); err != nil { return err } } if opts.osVersion != "" { - if err := list.SetOSVersion(digest, opts.osVersion); err != nil { + if opts.index { + return fmt.Errorf("--index is not compatible with --os-version") + } + if err := list.SetOSVersion(instance, opts.osVersion); err != nil { return err } } if len(opts.osFeatures) != 0 { - if err := list.SetOSFeatures(digest, opts.osFeatures); err != nil { + if opts.index { + return fmt.Errorf("--index is not compatible with --os-features") + } + if err := list.SetOSFeatures(instance, opts.osFeatures); err != nil { return err } } if opts.arch != "" { - if err := list.SetArchitecture(digest, opts.arch); err != nil { + if opts.index { + return fmt.Errorf("--index is not compatible with --arch") + } + if err := list.SetArchitecture(instance, opts.arch); err != nil { return err } } if opts.variant != "" { - if err := list.SetVariant(digest, opts.variant); err != nil { + if opts.index { + return fmt.Errorf("--index is not compatible with --variant") + } + if err := list.SetVariant(instance, opts.variant); err != nil { return err } } if len(opts.features) != 0 { - if err := list.SetFeatures(digest, opts.features); err != nil { + if opts.index { + return fmt.Errorf("--index is not compatible with --features") + } + if err := list.SetFeatures(instance, opts.features); err != nil { return err } } if len(opts.annotations) != 0 { annotations := make(map[string]string) for _, annotationSpec := range opts.annotations { - spec := strings.SplitN(annotationSpec, "=", 2) - if len(spec) != 2 { - return fmt.Errorf("no value given for annotation %q", spec[0]) + k, v, ok := strings.Cut(annotationSpec, "=") + if !ok { + return fmt.Errorf(`no "=" found in annotation %q`, annotationSpec) } - annotations[spec[0]] = spec[1] + annotations[k] = v } - if err := list.SetAnnotations(&digest, annotations); err != nil { + var instanceDigest *digest.Digest + if !opts.index { + instanceDigest = &instance + } + if err := list.SetAnnotations(instanceDigest, annotations); err != nil { + return err + } + } + if opts.subject != "" { + subjectRef, err := alltransports.ParseImageName(opts.subject) + if err != nil { + if subjectRef, err = alltransports.ParseImageName(util.DefaultTransport + opts.subject); err != nil { + // check if the local image exists + if subjectRef, _, err = util.FindImage(store, "", systemContext, opts.subject); err != nil { + logrus.Errorf("Error while trying to parse artifact subject: %v", err) + return err + } + } + } + ctx := getContext() + src, err := subjectRef.NewImageSource(ctx, systemContext) + if err != nil { + logrus.Errorf("Error while trying to read artifact subject: %v", err) + return err + } + defer src.Close() + + manifestBytes, manifestType, err := src.GetManifest(ctx, nil) + if err != nil { + logrus.Errorf("Error while trying to read artifact subject manifest: %v", err) + return err + } + manifestDigest, err := manifest.Digest(manifestBytes) + if err != nil { + logrus.Errorf("Error while trying to digest artifact subject manifest: %v", err) + return err + } + descriptor := imgspecv1.Descriptor{ + MediaType: manifestType, + Size: int64(len(manifestBytes)), + Digest: manifestDigest, + } + if err := list.SetSubject(&descriptor); err != nil { return err } } updatedListID, err := list.SaveToImage(store, manifestList.ID(), nil, "") if err == nil { - fmt.Printf("%s: %s\n", updatedListID, digest.String()) + if instance == "" { + fmt.Printf("%s\n", updatedListID) + } else { + fmt.Printf("%s: %s\n", updatedListID, instance.String()) + } } return nil diff --git a/docs/buildah-manifest-add.1.md b/docs/buildah-manifest-add.1.md index b63df248c..7c1c5816f 100644 --- a/docs/buildah-manifest-add.1.md +++ b/docs/buildah-manifest-add.1.md @@ -2,15 +2,16 @@ ## NAME -buildah\-manifest\-add - Add an image to a manifest list or image index. +buildah\-manifest\-add - Add an image or artifact to a manifest list or image index. ## SYNOPSIS -**buildah manifest add** *listNameOrIndexName* *imageName* +**buildah manifest add** [options...] *listNameOrIndexName* *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 @@ -27,7 +28,7 @@ from such a list or index will be added to the list or index. Combining **--annotation** *annotation=value* -Set an annotation on the entry for the newly-added image. +Set an annotation on the entry for the newly-added image or artifact manifest. **--arch** @@ -36,6 +37,55 @@ the image. If *imageName* refers to a manifest list or image index, the architecture information will be retrieved from it. Otherwise, it will be 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** *filename* + +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. + **--authfile** *path* Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. See containers-auth.json(5) for more information. This file is created using `buildah login`. @@ -105,5 +155,11 @@ buildah manifest add --arch arm64 --variant v8 mylist:v1.11 docker://fedora@sha2 506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:c829b1810d2dbb456e74a695fd3847530c8319e5a95dca623e9f1b1b89020d8b ``` +``` +buildah manifest add --artifact --artifact-type application/x-cd-image mylist:v1.11 ./imagefile.iso +506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:1768fae728f6f8ff3d0f8c7df409d7f4f0ca5c89b070810bd4aa4a2ed2eca8bb +``` + + ## SEE ALSO buildah(1), buildah-login(1), buildah-manifest(1), buildah-manifest-create(1), buildah-manifest-remove(1), buildah-manifest-annotate(1), buildah-manifest-inspect(1), buildah-manifest-push(1), buildah-rmi(1), docker-login(1), containers-auth.json(5) diff --git a/docs/buildah-manifest-annotate.1.md b/docs/buildah-manifest-annotate.1.md index 86e934189..c6be592c6 100644 --- a/docs/buildah-manifest-annotate.1.md +++ b/docs/buildah-manifest-annotate.1.md @@ -2,15 +2,15 @@ ## NAME -buildah\-manifest\-annotate - Add and update information about an image to a manifest list or image index. +buildah\-manifest\-annotate - Add and update information about an image or artifact to a manifest list or image index. ## SYNOPSIS -**buildah manifest annotate** [options...] *listNameOrIndexName* *imageManifestDigest* +**buildah 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. ## RETURN VALUE @@ -20,7 +20,8 @@ The list image's ID and the digest of the image's manifest. **--annotation** *annotation=value* -Set an annotation on the entry for the specified image. +Set an annotation on the entry for the specified image or artifact. If +**--index** is also specified, sets the annotation on the entire image index. **--arch** @@ -33,6 +34,12 @@ configuration information, so it is rarely necessary to use this option. Specify the features list which the list or index records as requirements for the image. This option is rarely used. +**--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** Override the OS which the list or index records as a requirement for the image. @@ -49,6 +56,12 @@ for the image. This option is rarely used. Specify the OS version which the list or index records as a requirement for the image. This option is rarely used. +**--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. + **--variant** Specify the variant which the list or index records for the image. This option @@ -62,5 +75,10 @@ buildah manifest annotate --arch arm64 --variant v8 mylist:v1.11 sha256:c829b181 506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:c829b1810d2dbb456e74a695fd3847530c8319e5a95dca623e9f1b1b89020d8b ``` +``` +buildah manifest annotate --index --annotation food=yummy mylist:v1.11 +506d8f4bb54931ea03a7e70173a0ed6302e3fb92dfadb3955ba5c17812e95c51: sha256:c829b1810d2dbb456e74a695fd3847530c8319e5a95dca623e9f1b1b89020d8b +``` + ## SEE ALSO buildah(1), buildah-manifest(1), buildah-manifest-create(1), buildah-manifest-add(1), buildah-manifest-remove(1), buildah-manifest-inspect(1), buildah-manifest-push(1), buildah-rmi(1) diff --git a/docs/buildah-manifest-create.1.md b/docs/buildah-manifest-create.1.md index 67ad77d0e..b66cff9a0 100644 --- a/docs/buildah-manifest-create.1.md +++ b/docs/buildah-manifest-create.1.md @@ -6,7 +6,7 @@ buildah\-manifest\-create - Create a manifest list or image index. ## SYNOPSIS -**buildah manifest create** *listNameOrIndexName* [*imageName* ...] +**buildah manifest create** [options...] *listNameOrIndexName* [*imageName* ...] ## DESCRIPTION @@ -35,6 +35,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** *annotation=value* + +Set an annotation on the newly-created image index. + **--tls-verify** *bool-value* Require HTTPS and verification of certificates when talking to container registries (defaults to true). TLS verification cannot be used when talking to an insecure registry. diff --git a/docs/buildah-manifest-remove.1.md b/docs/buildah-manifest-remove.1.md index f131a33b7..90f71582a 100644 --- a/docs/buildah-manifest-remove.1.md +++ b/docs/buildah-manifest-remove.1.md @@ -6,11 +6,12 @@ buildah\-manifest\-remove - Remove an image from a manifest list or image index. ## SYNOPSIS -**buildah manifest remove** *listNameOrIndexName* *imageManifestDigest* +**buildah manifest remove** *listNameOrIndexName* *imageNameOrManifestDigestOrArtifactName* ## DESCRIPTION -Removes the image with the specified digest from the specified manifest list or image index. +Removes the image with the specified name or digest from the specified manifest +list or image index, or the specified artifact from the specified image index. ## RETURN VALUE diff --git a/docs/buildah-manifest.1.md b/docs/buildah-manifest.1.md index a0c6c6411..c59754f34 100644 --- a/docs/buildah-manifest.1.md +++ b/docs/buildah-manifest.1.md @@ -11,6 +11,7 @@ The `buildah manifest` command provides subcommands which can be used to: * Create a working Docker manifest list or OCI image index. * Add an entry to a manifest list or image index for a specified image. + * Add an entry to an image index for an artifact manifest referring to a file. * Add or update information about an entry in a manifest list or image index. * Delete a working container or an image. * Push a manifest list or image index to a registry or other location. @@ -19,8 +20,8 @@ The `buildah manifest` command provides subcommands which can be used to: | Command | Man Page | Description | | ------- | -------------------------------------------------------------- | --------------------------------------------------------------------------- | -| add | [buildah-manifest-add(1)](buildah-manifest-add.1.md) | Add an image to a manifest list or image index. | -| annotate | [buildah-manifest-annotate(1)](buildah-manifest-annotate.1.md) | Add or update information about an image in a manifest list or image index. | +| add | [buildah-manifest-add(1)](buildah-manifest-add.1.md) | Add an image or artifact to a manifest list or image index. | +| annotate | [buildah-manifest-annotate(1)](buildah-manifest-annotate.1.md) | Add or update information about an image or artifact in a manifest list or image index. | | create | [buildah-manifest-create(1)](buildah-manifest-create.1.md) | Create a manifest list or image index. | | exists | [buildah-manifest-exists(1)](buildah-manifest-exists.1.md) | Check if a manifest list exists in local storage. | | inspect | [buildah-manifest-inspect(1)](buildah-manifest-inspect.1.md) | Display the contents of a manifest list or image index. | @@ -41,8 +42,8 @@ the scope of this example. Building a multi-arch manifest list $ platarch=linux/amd64,linux/ppc64le,linux/arm64,linux/s390x $ buildah build --jobs=4 --platform=$platarch --manifest shazam . -**Note:** The `--jobs` argument is optional, and the `-t` or `--tag` -option should *not* be used. +**Note:** The `--jobs` argument is optional, and the `--manifest` option +should be used instead of the`-t` or `--tag` options. ### Assembling a multi-arch manifest from separately built images diff --git a/tests/lists.bats b/tests/lists.bats index 5c388e25f..be51af6ee 100644 --- a/tests/lists.bats +++ b/tests/lists.bats @@ -12,12 +12,20 @@ IMAGE_LIST_PPC64LE_INSTANCE_DIGEST=sha256:bcf9771c0b505e68c65440474179592ffdfa98 IMAGE_LIST_S390X_INSTANCE_DIGEST=sha256:882a20ee0df7399a445285361d38b711c299ca093af978217112c73803546d5e @test "manifest-create" { + _prefetch busybox + run_buildah inspect -f '{{ .FromImageDigest }}' busybox + imagedigest="$output" run_buildah manifest create foo listid="$output" run_buildah 125 manifest create foo assert "$output" =~ "that name is already in use" run_buildah manifest create --amend foo assert "$output" == "$listid" + run_buildah manifest create --amend --annotation red=blue foo busybox + assert "$output" == "$listid" + run_buildah manifest inspect foo + assert "$output" =~ '"red": "blue"' + assert "$output" =~ "${imagedigest}" # since manifest exists in local storage this should exit with `0` run_buildah manifest exists foo # since manifest does not exist in local storage this should exit with `1` @@ -40,6 +48,33 @@ IMAGE_LIST_S390X_INSTANCE_DIGEST=sha256:882a20ee0df7399a445285361d38b711c299ca09 run_buildah manifest rm foo } +@test "manifest-add artifact" { + _prefetch busybox + createrandom $TEST_SCRATCH_DIR/randomfile2 + createrandom $TEST_SCRATCH_DIR/randomfile + run sha256sum $TEST_SCRATCH_DIR/randomfile + blobencoded="${output%% *}" + run_buildah manifest create foo + run_buildah manifest add --artifact --artifact-type image/jpeg --artifact-layer-type image/not-validated --artifact-config-type text/x-not-really --artifact-subject busybox foo $TEST_SCRATCH_DIR/randomfile2 + run_buildah manifest add --artifact --artifact-type image/png --artifact-layer-type image/not-validated --artifact-config-type text/x-not-really --artifact-subject busybox foo $TEST_SCRATCH_DIR/randomfile + digest="${output##* }" + alg="${digest%%:*}" + encoded="${digest##*:}" + run_buildah manifest annotate --annotation red=blue foo $TEST_SCRATCH_DIR/randomfile + run_buildah manifest inspect foo + assert "$output" =~ '"image/png"' + assert "$output" =~ '"red": "blue"' + run_buildah manifest push --all foo oci:$TEST_SCRATCH_DIR/pushed + run cmp $TEST_SCRATCH_DIR/randomfile $TEST_SCRATCH_DIR/pushed/blobs/sha256/$blobencoded + assert "$status" -eq 0 "pushed copy of random file did not match original" + run cat $TEST_SCRATCH_DIR/pushed/blobs/$alg/$encoded + assert "$status" -eq 0 "artifact manifest not found in expected location" + assert "$output" =~ '"artifactType":"image/png"' "cat $TEST_SCRATCH_DIR/pushed/blobs/$alg/$encoded" + assert "$output" =~ '"mediaType":"image/not-validated"' "cat $TEST_SCRATCH_DIR/pushed/blobs/$alg/$encoded" + assert "$output" =~ '"mediaType":"text/x-not-really"' "cat $TEST_SCRATCH_DIR/pushed/blobs/$alg/$encoded" + run_buildah manifest rm foo +} + @test "manifest-add local image" { target=scratch-image run_buildah bud $WITH_POLICY_JSON -t ${target} $BUDFILES/from-scratch @@ -70,6 +105,44 @@ IMAGE_LIST_S390X_INSTANCE_DIGEST=sha256:882a20ee0df7399a445285361d38b711c299ca09 expect_output --substring ${IMAGE_LIST_S390X_INSTANCE_DIGEST} } +@test "manifest-annotate global annotation" { + _prefetch busybox + run_buildah manifest create foo + run_buildah manifest add foo busybox + run_buildah manifest annotate --index --annotation red=blue foo + run_buildah manifest inspect foo + assert "$output" =~ '"red": "blue"' +} + +@test "manifest-annotate instance annotation" { + _prefetch busybox + run_buildah manifest create foo + run_buildah manifest add foo busybox + instance="${output##* }" + run_buildah manifest annotate --annotation red=blue foo "${instance}" + run_buildah manifest annotate --os OperatingSystem foo "${instance}" + run_buildah manifest annotate --arch aRCHITECTURE foo "${instance}" + run_buildah manifest annotate --variant vARIANT foo "${instance}" + run_buildah manifest annotate --features FEATURE1 --features FEATURE2 foo "${instance}" + run_buildah manifest annotate --os-features OSFEATURE1 --os-features OSFEATURE2 foo "${instance}" + run_buildah manifest inspect foo + assert "$output" =~ '"red": "blue"' + assert "$output" =~ '"os": "OperatingSystem"' + assert "$output" =~ '"architecture": "aRCHITECTURE"' + assert "$output" =~ '"variant": "vARIANT"' +} + +@test "manifest-annotate subject" { + _prefetch busybox "${IMAGE_LIST_INSTANCE##*://}" + run_buildah manifest create foo + run_buildah manifest add foo busybox + run_buildah manifest annotate --subject "${IMAGE_LIST_INSTANCE##*://}" foo + run_buildah inspect -f '{{ .FromImageDigest }}' "${IMAGE_LIST_INSTANCE##*://}" + imagedigest="$output" + run_buildah manifest inspect foo + assert "$output" =~ "$imagedigest" +} + @test "manifest-remove" { run_buildah manifest create foo run_buildah manifest add --all foo ${IMAGE_LIST}