diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8b7da7a3a..b675146b5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -59,7 +59,7 @@ updates: interval: "weekly" ignore: - dependency-name: "node" - versions: ["19-alpine3.19", "20-alpine3.19"] + versions: ["19-alpine3.19", "20-alpine3.19", "21-alpine3.19"] - package-ecosystem: "docker" directory: "/cmd/scanner" diff --git a/README.md b/README.md index 3912b68ad..5e6b0ad08 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ At the moment, the following artifacts kinds are supported *(with plans to suppo - [CoreDNS plugins](https://coredns.io/) - [Falco configurations](https://falco.org/) - [Gatekeeper policies](https://open-policy-agent.github.io/gatekeeper/website/docs/) +- [Headlamp plugins](https://headlamp.dev) - [Helm charts](https://helm.sh/) - [Helm plugins](https://helm.sh/docs/topics/plugins/) - [KCL modules](https://kcl-lang.io) diff --git a/charts/artifact-hub/Chart.yaml b/charts/artifact-hub/Chart.yaml index c6f74c0d3..1a09e3286 100644 --- a/charts/artifact-hub/Chart.yaml +++ b/charts/artifact-hub/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: artifact-hub description: Artifact Hub is a web-based application that enables finding, installing, and publishing Kubernetes packages. type: application -version: 1.16.1-4 +version: 1.16.1-5 appVersion: 1.16.0 kubeVersion: ">= 1.19.0-0" home: https://artifacthub.io @@ -28,6 +28,7 @@ keywords: - argo - kubearmor - kcl + - headlamp maintainers: - name: Sergio email: tegioz@icloud.com diff --git a/charts/artifact-hub/values.schema.json b/charts/artifact-hub/values.schema.json index 61e677575..2a61db4e5 100644 --- a/charts/artifact-hub/values.schema.json +++ b/charts/artifact-hub/values.schema.json @@ -1162,7 +1162,7 @@ }, "repositoriesKinds": { "title": "Repositories kinds to process ([] = all)", - "description": "The following kinds are supported at the moment: falco, helm, olm, opa, tbaction, krew, helm-plugin, tekton-task, keda-scaler, coredns, keptn, tekton-pipeline, container, kubewarden, gatekeeper, kyverno, knative-client-plugin, backstage, argo-template, kubearmor, kcl", + "description": "The following kinds are supported at the moment: falco, helm, olm, opa, tbaction, krew, helm-plugin, tekton-task, keda-scaler, coredns, keptn, tekton-pipeline, container, kubewarden, gatekeeper, kyverno, knative-client-plugin, backstage, argo-template, kubearmor, kcl, headlamp", "type": "array", "items": { "type": "string" diff --git a/cmd/ah/lint.go b/cmd/ah/lint.go index 06c80df56..c1a8fdaef 100644 --- a/cmd/ah/lint.go +++ b/cmd/ah/lint.go @@ -90,7 +90,7 @@ func newLintCmd() *cobra.Command { return lint(opts, &output{cmd.OutOrStdout()}) }, } - lintCmd.Flags().StringVarP(&opts.kind, "kind", "k", "helm", "repository kind: argo-template, backstage, coredns, falco, gatekeeper, helm, helm-plugin, kcl, keda-scaler, keptn, knative-client-plugin, krew, kubearmor, kubewarden, kyverno, olm, opa, tbaction, tekton-task, tekton-pipeline") + lintCmd.Flags().StringVarP(&opts.kind, "kind", "k", "helm", "repository kind: argo-template, backstage, coredns, falco, gatekeeper, headlamp, helm, helm-plugin, kcl, keda-scaler, keptn, knative-client-plugin, krew, kubearmor, kubewarden, kyverno, olm, opa, tbaction, tekton-task, tekton-pipeline") lintCmd.Flags().StringVarP(&opts.path, "path", "p", ".", "repository's packages path") return lintCmd } @@ -114,6 +114,7 @@ func lint(opts *lintOptions, out *output) error { hub.CoreDNS, hub.Falco, hub.Gatekeeper, + hub.Headlamp, hub.KCL, hub.KedaScaler, hub.Keptn, @@ -617,6 +618,7 @@ func (out *output) printPkgDetails(pkg *hub.Package) { hub.CoreDNS, hub.Falco, hub.Gatekeeper, + hub.Headlamp, hub.KCL, hub.KedaScaler, hub.Keptn, diff --git a/database/migrations/schema/053_headlamp_plugins.sql b/database/migrations/schema/053_headlamp_plugins.sql new file mode 100644 index 000000000..40153cc4a --- /dev/null +++ b/database/migrations/schema/053_headlamp_plugins.sql @@ -0,0 +1,5 @@ +insert into repository_kind values (21, 'Headlamp plugins'); + +---- create above / drop below ---- + +delete from repository_kind where repository_kind_id = 21; diff --git a/database/tests/schema/schema.sql b/database/tests/schema/schema.sql index 97b1bbf99..c59fa09a8 100644 --- a/database/tests/schema/schema.sql +++ b/database/tests/schema/schema.sql @@ -560,7 +560,8 @@ select results_eq( (17, 'Backstage plugins'), (18, 'Argo templates'), (19, 'KubeArmor policies'), - (20, 'KCL modules') + (20, 'KCL modules'), + (21, 'Headlamp plugins') $$, 'Repository kinds should exist' ); diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index dd4579496..a2082dba5 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1299,6 +1299,30 @@ paths: $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/InternalServerError" + "/packages/headlamp/{repoName}/{packageName}": + get: + tags: + - Packages + summary: Get package details + description: Get package details + operationId: getHeadlampPluginDetails + parameters: + - $ref: "#/components/parameters/RepoNameParam" + - $ref: "#/components/parameters/PackageNameParam" + responses: + "200": + description: "" + content: + application/json: + schema: + $ref: "#/components/schemas/HeadlampPluginPackage" + "404": + $ref: "#/components/responses/NotFoundResponse" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalServerError" + "/packages/kcl/{repoName}/{packageName}": get: tags: @@ -1790,7 +1814,31 @@ paths: $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/InternalServerError" - "/packages/KCL/{repoName}/{packageName}/{version}": + "/packages/headlamp/{repoName}/{packageName}/{version}": + get: + tags: + - Packages + summary: Get package version details + description: Get package version details + operationId: getHeadlampVersionDetails + parameters: + - $ref: "#/components/parameters/RepoNameParam" + - $ref: "#/components/parameters/PackageNameParam" + - $ref: "#/components/parameters/VersionParam" + responses: + "200": + description: "" + content: + application/json: + schema: + $ref: "#/components/schemas/HeadlampPluginPackage" + "404": + $ref: "#/components/responses/NotFoundResponse" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalServerError" + "/packages/kcl/{repoName}/{packageName}/{version}": get: tags: - Packages @@ -3967,6 +4015,27 @@ components: example: "http://repo.url" HelmPluginPackage: $ref: "#/components/schemas/Package" + HeadlampPluginPackage: + allOf: + - $ref: "#/components/schemas/Package" + - type: object + properties: + data: + type: object + nullable: false + properties: + headlamp/plugin/archive-url: + type: string + example: "https://headlamp.dev/sample-plugin-url" + headlamp/plugin/distro-compat: + type: string + example: "in-cluster,web,docker-desktop" + headlamp/plugin/version-compat: + type: string + example: ">=1.2.3" + headlamp/plugin/archive-checksum: + type: string + example: "sha256:123456..." KCLPackage: $ref: "#/components/schemas/Package" KedaScalerPackage: @@ -4689,6 +4758,7 @@ components: - 18 - 19 - 20 + - 21 description: | Repository kind: * `0` - Helm charts @@ -4712,6 +4782,7 @@ components: * `18` - Argo templates * `19` - KubeArmor templates * `20` - KCL packages + * `21` - Headlamp plugins RepositoryKindParam: type: string enum: @@ -4736,6 +4807,7 @@ components: - argo-template - kubearmor - kcl + - headlamp description: | Repository kind name: * `helm` - Helm charts @@ -4759,6 +4831,7 @@ components: * `argo-template` - Argo templates * `kubearmor` - KubeArmor policies * `kcl` - KCL packages + * `headlamp` - Headlamp plugins RepositorySummary: type: object required: @@ -5299,6 +5372,7 @@ components: * `18` - Argo templates * `19` - KubeArmor templates * `20` - KCL packages + * `21` - Headlamp plugins PackageNameParam: in: path name: packageName diff --git a/docs/headlamp_annotations.md b/docs/headlamp_annotations.md new file mode 100644 index 000000000..5056940e1 --- /dev/null +++ b/docs/headlamp_annotations.md @@ -0,0 +1,32 @@ +# Headlamp annotations + +You can provide some extra information about your Headlamp plugins by using the `annotations` field in the [Artifact Hub package metadata file](https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-pkg.yml). + +## Supported annotations + +- **headlamp/plugin/archive-url** *(string, required)* + +Plugin archive tarball URL (e.g. "https://.../my-archive.tar.gz"). + +- **headlamp/plugin/archive-checksum** *(string, required)* + +Plugin archive tarball checksum (e.g. "sha256:MY_CHECKSUM"). + +- **headlamp/plugin/version-compat** *(string, optional)* + +Headlamp versions this plugin is compatible with (e.g. ">=1.2.3"). + +- **headlamp/plugin/distro-compat** *(string, optional)* + +Headlamp flavor this plugin is compatible with (e.g. one or more of app, in-cluster, web, docker-desktop, linux, windows, mac). + +## Example + +```yaml +... +annotations: + headlamp/plugin/archive-url: "https://.../my-archive.tar.gz" + headlamp/plugin/archive-checksum: "sha256:MY_CHECKSUM" + headlamp/plugin/version-compat: ">=1.2.3" + headlamp/plugin/distro-compat: "in-cluster,web,docker-desktop" +``` diff --git a/docs/headlamp_plugins_repositories.md b/docs/headlamp_plugins_repositories.md new file mode 100644 index 000000000..cd912ea24 --- /dev/null +++ b/docs/headlamp_plugins_repositories.md @@ -0,0 +1,49 @@ +## Headlamp plugins repositories + +Headlamp plugins repositories are expected to be hosted in GitHub, GitLab or Bitbucket repos. When adding your repository to Artifact Hub, the url used **must** follow the following format: + +- `https://github.com/user/repo[/path/to/packages]` +- `https://gitlab.com/user/repo[/path/to/packages]` +- `https://bitbucket.org/user/repo[/path/to/packages]` + +By default the `master` branch is used, but it's possible to specify a different one from the UI. + +*Please NOTE that the repository URL used when adding the repository to Artifact Hub **must NOT** contain the git hosting platform specific parts, like **tree/branch**, just the path to your packages like it would show in the filesystem.* + +The *path/to/packages* provided can contain metadata for one or more packages. Each package version **must** be on a separate folder, and it's up to you to decide if you want to publish one or multiple versions of your package. + +The structure of a repository with multiple plugins packages and versions could look something like this: + +```sh +$ tree path/to/packages +path/to/packages +├── artifacthub-repo.yml +├── package1 +│   ├── 1.0.0 +│   │   ├── README.md +│   │   └── artifacthub-pkg.yml +│   └── 2.0.0 +│      ├── README.md +│   └── artifacthub-pkg.yml +└── package2 + └── 1.0.0 +       ├── README.md + └── artifacthub-pkg.yml +``` + +This structure is flexible, and in some cases where you only have a package and a version it can be greatly simplified. In the case of a single package with a single version available at a time (the publisher doesn't want to make previous ones available, for example), the structure could look like this: + +```sh +$ tree path/to/packages +path/to/packages +├── artifacthub-repo.yml +└── package1 +    ├── README.md + └── artifacthub-pkg.yml +``` + +In the previous case, even the `package1` directory could be omitted. The reason is that both packages names and versions are read from the `artifacthub-pkg.yml` metadata file, so directories names are not used at all. + +Each package version **needs** an `artifacthub-pkg.yml` metadata file. Please see the file [spec](https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-pkg.yml) and the [Headlamp annotations documentation](https://github.com/artifacthub/hub/blob/master/docs/headlamp_annotations.md) for more details. The [artifacthub-repo.yml](https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-repo.yml) repository metadata file shown above can be used to setup features like [Verified publisher](https://github.com/artifacthub/hub/blob/master/docs/repositories.md#verified-publisher) or [Ownership claim](https://github.com/artifacthub/hub/blob/master/docs/repositories.md#ownership-claim). This file must be located at `/path/to/packages`. + +Once you have added your repository, you are all set up. As you add new versions of your plugins packages or new packages to your git repository, they'll be automatically indexed and listed in Artifact Hub. diff --git a/docs/repositories.md b/docs/repositories.md index 0945e7285..92b31a9db 100644 --- a/docs/repositories.md +++ b/docs/repositories.md @@ -9,6 +9,7 @@ The following repositories kinds are supported at the moment: - [Containers images repositories](https://github.com/artifacthub/hub/blob/master/docs/container_images_repositories.md) - [CoreDNS plugins repositories](https://github.com/artifacthub/hub/blob/master/docs/coredns_plugins_repositories.md) - [Falco rules repositories](https://github.com/artifacthub/hub/blob/master/docs/falco_rules_repositories.md) +- [Headlamp plugins repositories](https://github.com/artifacthub/hub/blob/master/docs/headlamp_plugins_repositories.md) - [Helm charts repositories](https://github.com/artifacthub/hub/blob/master/docs/helm_charts_repositories.md) - [Helm plugins repositories](https://github.com/artifacthub/hub/blob/master/docs/helm_plugins_repositories.md) - [KCL modules repositories](https://github.com/artifacthub/hub/blob/master/docs/kcl_modules_repositories.md) diff --git a/docs/www/content/_index.md b/docs/www/content/_index.md index a74eaf8b6..09fa1657a 100644 --- a/docs/www/content/_index.md +++ b/docs/www/content/_index.md @@ -13,14 +13,15 @@ The documentation is organized in the following topics: | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Repositories guide](/docs/topics/repositories) | The repositories guide explains how to add repositories to Artifact Hub, as well as other related concepts like Verified publisher or Ownership Claim. | | [Argo annotations](/docs/topics/annotations/argo) | Describes some custom annotations that allow enriching the existing metadata in Argo templates to improve users' experience in Artifact Hub. | +| [Headlamp annotations](/docs/topics/annotations/headlamp) | Describes some custom annotations that allow enriching the existing metadata in Headlamp plugins to improve users' experience in Artifact Hub. | | [Helm annotations](/docs/topics/annotations/helm) | Describes some custom annotations that allow enriching the existing metadata in Helm Charts to improve users' experience in Artifact Hub. | | [Keptn annotations](/docs/topics/annotations/keptn) | Describes some custom annotations that allow enriching the existing metadata in Keptn integrations to improve users' experience in Artifact Hub. | | [Krew annotations](/docs/topics/annotations/krew) | Describes some custom annotations that allow enriching the existing metadata in Krew kubectl plugins to improve users' experience in Artifact Hub. | | [Kubewarden annotations](/docs/topics/annotations/kubewarden) | Describes some custom annotations that allow enriching the existing metadata in Kubewarden policies to improve users' experience in Artifact Hub. | | [Kyverno annotations](/docs/topics/annotations/kyverno) | Describes some custom annotations that allow enriching the existing metadata in Kyverno policies to improve users' experience in Artifact Hub. | | [OLM annotations](/docs/topics/annotations/olm) | Describes some custom annotations that allow enriching the existing metadata in OLM operators to improve users' experience in Artifact Hub. | -| [Tekton annotations](/docs/topics/annotations/tekton) | Describes some custom annotations that allow enriching the existing metadata in Tekton tasks to improve users' experience in Artifact Hub. -| [Embedding artifacts](/docs/topics/embedding_artifacts) | Explains how to embed a single artifact or a group of them in other websites. | +| [Tekton annotations](/docs/topics/annotations/tekton) | Describes some custom annotations that allow enriching the existing metadata in Tekton tasks to improve users' experience in Artifact Hub. | +| [Embedding artifacts](/docs/topics/embedding_artifacts) | Explains how to embed a single artifact or a group of them in other websites. | | [Packages security report](/docs/topics/security_report) | Explains how packages are scanned for security vulnerabilities and the structure of the security report. | | [Authorization](/docs/topics/authorization) | Explains how the authorization mechanism that allows organizations to define what actions can be performed by their members works and how to set it up. | | [Architecture](/docs/topics/architecture) | Describes the components that form Artifact Hub, what each of them do and the layout of the source repository. | diff --git a/docs/www/headers/headlamp_annotations b/docs/www/headers/headlamp_annotations new file mode 100644 index 000000000..d12b9186c --- /dev/null +++ b/docs/www/headers/headlamp_annotations @@ -0,0 +1,6 @@ +--- +title: "Headlamp" +aliases: [ + "/headlamp_annotations", +] +--- diff --git a/docs/www/headers/headlamp_plugins_repositories b/docs/www/headers/headlamp_plugins_repositories new file mode 100644 index 000000000..9d95d4dee --- /dev/null +++ b/docs/www/headers/headlamp_plugins_repositories @@ -0,0 +1,6 @@ +--- +title: "Headlamp plugins" +aliases: [ + "/headlamp_plugins_repositories", +] +--- diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index caac8aedc..27ae13a76 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -265,7 +265,7 @@ func (h *Handlers) setupRouter() { r.Get("/stats", h.Packages.GetStats) r.With(corsMW).Get("/search", h.Packages.Search) r.With(h.Users.RequireLogin).Get("/starred", h.Packages.GetStarredByUser) - r.Route("/{^helm$|^falco$|^opa$|^olm|^tbaction|^krew|^helm-plugin|^tekton-task|^keda-scaler|^coredns|^keptn|^tekton-pipeline|^container|^kubewarden|^gatekeeper|^kyverno|^knative-client-plugin|^backstage|^argo-template|^kubearmor|^kcl$}/{repoName}/{packageName}", func(r chi.Router) { + r.Route("/{^helm$|^falco$|^opa$|^olm|^tbaction|^krew|^helm-plugin|^tekton-task|^keda-scaler|^coredns|^keptn|^tekton-pipeline|^container|^kubewarden|^gatekeeper|^kyverno|^knative-client-plugin|^backstage|^argo-template|^kubearmor|^kcl|^headlamp$}/{repoName}/{packageName}", func(r chi.Router) { r.Get("/feed/rss", h.Packages.RssFeed) r.With(corsMW).Get("/summary", h.Packages.GetSummary) r.Get("/{version}", h.Packages.Get) @@ -430,7 +430,7 @@ func (h *Handlers) setupRouter() { // Index special entry points r.Route("/packages", func(r chi.Router) { - r.Route("/{^helm$|^falco$|^opa$|^olm|^tbaction|^krew|^helm-plugin|^tekton-task|^keda-scaler|^coredns|^keptn|^tekton-pipeline|^container|^kubewarden|^gatekeeper|^kyverno|^knative-client-plugin|^backstage|^argo-template|^kubearmor|^kcl$}/{repoName}/{packageName}", func(r chi.Router) { + r.Route("/{^helm$|^falco$|^opa$|^olm|^tbaction|^krew|^helm-plugin|^tekton-task|^keda-scaler|^coredns|^keptn|^tekton-pipeline|^container|^kubewarden|^gatekeeper|^kyverno|^knative-client-plugin|^backstage|^argo-template|^kubearmor|^kcl|^headlamp$}/{repoName}/{packageName}", func(r chi.Router) { r.With(h.Packages.InjectIndexMeta).Get("/{version}", h.Static.Index) r.With(h.Packages.InjectIndexMeta).Get("/", h.Static.Index) }) diff --git a/internal/handlers/pkg/handlers_test.go b/internal/handlers/pkg/handlers_test.go index 26d19b696..805852056 100644 --- a/internal/handlers/pkg/handlers_test.go +++ b/internal/handlers/pkg/handlers_test.go @@ -2155,6 +2155,17 @@ func TestBuildURL(t *testing.T) { "2.0.0", baseURL + "/packages/kcl/repo1/pkg1/2.0.0", }, + { + &hub.Package{ + NormalizedName: "pkg1", + Repository: &hub.Repository{ + Kind: hub.Headlamp, + Name: "repo1", + }, + }, + "2.0.0", + baseURL + "/packages/headlamp/repo1/pkg1/2.0.0", + }, } for _, tc := range testCases { tc := tc diff --git a/internal/hub/repo.go b/internal/hub/repo.go index 5edc7967c..44f8566cf 100644 --- a/internal/hub/repo.go +++ b/internal/hub/repo.go @@ -109,6 +109,9 @@ const ( // KCL represents a repository with KCL modules. KCL RepositoryKind = 20 + + // Headlamp represents a repository with Headlamp plugins. + Headlamp RepositoryKind = 21 ) // GetKindName returns the name of the provided repository kind. @@ -126,6 +129,8 @@ func GetKindName(kind RepositoryKind) string { return "falco" case Gatekeeper: return "gatekeeper" + case Headlamp: + return "headlamp" case Helm: return "helm" case HelmPlugin: @@ -177,6 +182,8 @@ func GetKindFromName(kind string) (RepositoryKind, error) { return Falco, nil case "gatekeeper": return Gatekeeper, nil + case "headlamp": + return Headlamp, nil case "helm": return Helm, nil case "helm-plugin": diff --git a/internal/pkg/metadata.go b/internal/pkg/metadata.go index f510c2a82..049abc933 100644 --- a/internal/pkg/metadata.go +++ b/internal/pkg/metadata.go @@ -29,6 +29,13 @@ var ( // ErrInvalidMetadata indicates that the metadata provided is not valid. ErrInvalidMetadata = errors.New("invalid metadata") + // headlampRequiredAnnotations represents a list of annotations that must + // be present in Headlamp plugins packages. + headlampRequiredAnnotations = []string{ + "headlamp/plugin/archive-url", + "headlamp/plugin/archive-checksum", + } + // validChangeKinds is the list of valid kinds that a pkg change can use. validChangeKinds = []string{ "added", @@ -119,49 +126,70 @@ func PreparePackageFromMetadata(md *hub.PackageMetadata) (*hub.Package, error) { func ValidatePackageMetadata(kind hub.RepositoryKind, md *hub.PackageMetadata) error { var errs *multierror.Error + // Version if md.Version == "" { errs = multierror.Append(errs, fmt.Errorf("%w: %s", ErrInvalidMetadata, "version not provided")) } else if _, err := semver.NewVersion(md.Version); err != nil { errs = multierror.Append(errs, fmt.Errorf("%w: %s: %w", ErrInvalidMetadata, "invalid version (semver expected)", err)) } + + // Name, display name and alternative name if md.Name == "" { errs = multierror.Append(errs, fmt.Errorf("%w: %s", ErrInvalidMetadata, "name not provided")) } + if md.DisplayName == "" { + errs = multierror.Append(errs, fmt.Errorf("%w: %s", ErrInvalidMetadata, "display name not provided")) + } if md.AlternativeName != "" && !strings.Contains(md.Name, md.AlternativeName) && !strings.Contains(md.AlternativeName, md.Name) { errs = multierror.Append(errs, fmt.Errorf("%w: %s", ErrInvalidMetadata, "invalid alternative name (must be a subset or superset of the name)")) } + + // Category if md.Category != "" { if _, err := hub.PackageCategoryFromName(md.Category); err != nil { errs = multierror.Append(errs, fmt.Errorf("%w: %w", ErrInvalidMetadata, err)) } } - if md.DisplayName == "" { - errs = multierror.Append(errs, fmt.Errorf("%w: %s", ErrInvalidMetadata, "display name not provided")) - } + + // Created at if md.CreatedAt == "" { errs = multierror.Append(errs, fmt.Errorf("%w: %s", ErrInvalidMetadata, "createdAt not provided")) } else if _, err := time.Parse(time.RFC3339, md.CreatedAt); err != nil { errs = multierror.Append(errs, fmt.Errorf("%w: %s: %w", ErrInvalidMetadata, "invalid createdAt (RFC3339 expected)", err)) } + + // Description if md.Description == "" { errs = multierror.Append(errs, fmt.Errorf("%w: %s", ErrInvalidMetadata, "description not provided")) + } + + // Maintainers for _, maintainer := range md.Maintainers { if maintainer.Email == "" { errs = multierror.Append(errs, fmt.Errorf("%w: %s", ErrInvalidMetadata, "maintainer email not provided")) } } + + // Changes for _, change := range md.Changes { if err := ValidateChange(change); err != nil { errs = multierror.Append(errs, fmt.Errorf("%w: %w", ErrInvalidMetadata, err)) } } + + // Containers images if err := ValidateContainersImages(kind, md.ContainersImages); err != nil { errs = multierror.Append(errs, fmt.Errorf("%w: %w", ErrInvalidMetadata, err)) } + // Annotations + if err := ValidateAnnotations(kind, md.Annotations); err != nil { + errs = multierror.Append(errs, fmt.Errorf("%w: %w", ErrInvalidMetadata, err)) + } + return errs.ErrorOrNil() } @@ -250,3 +278,21 @@ func ValidateContainersImages(kind hub.RepositoryKind, images []*hub.ContainerIm return errs.ErrorOrNil() } + +// ValidateAnnotations checks if the provided annotations are valid. +func ValidateAnnotations(kind hub.RepositoryKind, annotations map[string]string) error { + var errs *multierror.Error + + // Repository kind specific validation + switch kind { + case hub.Headlamp: + // Required annotations + for _, requiredAnnotation := range headlampRequiredAnnotations { + if _, ok := annotations[requiredAnnotation]; !ok { + errs = multierror.Append(errs, fmt.Errorf(`required annotation "%s" not provided`, requiredAnnotation)) + } + } + } + + return errs.ErrorOrNil() +} diff --git a/internal/repo/manager.go b/internal/repo/manager.go index 2c0f78534..049f9b5b5 100644 --- a/internal/repo/manager.go +++ b/internal/repo/manager.go @@ -84,6 +84,7 @@ var ( hub.CoreDNS, hub.Falco, hub.Gatekeeper, + hub.Headlamp, hub.Helm, hub.HelmPlugin, hub.KCL, @@ -283,6 +284,7 @@ func (m *Manager) ClaimOwnership(ctx context.Context, repoName, orgName string) hub.CoreDNS, hub.Falco, hub.Gatekeeper, + hub.Headlamp, hub.HelmPlugin, hub.KCL, hub.KedaScaler, @@ -461,6 +463,7 @@ func (m *Manager) locateMetadataFile(r *hub.Repository, basePath string) string hub.CoreDNS, hub.Falco, hub.Gatekeeper, + hub.Headlamp, hub.HelmPlugin, hub.KCL, hub.KedaScaler, @@ -823,6 +826,7 @@ func (m *Manager) validateURL(r *hub.Repository) error { hub.CoreDNS, hub.Falco, hub.Gatekeeper, + hub.Headlamp, hub.HelmPlugin, hub.KCL, hub.KedaScaler, diff --git a/internal/tracker/helpers.go b/internal/tracker/helpers.go index 738e59227..b84b48ab9 100644 --- a/internal/tracker/helpers.go +++ b/internal/tracker/helpers.go @@ -117,6 +117,7 @@ func SetupSource(i *hub.TrackerSourceInput) hub.TrackerSource { hub.Backstage, hub.CoreDNS, hub.Gatekeeper, + hub.Headlamp, hub.KCL, hub.KedaScaler, hub.Keptn, diff --git a/internal/tracker/tracker.go b/internal/tracker/tracker.go index 16ac6bcfb..1ce859eed 100644 --- a/internal/tracker/tracker.go +++ b/internal/tracker/tracker.go @@ -189,6 +189,7 @@ func (t *Tracker) cloneRepository() (string, string, error) { hub.CoreDNS, hub.Falco, hub.Gatekeeper, + hub.Headlamp, hub.HelmPlugin, hub.KCL, hub.KedaScaler, diff --git a/scripts/prepare-docs.sh b/scripts/prepare-docs.sh index 8fd930803..9d9b30ca0 100755 --- a/scripts/prepare-docs.sh +++ b/scripts/prepare-docs.sh @@ -12,6 +12,7 @@ cat docs/www/headers/container_images_repositories docs/container_images_reposit cat docs/www/headers/coredns_plugins_repositories docs/coredns_plugins_repositories.md > docs/www/content/topics/repositories/coredns-plugins.md cat docs/www/headers/falco_rules_repositories docs/falco_rules_repositories.md > docs/www/content/topics/repositories/falco-rules.md cat docs/www/headers/gatekeeper_policies_repositories docs/gatekeeper_policies_repositories.md > docs/www/content/topics/repositories/gatekeeper-policies.md +cat docs/www/headers/headlamp_plugins_repositories docs/headlamp_plugins_repositories.md > docs/www/content/topics/repositories/headlamp-plugins.md cat docs/www/headers/helm_charts_repositories docs/helm_charts_repositories.md > docs/www/content/topics/repositories/helm-charts.md cat docs/www/headers/helm_plugins_repositories docs/helm_plugins_repositories.md > docs/www/content/topics/repositories/helm-plugins.md cat docs/www/headers/kcl_modules_repositories docs/kcl_modules_repositories.md > docs/www/content/topics/repositories/kcl-modules.md @@ -30,6 +31,7 @@ cat docs/www/headers/tinkerbell_actions_repositories docs/tinkerbell_actions_rep cat docs/www/headers/security_report docs/security_report.md > docs/www/content/topics/security_report.md cat docs/www/headers/cli docs/cli.md > docs/www/content/topics/cli.md cat docs/www/headers/argo_annotations docs/argo_annotations.md > docs/www/content/topics/annotations/argo.md +cat docs/www/headers/headlamp_annotations docs/headlamp_annotations.md > docs/www/content/topics/annotations/headlamp.md cat docs/www/headers/helm_annotations docs/helm_annotations.md > docs/www/content/topics/annotations/helm.md cat docs/www/headers/keptn_annotations docs/keptn_annotations.md > docs/www/content/topics/annotations/keptn.md cat docs/www/headers/krew_annotations docs/krew_annotations.md > docs/www/content/topics/annotations/krew.md diff --git a/web/public/static/media/headlamp-light.svg b/web/public/static/media/headlamp-light.svg new file mode 100644 index 000000000..cdf0a62fc --- /dev/null +++ b/web/public/static/media/headlamp-light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/public/static/media/headlamp.svg b/web/public/static/media/headlamp.svg new file mode 100644 index 000000000..72ce76dbb --- /dev/null +++ b/web/public/static/media/headlamp.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/public/static/media/headlamp_icon.png b/web/public/static/media/headlamp_icon.png new file mode 100644 index 000000000..353329bdb Binary files /dev/null and b/web/public/static/media/headlamp_icon.png differ diff --git a/web/public/static/media/placeholder_pkg_headlamp.png b/web/public/static/media/placeholder_pkg_headlamp.png new file mode 100644 index 000000000..7b6d9f83e Binary files /dev/null and b/web/public/static/media/placeholder_pkg_headlamp.png differ diff --git a/web/src/layout/common/ButtonCopyToClipboard.tsx b/web/src/layout/common/ButtonCopyToClipboard.tsx index 738dddb89..86479eea7 100644 --- a/web/src/layout/common/ButtonCopyToClipboard.tsx +++ b/web/src/layout/common/ButtonCopyToClipboard.tsx @@ -17,6 +17,7 @@ interface Props { icon?: JSX.Element; disabled?: boolean; label?: string; + title?: string; tooltipType?: 'normal' | 'light'; noTooltip?: boolean; onClick?: () => void; @@ -105,6 +106,7 @@ const ButtonCopyToClipboard = (props: Props) => { }} disabled={props.disabled} aria-label={props.label || 'Copy to clipboard'} + title={props.title} > -
{
+ +
{
- -
-
{
-
Discovering artifacts to use with CNCF projects can be difficult. If every CNCF project that needs to diff --git a/web/src/layout/navigation/Footer.tsx b/web/src/layout/navigation/Footer.tsx index a377aa48f..2c0b5b502 100644 --- a/web/src/layout/navigation/Footer.tsx +++ b/web/src/layout/navigation/Footer.tsx @@ -143,7 +143,7 @@ const Footer = (props: Props) => { {!whiteLabel && (
- © 2022{' '} + © 2024{' '} The Linux Foundation diff --git a/web/src/layout/navigation/__snapshots__/Footer.test.tsx.snap b/web/src/layout/navigation/__snapshots__/Footer.test.tsx.snap index 5d7f68d11..022486605 100644 --- a/web/src/layout/navigation/__snapshots__/Footer.test.tsx.snap +++ b/web/src/layout/navigation/__snapshots__/Footer.test.tsx.snap @@ -264,7 +264,7 @@ exports[`Footer creates snapshot 1`] = ` - © 2022 + © 2024 { case RepositoryKind.Keptn: case RepositoryKind.Kubewarden: case RepositoryKind.Gatekeeper: + case RepositoryKind.KCL: + case RepositoryKind.Headlamp: return ( <> {props.package.appVersion && ( @@ -443,6 +448,51 @@ const Details = (props: Props) => { ); + case RepositoryKind.Headlamp: + return ( + <> + {props.package.data && + (props.package.data[HeadlampData.Url] || props.package.data[HeadlampData.Checksum]) && ( +
+ +
+ {props.package.data[HeadlampData.Url] && ( + + )} + {props.package.data[HeadlampData.Checksum] && ( + + )} +
+
+ )} + {props.package.data && props.package.data[HeadlampData.Version] && ( +
+ +

+ {props.package.data[HeadlampData.Version]} +

+
+ )} + {props.package.data && props.package.data[HeadlampData.Flavors] && ( + + )} + + ); + default: return null; } diff --git a/web/src/layout/package/Flavors.module.css b/web/src/layout/package/Flavors.module.css new file mode 100644 index 000000000..4ef6e9a86 --- /dev/null +++ b/web/src/layout/package/Flavors.module.css @@ -0,0 +1,4 @@ +.text { + font-size: 0.9rem; + line-height: 1.25; +} diff --git a/web/src/layout/package/Flavors.test.tsx b/web/src/layout/package/Flavors.test.tsx new file mode 100644 index 000000000..f30bcf3a3 --- /dev/null +++ b/web/src/layout/package/Flavors.test.tsx @@ -0,0 +1,41 @@ +import { render, screen } from '@testing-library/react'; + +import Flavors from './Flavors'; + +const defaultProps = { + flavors: 'in-cluster,web,docker-desktop', + title: 'Headlamp Flavors', +}; + +describe('Flavors', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('creates snapshot', () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + describe('Render', () => { + it('renders component', () => { + render(); + + expect(screen.getByText('Headlamp Flavors')).toBeInTheDocument(); + const flavors = screen.getAllByTestId('flavor'); + expect(flavors).toHaveLength(3); + }); + + it('renders only uniq flavors', () => { + render(); + + const flavors = screen.getAllByTestId('flavor'); + expect(flavors).toHaveLength(3); + }); + + it('does not render component if platforms is undefined', () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + }); +}); diff --git a/web/src/layout/package/Flavors.tsx b/web/src/layout/package/Flavors.tsx new file mode 100644 index 000000000..4adcdf024 --- /dev/null +++ b/web/src/layout/package/Flavors.tsx @@ -0,0 +1,49 @@ +import compact from 'lodash/compact'; +import trim from 'lodash/trim'; +import uniq from 'lodash/uniq'; +import { useCallback, useEffect, useState } from 'react'; + +import SmallTitle from '../common/SmallTitle'; +import styles from './Flavors.module.css'; + +interface Props { + title: string; + flavors?: string; +} + +const Flavors = (props: Props) => { + const cleanFlavors = useCallback((): string[] => { + let flavors: string[] = []; + if (props.flavors) { + flavors = uniq(compact(props.flavors.split(','))); + } + + return flavors; + }, [props.flavors]); + + const [flavors, setFlavors] = useState(cleanFlavors()); + + useEffect(() => { + setFlavors(cleanFlavors()); + }, [cleanFlavors, props.flavors]); + + if (flavors.length === 0) return null; + + return ( + <> + +
+ {flavors.map((flavor: string) => ( +
+
+ · + {trim(flavor)} +
+
+ ))} +
+ + ); +}; + +export default Flavors; diff --git a/web/src/layout/package/__snapshots__/Flavors.test.tsx.snap b/web/src/layout/package/__snapshots__/Flavors.test.tsx.snap new file mode 100644 index 000000000..a7681b964 --- /dev/null +++ b/web/src/layout/package/__snapshots__/Flavors.test.tsx.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Flavors creates snapshot 1`] = ` + +
+ + + Headlamp Flavors + + +
+
+
+
+ + · + + + in-cluster + +
+
+
+
+ + · + + + web + +
+
+
+
+ + · + + + docker-desktop + +
+
+
+
+`; diff --git a/web/src/types.ts b/web/src/types.ts index 2ac4b9f47..62134579b 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -22,6 +22,7 @@ export enum RepositoryKind { ArgoTemplate, KubeArmor, KCL, + Headlamp, } export enum PackageCategory { @@ -52,6 +53,13 @@ export enum KyvernoData { Category = 'kyvernoCategory', } +export enum HeadlampData { + Url = 'headlampPluginArchiveUrl', + Checksum = 'headlampPluginArchiveChecksum', + Version = 'headlampPluginVersionCompat', + Flavors = 'headlampPluginDistroCompat', +} + export enum HelmChartType { Library = 'library', Application = 'application', @@ -255,6 +263,10 @@ export interface PackageData { [KyvernoData.Version]?: string; [KyvernoData.Category]?: string; [KyvernoData.KubernetesVersion]?: string; + [HeadlampData.Url]?: string; + [HeadlampData.Checksum]?: string; + [HeadlampData.Version]?: string; + [HeadlampData.Flavors]?: string; [ArgoTemplateData.Version]?: string; tasks?: TektonTaskInPipeline[]; alternativeLocations?: string[]; diff --git a/web/src/utils/data.tsx b/web/src/utils/data.tsx index 245581f96..4eb3e72c1 100644 --- a/web/src/utils/data.tsx +++ b/web/src/utils/data.tsx @@ -132,6 +132,24 @@ export const REPOSITORY_KINDS: RepoKindDef[] = [ icon: , active: true, }, + { + kind: RepositoryKind.Gatekeeper, + label: 'gatekeeper', + name: 'Gatekeeper policies', + singular: 'Gatekeeper policy', + plural: 'Gatekeeper policies', + icon: , + active: true, + }, + { + kind: RepositoryKind.Headlamp, + label: 'headlamp', + name: 'Headlamp plugins', + singular: 'Headlamp plugin', + plural: 'Headlamp plugins', + icon: , + active: true, + }, { kind: RepositoryKind.Helm, label: 'helm', @@ -150,15 +168,6 @@ export const REPOSITORY_KINDS: RepoKindDef[] = [ icon: , active: true, }, - { - kind: RepositoryKind.Gatekeeper, - label: 'gatekeeper', - name: 'Gatekeeper policies', - singular: 'Gatekeeper policy', - plural: 'Gatekeeper policies', - icon: , - active: true, - }, { kind: RepositoryKind.KCL, label: 'kcl', @@ -777,7 +786,7 @@ export const CVSS_V3_VECTORS: { [key: string]: CVSSVectorMetric[] } = { export const OCI_PREFIX = 'oci://'; export const PKG_DETAIL_PATH = - /^\/packages\/(helm|falco|opa|olm|tbaction|krew|helm-plugin|tekton-task|keda-scaler|coredns|keptn|tekton-pipeline|kubewarden|gatekeeper|kyverno|knative-client-plugin|backstage|argo-template|kubearmor|kcl|container)\//; + /^\/packages\/(helm|falco|opa|olm|tbaction|krew|helm-plugin|tekton-task|keda-scaler|coredns|keptn|tekton-pipeline|kubewarden|gatekeeper|kyverno|knative-client-plugin|backstage|argo-template|kubearmor|kcl|headlamp|container)\//; export const HOME_ROUTES = [ '/verify-email', diff --git a/web/src/utils/repoKind.ts b/web/src/utils/repoKind.ts index 7ed15fbb1..dc13756a8 100644 --- a/web/src/utils/repoKind.ts +++ b/web/src/utils/repoKind.ts @@ -44,6 +44,8 @@ const getRepoKind = (repoName: string): RepositoryKind | null => { return RepositoryKind.KubeArmor; case 'kcl': return RepositoryKind.KCL; + case 'headlamp': + return RepositoryKind.Headlamp; default: return null; } @@ -93,6 +95,8 @@ const getRepoKindName = (repoKind: RepositoryKind): string | null => { return 'kubearmor'; case RepositoryKind.KCL: return 'kcl'; + case RepositoryKind.Headlamp: + return 'headlamp'; default: return null; } diff --git a/widget/public/index.html b/widget/public/index.html index b6ee39abc..f599572ca 100644 --- a/widget/public/index.html +++ b/widget/public/index.html @@ -11,6 +11,8 @@ +
+
diff --git a/widget/src/layout/Widget.tsx b/widget/src/layout/Widget.tsx index 5b3cab905..fe003b67e 100644 --- a/widget/src/layout/Widget.tsx +++ b/widget/src/layout/Widget.tsx @@ -91,6 +91,8 @@ const getRepoKindName = (repoKind: RepositoryKind): string | null => { return 'kubearmor'; case RepositoryKind.KCL: return 'kcl'; + case RepositoryKind.Headlamp: + return 'headlamp'; default: return null; } diff --git a/widget/src/layout/common/Image.test.tsx b/widget/src/layout/common/Image.test.tsx index 331459b6b..0227b3916 100644 --- a/widget/src/layout/common/Image.test.tsx +++ b/widget/src/layout/common/Image.test.tsx @@ -158,6 +158,13 @@ describe('Image', () => { expect(image).toHaveProperty('src', 'https://localhost:8000/static/media/placeholder_pkg_kcl.png'); }); + it('renders Headlamp icon', () => { + render(); + const image = screen.getByAltText('alt image'); + expect(image).toBeInTheDocument(); + expect(image).toHaveProperty('src', 'https://localhost:8000/static/media/placeholder_pkg_headlamp.png'); + }); + it('renders placeholder icon', () => { render(icon} />); expect(screen.getByText('icon')).toBeInTheDocument(); diff --git a/widget/src/layout/common/Image.tsx b/widget/src/layout/common/Image.tsx index eea7bfcbd..25a7ea70a 100644 --- a/widget/src/layout/common/Image.tsx +++ b/widget/src/layout/common/Image.tsx @@ -71,6 +71,8 @@ const Image = (props: Props) => { return '/static/media/placeholder_pkg_kubearmor.png'; case RepositoryKind.KCL: return '/static/media/placeholder_pkg_kcl.png'; + case RepositoryKind.Headlamp: + return '/static/media/placeholder_pkg_headlamp.png'; default: return PLACEHOLDER_SRC; } diff --git a/widget/src/layout/common/RepositoryIcon.tsx b/widget/src/layout/common/RepositoryIcon.tsx index df144b6d6..9ac2b8f0e 100644 --- a/widget/src/layout/common/RepositoryIcon.tsx +++ b/widget/src/layout/common/RepositoryIcon.tsx @@ -33,6 +33,7 @@ const ICONS: IconsList = { [RepositoryKind.ArgoTemplate]: , [RepositoryKind.KubeArmor]: , [RepositoryKind.KCL]: , + [RepositoryKind.Headlamp]: , }; const RepositoryIcon = (props: Props) => ( diff --git a/widget/src/layout/common/RepositoryIconLabel.tsx b/widget/src/layout/common/RepositoryIconLabel.tsx index 3d3cd405a..1d66f9371 100644 --- a/widget/src/layout/common/RepositoryIconLabel.tsx +++ b/widget/src/layout/common/RepositoryIconLabel.tsx @@ -100,6 +100,10 @@ const REPOSITORY_KINDS: RepoKindDef[] = [ kind: RepositoryKind.KCL, name: 'KCL module', }, + { + kind: RepositoryKind.Headlamp, + name: 'Headlamp plugin', + }, ]; const Wrapper = styled('span')` diff --git a/widget/src/layout/common/SVGIcons.test.tsx b/widget/src/layout/common/SVGIcons.test.tsx index bb20ad903..0a6f3326f 100644 --- a/widget/src/layout/common/SVGIcons.test.tsx +++ b/widget/src/layout/common/SVGIcons.test.tsx @@ -118,6 +118,11 @@ describe('SVGIcons', () => { expect(screen.getByTitle('kcl')); }); + it('renders Headlamp icon', () => { + render(); + expect(screen.getByTitle('headlamp')); + }); + it('does not render when name is not in the list', () => { render(); expect(screen.getByTestId('iconWrapper')).toBeEmptyDOMElement(); diff --git a/widget/src/layout/common/SVGIcons.tsx b/widget/src/layout/common/SVGIcons.tsx index 4c99fa5c8..f3362c062 100644 --- a/widget/src/layout/common/SVGIcons.tsx +++ b/widget/src/layout/common/SVGIcons.tsx @@ -939,6 +939,21 @@ const SVGIcons = (props: Props) => ( ); + case 'headlamp': + return ( + + {props.name} + + + + ); + case 'kubewarden': return ( diff --git a/widget/src/types.ts b/widget/src/types.ts index f378c7d4b..f25b114e3 100644 --- a/widget/src/types.ts +++ b/widget/src/types.ts @@ -48,6 +48,7 @@ export enum RepositoryKind { ArgoTemplate, KubeArmor, KCL, + Headlamp, } export interface SearchResults {