diff --git a/docs/helm_annotations.md b/docs/helm_annotations.md index b40c4a0ac..b770ae137 100644 --- a/docs/helm_annotations.md +++ b/docs/helm_annotations.md @@ -34,6 +34,8 @@ By default, Artifact Hub will try to extract the containers images used by Helm Containers images will be scanned for security vulnerabilities. The security report generated will be available in the package detail view. It is possible to whitelist images so that they are not scanned by setting the `whitelisted` flag to true. +When the standard [helm.sh/images](#standard-annotations) annotation is also present, the images listed in it are processed first and the ones provided in this annotation are added to that list. Entries using an image reference already listed in the `helm.sh/images` annotation override the existing one, which allows extending them with Artifact Hub specific fields like `whitelisted` or `platforms`. + - **artifacthub.io/crds** *(yaml string, see example below)* This annotation can be used to list the operator's CRDs. They will be visible in the package's detail view as cards. @@ -82,12 +84,27 @@ This annotation can be used to provide some screenshots that will be featured in This annotation can be used to provide some information about the key used to sign a given chart version. This information will be displayed on the Artifact Hub UI, making it easier for users to get the information they need to verify the integrity and origin of your chart. The `url` field indicates where users can find the public key and it is mandatory when a sign key entry is provided. +## Standard annotations + +In addition to the Artifact Hub specific annotations listed above, some standard annotations defined by the Helm community are supported as well. + +- **helm.sh/images** *(yaml string, see example below)* + +This annotation, defined in [HIP-0015](https://github.com/helm/community/blob/main/hips/hip-0015.md), allows chart maintainers to list the containers images used by the chart. Each entry must provide at least a `name` and an `image` field. Additional fields, like the Artifact Hub specific `whitelisted` and `platforms` ones, may be provided as well. + +The images listed in this annotation are processed **before** the ones in the `artifacthub.io/images` annotation, and both lists are combined. Please see the [artifacthub.io/images](#supported-annotations) annotation for more details about how they are merged. Containers images provided this way replace the ones Artifact Hub extracts automatically from the manifests generated from a dry-run install using the default values. + ## Example Artifact Hub annotations in `Chart.yaml`: ```yaml annotations: + helm.sh/images: | + - name: img1 + image: repo/img1:1.0.0 + - name: img2 + image: repo/img2:2.0.0 artifacthub.io/category: security artifacthub.io/changes: | - Added cool feature diff --git a/internal/tracker/source/helm/helm.go b/internal/tracker/source/helm/helm.go index f088e10f8..70d5f433c 100644 --- a/internal/tracker/source/helm/helm.go +++ b/internal/tracker/source/helm/helm.go @@ -54,6 +54,8 @@ const ( securityUpdatesAnnotation = "artifacthub.io/containsSecurityUpdates" signKeyAnnotation = "artifacthub.io/signKey" + helmImagesAnnotation = "helm.sh/images" + legacyChartContentLayerMediaType = "application/tar+gzip" ChartContentLayerMediaType = "application/vnd.cncf.helm.chart.content.v1.tar+gzip" ChartProvenanceLayerMediaType = "application/vnd.cncf.helm.chart.provenance.v1.prov" @@ -659,11 +661,34 @@ func EnrichPackageFromAnnotations(p *hub.Package, annotations map[string]string) } // Images - if v, ok := annotations[imagesAnnotation]; ok { + // The images provided in the helm.sh/images annotation are processed + // first. The ones provided in the artifacthub.io/images annotation are + // added to that list, overriding the entries that use the same image + // reference. + helmImagesValue, helmImagesProvided := annotations[helmImagesAnnotation] + hubImagesValue, hubImagesProvided := annotations[imagesAnnotation] + if helmImagesProvided || hubImagesProvided { var images []*hub.ContainerImage - if err := yaml.Unmarshal([]byte(v), &images); err != nil { - errs = multierror.Append(errs, fmt.Errorf("%w: invalid images value", errInvalidAnnotation)) - } else { + imagesParsed := true + if helmImagesProvided { + helmImages, err := parseContainersImages(helmImagesValue) + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("%w: invalid helm images value", errInvalidAnnotation)) + imagesParsed = false + } else { + images = helmImages + } + } + if hubImagesProvided { + hubImages, err := parseContainersImages(hubImagesValue) + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("%w: invalid images value", errInvalidAnnotation)) + imagesParsed = false + } else { + images = mergeContainersImages(images, hubImages) + } + } + if imagesParsed { if err := pkg.ValidateContainersImages(hub.Helm, images); err != nil { errs = multierror.Append(errs, fmt.Errorf("%w: %w", errInvalidAnnotation, err)) } else { @@ -821,3 +846,39 @@ func contains(l []string, e string) bool { } return false } + +// parseContainersImages parses the containers images provided in an +// annotation's value. +func parseContainersImages(v string) ([]*hub.ContainerImage, error) { + var images []*hub.ContainerImage + if err := yaml.Unmarshal([]byte(v), &images); err != nil { + return nil, err + } + for _, image := range images { + if image == nil { + return nil, errors.New("invalid containers images entry") + } + } + return images, nil +} + +// mergeContainersImages adds the extra containers images provided to the base +// list. Entries using an image reference already present in the base list +// override the existing one. +func mergeContainersImages(base, extra []*hub.ContainerImage) []*hub.ContainerImage { + images := base + for _, extraImage := range extra { + var found bool + for i, image := range images { + if image.Image == extraImage.Image { + images[i] = extraImage + found = true + break + } + } + if !found { + images = append(images, extraImage) + } + } + return images +} diff --git a/internal/tracker/source/helm/helm_test.go b/internal/tracker/source/helm/helm_test.go index 618f6bf10..78ed3f09b 100644 --- a/internal/tracker/source/helm/helm_test.go +++ b/internal/tracker/source/helm/helm_test.go @@ -725,6 +725,88 @@ func TestEnrichPackageFromAnnotations(t *testing.T) { &hub.Package{}, "invalid image reference: could not parse reference", }, + { + &hub.Package{}, + map[string]string{ + helmImagesAnnotation: ` +- name: img1 + image: repo/img1:1.0.0 +- name: img2 + image: repo/img2:2.0.0 +`, + }, + &hub.Package{ + ContainersImages: []*hub.ContainerImage{ + { + Name: "img1", + Image: "repo/img1:1.0.0", + }, + { + Name: "img2", + Image: "repo/img2:2.0.0", + }, + }, + }, + "", + }, + { + &hub.Package{}, + map[string]string{ + helmImagesAnnotation: ` +- name: img1 + image: repo/img1:1.0.0 +- name: img2 + image: repo/img2:2.0.0 +`, + imagesAnnotation: ` +- name: img2 + image: repo/img2:2.0.0 + whitelisted: true +- name: img3 + image: repo/img3:3.0.0 + platforms: + - linux/amd64 +`, + }, + &hub.Package{ + ContainersImages: []*hub.ContainerImage{ + { + Name: "img1", + Image: "repo/img1:1.0.0", + }, + { + Name: "img2", + Image: "repo/img2:2.0.0", + Whitelisted: true, + }, + { + Name: "img3", + Image: "repo/img3:3.0.0", + Platforms: []string{"linux/amd64"}, + }, + }, + }, + "", + }, + { + &hub.Package{}, + map[string]string{ + helmImagesAnnotation: ` +- name: img1 + image: ":" +`, + }, + &hub.Package{}, + "invalid image reference: could not parse reference", + }, + { + &hub.Package{}, + map[string]string{ + helmImagesAnnotation: "-", + }, + &hub.Package{}, + "invalid annotation: invalid helm images value", + }, // License { &hub.Package{},