diff --git a/workspaces/extensions/.changeset/khaki-points-mate.md b/workspaces/extensions/.changeset/khaki-points-mate.md new file mode 100644 index 00000000000..fb22a11ee01 --- /dev/null +++ b/workspaces/extensions/.changeset/khaki-points-mate.md @@ -0,0 +1,6 @@ +--- +'@red-hat-developer-hub/backstage-plugin-catalog-backend-module-extensions': minor +'@red-hat-developer-hub/backstage-plugin-extensions-common': minor +--- + +Add catalog source annotations to entities diff --git a/workspaces/extensions/app-config.yaml b/workspaces/extensions/app-config.yaml index d1dd92f786c..88e8941a13b 100644 --- a/workspaces/extensions/app-config.yaml +++ b/workspaces/extensions/app-config.yaml @@ -26,7 +26,6 @@ backend: # See https://backstage.io/docs/auth/service-to-service-auth for # information on the format auth: - dangerouslyDisableDefaultAuthPolicy: true # keys: # - secret: ${BACKEND_SECRET} baseUrl: http://localhost:7007 @@ -101,37 +100,25 @@ catalog: pullRequestBranchName: backstage-integration rules: - allow: - [ - Component, - System, - API, - Resource, - Plugin, - PluginCollection, - Package, - Location, - ] + - Component + - System + - API + - Resource + - Plugin + - PluginCollection + - Package + - Location + locations: - type: file target: ../../catalog-info.yaml rules: - allow: [Component] + # Guest user and group - type: file - target: ../../examples/all-orgs.yaml - rules: - - allow: [Group] - - type: file - target: ../../examples/all-plugins.yaml - rules: - - allow: [Plugin] - - type: file - target: ../../examples/all-collections.yaml - rules: - - allow: [PluginCollection] - - type: file - target: ../../examples/all-packages.yaml + target: ../../org.yaml rules: - - allow: [Package] + - allow: [User, Group] kubernetes: # see https://backstage.io/docs/features/kubernetes/configuration for kubernetes configuration options @@ -151,10 +138,11 @@ permission: - name: user:development/guest extensions: - # directory: ../../examples # YAML files are in the examples directory - ### Example for how to enable installation to a file. + # YAML files are in the examples directory + directory: ../../examples + + # Example for how to enable installation to a file. # installation: # enabled: true # saveToSingleFile: # file: ${EXTENSIONS_PLUGIN_CONFIG} -dangerouslyAllowSignInWithoutUserInCatalog: true diff --git a/workspaces/extensions/docs/catalog/plugins.md b/workspaces/extensions/docs/catalog/plugins.md index 3827c873f84..e6c83d42323 100644 --- a/workspaces/extensions/docs/catalog/plugins.md +++ b/workspaces/extensions/docs/catalog/plugins.md @@ -229,6 +229,21 @@ spec: ## Annotations +### Catalog source + +Automatically set by the `BaseEntityProvider` to identify which catalog image each entity came from. The value is derived from the on-disk directory layout produced by `install-dynamic-plugins`: + +- Entities under the primary `catalog-entities/` tree get `"primary"` +- Entities under `extra//catalog-entities/` get `` as the source (matching the name from `EXTRA_CATALOG_INDEX_IMAGES`) + +This annotation is always set and should not be manually specified in YAML files. + +```yaml +metadata: + annotations: + extensions.backstage.io/catalog-source: 'primary' +``` + ### Support type for Core and Community plugins ```yaml diff --git a/workspaces/extensions/e2e-tests/extensions.test.ts b/workspaces/extensions/e2e-tests/extensions.test.ts index 8924c9bb901..d464376f4e2 100644 --- a/workspaces/extensions/e2e-tests/extensions.test.ts +++ b/workspaces/extensions/e2e-tests/extensions.test.ts @@ -108,14 +108,14 @@ test.describe('Admin > Extensions', () => { await extensions.selectDropdown(translations.search.author); await extensions.toggleOption('Red Hat'); await sharedPage.keyboard.press(`Escape`); - await extensionHelper.verifyHeading('APIs with 3scale'); + await extensionHelper.verifyHeading('Pipelines with Tekton'); await extensionHelper.verifyTextInLocator( '', `${translations.metadata.by} Red Hat`, true, ); - await extensionHelper.clickHeading('APIs with 3scale'); + await extensionHelper.clickHeading('Pipelines with Tekton'); await extensionHelper.verifyTableHeadingAndRows([ translations.table.packageName, translations.table.version, diff --git a/workspaces/extensions/examples/all-collections.yaml b/workspaces/extensions/examples/all-collections.yaml deleted file mode 100644 index 18b52b26cd7..00000000000 --- a/workspaces/extensions/examples/all-collections.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: backstage.io/v1alpha1 -kind: Location -metadata: - namespace: extensions-plugin-demo - name: all-collections - description: A collection of extensions plugin collections -spec: - targets: - - ./collections/featured.yaml - - ./collections/kubernetes.yaml diff --git a/workspaces/extensions/examples/all-orgs.yaml b/workspaces/extensions/examples/all-orgs.yaml deleted file mode 100644 index 196142b6a35..00000000000 --- a/workspaces/extensions/examples/all-orgs.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: backstage.io/v1alpha1 -kind: Location -metadata: - namespace: extensions-plugin-demo - name: all-orgs - description: A collection of all the extensions orgs -spec: - targets: - - ./orgs/redhat.yaml diff --git a/workspaces/extensions/examples/all-packages.yaml b/workspaces/extensions/examples/all-packages.yaml deleted file mode 100644 index 4eacc4fc143..00000000000 --- a/workspaces/extensions/examples/all-packages.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: backstage.io/v1alpha1 -kind: Location -metadata: - namespace: extensions-plugin-demo - name: all-packages - description: A collection of all the extensions packages -spec: - targets: - - ./packages/backstage-community-plugin-3scale-backend.yaml - - ./packages/backstage-community-plugin-acr.yaml - - ./packages/backstage-community-plugin-catalog-backend-module-keycloak.yaml - - ./packages/backstage-community-plugin-quay.yaml - - ./packages/backstage-community-plugin-tekton.yaml - - ./packages/backstage-community-plugin-topology.yaml - - ./packages/red-hat-developer-hub-backstage-plugin-bulk-import-backend.yaml - - ./packages/red-hat-developer-hub-backstage-plugin-bulk-import.yaml - - ./packages/red-hat-developer-hub-backstage-plugin-extensions-backend.yaml - - ./packages/red-hat-developer-hub-backstage-plugin-extensions.yaml diff --git a/workspaces/extensions/examples/all-plugins.yaml b/workspaces/extensions/examples/all-plugins.yaml deleted file mode 100644 index c372d5e8eeb..00000000000 --- a/workspaces/extensions/examples/all-plugins.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: backstage.io/v1alpha1 -kind: Location -metadata: - namespace: extensions-plugin-demo - name: all-plugins - description: A collection of all the extensions plugins -spec: - targets: - - ./plugins/3scale.yaml - - ./plugins/acr.yaml - - ./plugins/bulk-import.yaml - - ./plugins/certified-plugin-1-by-vendor-a.yaml - - ./plugins/certified-plugin-2-by-vendor-a.yaml - - ./plugins/certified-plugin-3-by-vendor-b.yaml - - ./plugins/keycloak.yaml - - ./plugins/extensions.yaml - - ./plugins/no-icon-no-description.yaml - - ./plugins/pre-installed-false.yaml - - ./plugins/pre-installed-true.yaml - - ./plugins/pre-installed-undefined.yaml - - ./plugins/quay.yaml - - ./plugins/search.yaml - - ./plugins/support-community.yaml - - ./plugins/support-dev-preview.yaml - - ./plugins/support-generally-available.yaml - - ./plugins/support-none.yaml - - ./plugins/support-string.yaml - - ./plugins/support-tech-preview.yaml - - ./plugins/support-undefined.yaml - - ./plugins/support-unknown-level.yaml - - ./plugins/tekton.yaml - - ./plugins/topology.yaml diff --git a/workspaces/extensions/examples/plugins/3scale.yaml b/workspaces/extensions/examples/extra/community/3scale.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/3scale.yaml rename to workspaces/extensions/examples/extra/community/3scale.yaml diff --git a/workspaces/extensions/examples/plugins/acr.yaml b/workspaces/extensions/examples/extra/community/acr.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/acr.yaml rename to workspaces/extensions/examples/extra/community/acr.yaml diff --git a/workspaces/extensions/examples/packages/backstage-community-plugin-3scale-backend.yaml b/workspaces/extensions/examples/extra/community/backstage-community-plugin-3scale-backend.yaml similarity index 100% rename from workspaces/extensions/examples/packages/backstage-community-plugin-3scale-backend.yaml rename to workspaces/extensions/examples/extra/community/backstage-community-plugin-3scale-backend.yaml diff --git a/workspaces/extensions/examples/packages/backstage-community-plugin-acr.yaml b/workspaces/extensions/examples/extra/community/backstage-community-plugin-acr.yaml similarity index 100% rename from workspaces/extensions/examples/packages/backstage-community-plugin-acr.yaml rename to workspaces/extensions/examples/extra/community/backstage-community-plugin-acr.yaml diff --git a/workspaces/extensions/examples/packages/backstage-community-plugin-catalog-backend-module-keycloak.yaml b/workspaces/extensions/examples/extra/community/backstage-community-plugin-catalog-backend-module-keycloak.yaml similarity index 100% rename from workspaces/extensions/examples/packages/backstage-community-plugin-catalog-backend-module-keycloak.yaml rename to workspaces/extensions/examples/extra/community/backstage-community-plugin-catalog-backend-module-keycloak.yaml diff --git a/workspaces/extensions/examples/packages/backstage-community-plugin-quay.yaml b/workspaces/extensions/examples/extra/community/backstage-community-plugin-quay.yaml similarity index 100% rename from workspaces/extensions/examples/packages/backstage-community-plugin-quay.yaml rename to workspaces/extensions/examples/extra/community/backstage-community-plugin-quay.yaml diff --git a/workspaces/extensions/examples/packages/backstage-community-plugin-tekton.yaml b/workspaces/extensions/examples/extra/community/backstage-community-plugin-tekton.yaml similarity index 100% rename from workspaces/extensions/examples/packages/backstage-community-plugin-tekton.yaml rename to workspaces/extensions/examples/extra/community/backstage-community-plugin-tekton.yaml diff --git a/workspaces/extensions/examples/packages/backstage-community-plugin-topology.yaml b/workspaces/extensions/examples/extra/community/backstage-community-plugin-topology.yaml similarity index 100% rename from workspaces/extensions/examples/packages/backstage-community-plugin-topology.yaml rename to workspaces/extensions/examples/extra/community/backstage-community-plugin-topology.yaml diff --git a/workspaces/extensions/examples/plugins/keycloak.yaml b/workspaces/extensions/examples/extra/community/keycloak.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/keycloak.yaml rename to workspaces/extensions/examples/extra/community/keycloak.yaml diff --git a/workspaces/extensions/examples/plugins/quay.yaml b/workspaces/extensions/examples/extra/community/quay.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/quay.yaml rename to workspaces/extensions/examples/extra/community/quay.yaml diff --git a/workspaces/extensions/examples/plugins/tekton.yaml b/workspaces/extensions/examples/extra/community/tekton.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/tekton.yaml rename to workspaces/extensions/examples/extra/community/tekton.yaml diff --git a/workspaces/extensions/examples/plugins/topology.yaml b/workspaces/extensions/examples/extra/community/topology.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/topology.yaml rename to workspaces/extensions/examples/extra/community/topology.yaml diff --git a/workspaces/extensions/examples/plugins/bulk-import.yaml b/workspaces/extensions/examples/extra/redhat/bulk-import.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/bulk-import.yaml rename to workspaces/extensions/examples/extra/redhat/bulk-import.yaml diff --git a/workspaces/extensions/examples/plugins/extensions.yaml b/workspaces/extensions/examples/extra/redhat/extensions.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/extensions.yaml rename to workspaces/extensions/examples/extra/redhat/extensions.yaml diff --git a/workspaces/extensions/examples/packages/red-hat-developer-hub-backstage-plugin-bulk-import-backend.yaml b/workspaces/extensions/examples/extra/redhat/red-hat-developer-hub-backstage-plugin-bulk-import-backend.yaml similarity index 100% rename from workspaces/extensions/examples/packages/red-hat-developer-hub-backstage-plugin-bulk-import-backend.yaml rename to workspaces/extensions/examples/extra/redhat/red-hat-developer-hub-backstage-plugin-bulk-import-backend.yaml diff --git a/workspaces/extensions/examples/packages/red-hat-developer-hub-backstage-plugin-bulk-import.yaml b/workspaces/extensions/examples/extra/redhat/red-hat-developer-hub-backstage-plugin-bulk-import.yaml similarity index 100% rename from workspaces/extensions/examples/packages/red-hat-developer-hub-backstage-plugin-bulk-import.yaml rename to workspaces/extensions/examples/extra/redhat/red-hat-developer-hub-backstage-plugin-bulk-import.yaml diff --git a/workspaces/extensions/examples/packages/red-hat-developer-hub-backstage-plugin-extensions-backend.yaml b/workspaces/extensions/examples/extra/redhat/red-hat-developer-hub-backstage-plugin-extensions-backend.yaml similarity index 100% rename from workspaces/extensions/examples/packages/red-hat-developer-hub-backstage-plugin-extensions-backend.yaml rename to workspaces/extensions/examples/extra/redhat/red-hat-developer-hub-backstage-plugin-extensions-backend.yaml diff --git a/workspaces/extensions/examples/packages/red-hat-developer-hub-backstage-plugin-extensions.yaml b/workspaces/extensions/examples/extra/redhat/red-hat-developer-hub-backstage-plugin-extensions.yaml similarity index 100% rename from workspaces/extensions/examples/packages/red-hat-developer-hub-backstage-plugin-extensions.yaml rename to workspaces/extensions/examples/extra/redhat/red-hat-developer-hub-backstage-plugin-extensions.yaml diff --git a/workspaces/extensions/examples/orgs/redhat.yaml b/workspaces/extensions/examples/orgs/redhat.yaml deleted file mode 100644 index bab7d4aa038..00000000000 --- a/workspaces/extensions/examples/orgs/redhat.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-group -apiVersion: backstage.io/v1alpha1 -kind: Group -metadata: - name: redhat -spec: - type: team - children: [] diff --git a/workspaces/extensions/examples/plugins/no-icon-no-description.yaml b/workspaces/extensions/examples/other-tests/no-icon-no-description.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/no-icon-no-description.yaml rename to workspaces/extensions/examples/other-tests/no-icon-no-description.yaml diff --git a/workspaces/extensions/examples/plugins/pre-installed-false.yaml b/workspaces/extensions/examples/other-tests/pre-installed-false.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/pre-installed-false.yaml rename to workspaces/extensions/examples/other-tests/pre-installed-false.yaml diff --git a/workspaces/extensions/examples/plugins/pre-installed-true.yaml b/workspaces/extensions/examples/other-tests/pre-installed-true.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/pre-installed-true.yaml rename to workspaces/extensions/examples/other-tests/pre-installed-true.yaml diff --git a/workspaces/extensions/examples/plugins/pre-installed-undefined.yaml b/workspaces/extensions/examples/other-tests/pre-installed-undefined.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/pre-installed-undefined.yaml rename to workspaces/extensions/examples/other-tests/pre-installed-undefined.yaml diff --git a/workspaces/extensions/examples/plugins/search.yaml b/workspaces/extensions/examples/other-tests/search.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/search.yaml rename to workspaces/extensions/examples/other-tests/search.yaml diff --git a/workspaces/extensions/examples/plugins/support-community.yaml b/workspaces/extensions/examples/other-tests/support-community.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/support-community.yaml rename to workspaces/extensions/examples/other-tests/support-community.yaml diff --git a/workspaces/extensions/examples/plugins/support-dev-preview.yaml b/workspaces/extensions/examples/other-tests/support-dev-preview.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/support-dev-preview.yaml rename to workspaces/extensions/examples/other-tests/support-dev-preview.yaml diff --git a/workspaces/extensions/examples/plugins/support-generally-available.yaml b/workspaces/extensions/examples/other-tests/support-generally-available.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/support-generally-available.yaml rename to workspaces/extensions/examples/other-tests/support-generally-available.yaml diff --git a/workspaces/extensions/examples/plugins/support-none.yaml b/workspaces/extensions/examples/other-tests/support-none.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/support-none.yaml rename to workspaces/extensions/examples/other-tests/support-none.yaml diff --git a/workspaces/extensions/examples/plugins/support-string.yaml b/workspaces/extensions/examples/other-tests/support-string.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/support-string.yaml rename to workspaces/extensions/examples/other-tests/support-string.yaml diff --git a/workspaces/extensions/examples/plugins/support-tech-preview.yaml b/workspaces/extensions/examples/other-tests/support-tech-preview.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/support-tech-preview.yaml rename to workspaces/extensions/examples/other-tests/support-tech-preview.yaml diff --git a/workspaces/extensions/examples/plugins/support-undefined.yaml b/workspaces/extensions/examples/other-tests/support-undefined.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/support-undefined.yaml rename to workspaces/extensions/examples/other-tests/support-undefined.yaml diff --git a/workspaces/extensions/examples/plugins/support-unknown-level.yaml b/workspaces/extensions/examples/other-tests/support-unknown-level.yaml similarity index 100% rename from workspaces/extensions/examples/plugins/support-unknown-level.yaml rename to workspaces/extensions/examples/other-tests/support-unknown-level.yaml diff --git a/workspaces/extensions/examples/plugins/certified-plugin-1-by-vendor-a.yaml b/workspaces/extensions/examples/plugins/certified-plugin-1-by-vendor-a.yaml deleted file mode 100644 index 9e96bf4fad9..00000000000 --- a/workspaces/extensions/examples/plugins/certified-plugin-1-by-vendor-a.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=../../json-schema/plugins.json -apiVersion: extensions.backstage.io/v1alpha1 -kind: Plugin -metadata: - namespace: extensions-plugin-demo - name: certified-plugin-1-by-vendor-a - title: Certified Plugin 1 by Vendor A - description: This is a certified plugin example - annotations: - extensions.backstage.io/pre-installed: 'true' -spec: - categories: - - Test plugin entity diff --git a/workspaces/extensions/examples/plugins/certified-plugin-2-by-vendor-a.yaml b/workspaces/extensions/examples/plugins/certified-plugin-2-by-vendor-a.yaml deleted file mode 100644 index 44a34c360e1..00000000000 --- a/workspaces/extensions/examples/plugins/certified-plugin-2-by-vendor-a.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=../../json-schema/plugins.json -apiVersion: extensions.backstage.io/v1alpha1 -kind: Plugin -metadata: - namespace: extensions-plugin-demo - name: certified-plugin-2-by-vendor-a - title: Certified Plugin 2 by Vendor A - description: This is a certified plugin example - annotations: - extensions.backstage.io/pre-installed: 'true' -spec: - categories: - - Test plugin entity diff --git a/workspaces/extensions/examples/plugins/certified-plugin-3-by-vendor-b.yaml b/workspaces/extensions/examples/plugins/certified-plugin-3-by-vendor-b.yaml deleted file mode 100644 index 2f9f2d55176..00000000000 --- a/workspaces/extensions/examples/plugins/certified-plugin-3-by-vendor-b.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=../../json-schema/plugins.json -apiVersion: extensions.backstage.io/v1alpha1 -kind: Plugin -metadata: - namespace: extensions-plugin-demo - name: certified-plugin-3-by-vendor-b - title: Certified Plugin 3 by Vendor B - description: This is a certified plugin example - annotations: - extensions.backstage.io/pre-installed: 'true' -spec: - categories: - - Test plugin entity diff --git a/workspaces/extensions/org.yaml b/workspaces/extensions/org.yaml new file mode 100644 index 00000000000..c3d572f10a9 --- /dev/null +++ b/workspaces/extensions/org.yaml @@ -0,0 +1,28 @@ +--- +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-user +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + namespace: development + name: guest +spec: + memberOf: [guests] +--- +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-group +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + namespace: development + name: guests +spec: + type: team + children: [] +--- +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-group +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: redhat +spec: + type: team + children: [] diff --git a/workspaces/extensions/package.json b/workspaces/extensions/package.json index 914e2245cb5..5181b362931 100644 --- a/workspaces/extensions/package.json +++ b/workspaces/extensions/package.json @@ -45,6 +45,7 @@ }, "devDependencies": { "@backstage/cli": "^0.36.3", + "@backstage/cli-defaults": "^0.1.3", "@backstage/e2e-test-utils": "^0.1.2", "@backstage/repo-tools": "^0.17.3", "@changesets/cli": "^2.27.1", diff --git a/workspaces/extensions/playwright.config.ts b/workspaces/extensions/playwright.config.ts index 1a9f696e3d9..83de61e0a39 100644 --- a/workspaces/extensions/playwright.config.ts +++ b/workspaces/extensions/playwright.config.ts @@ -37,9 +37,13 @@ export default defineConfig({ : [ { command: startCommand, - port: 3000, reuseExistingServer: false, cwd: __dirname, + wait: { + // wait for this message and expect 10+ loaded plugins. + stdout: + /extensions-plugin-provider applied [1-9]\d+ entities to the catalog/, + }, }, ], diff --git a/workspaces/extensions/plugins/catalog-backend-module-extensions/README.md b/workspaces/extensions/plugins/catalog-backend-module-extensions/README.md index 723d3225199..aa59f30d3c5 100644 --- a/workspaces/extensions/plugins/catalog-backend-module-extensions/README.md +++ b/workspaces/extensions/plugins/catalog-backend-module-extensions/README.md @@ -59,6 +59,33 @@ extensions: directory: /path/to/custom/extensions ``` +### Multi-source catalog layout + +When the `install-dynamic-plugins` init container is configured with `EXTRA_CATALOG_INDEX_IMAGES`, it extracts additional catalog entities into subdirectories under `extra/`: + +``` +/ + catalog-entities/ # primary source + plugins/ + plugin-a.yaml + extra/ + community/ # extra source "community" + catalog-entities/ + plugins/ + plugin-b.yaml + partner/ # extra source "partner" + catalog-entities/ + plugins/ + plugin-c.yaml +``` + +The provider reads all YAML files recursively from the extensions directory and automatically sets the `extensions.backstage.io/catalog-source` annotation on each entity based on where it was found: + +- Entities from the primary `catalog-entities/` tree get `catalog-source: "primary"` +- Entities from `extra//catalog-entities/` get `catalog-source: ""` + +This allows the Extensions UI to identify which catalog each plugin came from (e.g. Red Hat, Community, partner). + ### Collision behavior When multiple YAML sources define the same entity identity (`kind:namespace/name`), the provider handles collisions as follows: diff --git a/workspaces/extensions/plugins/catalog-backend-module-extensions/report.api.md b/workspaces/extensions/plugins/catalog-backend-module-extensions/report.api.md index 26ceca31a5e..4a4d59a19db 100644 --- a/workspaces/extensions/plugins/catalog-backend-module-extensions/report.api.md +++ b/workspaces/extensions/plugins/catalog-backend-module-extensions/report.api.md @@ -34,8 +34,10 @@ export abstract class BaseEntityProvider ); // (undocumented) connect(connection: EntityProviderConnection): Promise; + static readonly DEFAULT_CATALOG_SOURCE = 'primary'; + static deriveCatalogSource(filePath: string, extensionsRoot: string): string; // (undocumented) - getEntities(allEntities: JsonFileData[]): T[]; + getEntities(allEntities: JsonFileData[], extensionsRoot?: string): T[]; // (undocumented) abstract getKind(): string; // (undocumented) diff --git a/workspaces/extensions/plugins/catalog-backend-module-extensions/src/module.ts b/workspaces/extensions/plugins/catalog-backend-module-extensions/src/module.ts index 0a5a1b01e84..d9bd89bd2d6 100644 --- a/workspaces/extensions/plugins/catalog-backend-module-extensions/src/module.ts +++ b/workspaces/extensions/plugins/catalog-backend-module-extensions/src/module.ts @@ -61,11 +61,11 @@ export const catalogModuleExtensions = createBackendModule({ }) { logger.info('Adding Extensions providers and processors to catalog...'); const taskRunner = scheduler.createScheduledTaskRunner({ - frequency: { minutes: 30 }, + frequency: { hours: 1 }, timeout: { minutes: 10 }, }); const delayedTaskRunner = scheduler.createScheduledTaskRunner({ - frequency: { minutes: 30 }, + frequency: { hours: 1 }, timeout: { minutes: 10 }, initialDelay: { seconds: 20 }, }); diff --git a/workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.test.ts b/workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.test.ts index fd8b45a01fe..ea2d8c35d69 100644 --- a/workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.test.ts +++ b/workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.test.ts @@ -19,6 +19,7 @@ import { LoggerService, SchedulerServiceTaskRunner, } from '@backstage/backend-plugin-api'; +import { ExtensionsAnnotation } from '@red-hat-developer-hub/backstage-plugin-extensions-common'; import { BaseEntityProvider } from './BaseEntityProvider'; import { JsonFileData } from '../types'; @@ -136,3 +137,194 @@ describe('BaseEntityProvider collision policy', () => { expect(entities).toHaveLength(2); }); }); + +describe('BaseEntityProvider.deriveCatalogSource', () => { + const root = '/extensions'; + + it('returns "primary" for paths in the main catalog-entities directory', () => { + expect( + BaseEntityProvider.deriveCatalogSource( + '/extensions/catalog-entities/plugin.yaml', + root, + ), + ).toBe('primary'); + }); + + it('returns "primary" for paths without the extra/ segment', () => { + expect( + BaseEntityProvider.deriveCatalogSource( + '/extensions/plugins/foo.yaml', + root, + ), + ).toBe('primary'); + }); + + it('returns the source name for paths under extra//', () => { + expect( + BaseEntityProvider.deriveCatalogSource( + '/extensions/extra/community/catalog-entities/plugin.yaml', + root, + ), + ).toBe('community'); + }); + + it('returns the source name for a different extra source', () => { + expect( + BaseEntityProvider.deriveCatalogSource( + '/extensions/extra/partner/catalog-entities/plugins/plugin.yaml', + root, + ), + ).toBe('partner'); + }); + + it('handles auto-derived subdirectory names with special characters', () => { + // imageRefToSubdirectory replaces /:@ with _ so names like this are common + expect( + BaseEntityProvider.deriveCatalogSource( + '/extensions/extra/quay.io_rhdh_index_1.10/catalog-entities/plugin.yaml', + root, + ), + ).toBe('quay.io_rhdh_index_1.10'); + }); + + it('handles deeply nested files within a source directory', () => { + expect( + BaseEntityProvider.deriveCatalogSource( + '/extensions/extra/community/catalog-entities/nested/deep/plugin.yaml', + root, + ), + ).toBe('community'); + }); +}); + +describe('BaseEntityProvider source metadata annotations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + const root = '/extensions'; + + it('sets catalog-source to "primary" for entities from the main catalog root', () => { + const provider = new TestEntityProvider(taskRunner, undefined, logger); + const entity = createEntity({ metadata: { name: 'primary-plugin' } }); + + const entities = provider.getEntities( + [createFileData('/extensions/catalog-entities/plugin.yaml', entity)], + root, + ); + + expect(entities).toHaveLength(1); + expect( + entities[0].metadata.annotations?.[ExtensionsAnnotation.CATALOG_SOURCE], + ).toBe('primary'); + }); + + it('sets catalog-source to the extra source name for entities under extra//', () => { + const provider = new TestEntityProvider(taskRunner, undefined, logger); + const entity = createEntity({ metadata: { name: 'community-plugin' } }); + + const entities = provider.getEntities( + [ + createFileData( + '/extensions/extra/community/catalog-entities/plugin.yaml', + entity, + ), + ], + root, + ); + + expect(entities).toHaveLength(1); + expect( + entities[0].metadata.annotations?.[ExtensionsAnnotation.CATALOG_SOURCE], + ).toBe('community'); + }); + + it('sets distinct source annotations when entities come from different sources', () => { + const provider = new TestEntityProvider(taskRunner, undefined, logger); + const primaryPlugin = createEntity({ + metadata: { name: 'plugin-a' }, + }); + const communityPlugin = createEntity({ + metadata: { name: 'plugin-b' }, + }); + + const entities = provider.getEntities( + [ + createFileData( + '/extensions/catalog-entities/plugin-a.yaml', + primaryPlugin, + ), + createFileData( + '/extensions/extra/community/catalog-entities/plugin-b.yaml', + communityPlugin, + ), + ], + root, + ); + + expect(entities).toHaveLength(2); + const sourceA = + entities[0].metadata.annotations?.[ExtensionsAnnotation.CATALOG_SOURCE]; + const sourceB = + entities[1].metadata.annotations?.[ExtensionsAnnotation.CATALOG_SOURCE]; + expect(sourceA).toBe('primary'); + expect(sourceB).toBe('community'); + }); + + it('preserves the winning entity source annotation on duplicate (first-wins)', () => { + const provider = new TestEntityProvider(taskRunner, undefined, logger); + const entity = createEntity({ metadata: { name: 'dup-plugin' } }); + + const entities = provider.getEntities( + [ + createFileData( + '/extensions/extra/community/catalog-entities/plugin.yaml', + entity, + ), + createFileData('/extensions/catalog-entities/plugin.yaml', entity), + ], + root, + ); + + expect(entities).toHaveLength(1); + // First-wins: the community entity was seen first + expect( + entities[0].metadata.annotations?.[ExtensionsAnnotation.CATALOG_SOURCE], + ).toBe('community'); + }); + + it('sets correct distinct sources when entities share name but differ by namespace', () => { + const provider = new TestEntityProvider(taskRunner, undefined, logger); + const primaryEntity = createEntity({ + metadata: { name: 'shared-name' }, + }); + const communityEntity = createEntity({ + metadata: { name: 'shared-name', namespace: 'community' }, + }); + + const entities = provider.getEntities( + [ + createFileData( + '/extensions/catalog-entities/plugin.yaml', + primaryEntity, + ), + createFileData( + '/extensions/extra/community/catalog-entities/plugin.yaml', + communityEntity, + ), + ], + root, + ); + + expect(entities).toHaveLength(2); + const sources = entities.map( + e => e.metadata.annotations?.[ExtensionsAnnotation.CATALOG_SOURCE], + ); + expect(sources).toContain('primary'); + expect(sources).toContain('community'); + }); +}); diff --git a/workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts b/workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts index 12f8c4910a8..06adbd73bbb 100644 --- a/workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts +++ b/workspaces/extensions/plugins/catalog-backend-module-extensions/src/providers/BaseEntityProvider.ts @@ -28,6 +28,7 @@ import { EntityProviderConnection, } from '@backstage/plugin-catalog-node'; import { Config } from '@backstage/config'; +import { ExtensionsAnnotation } from '@red-hat-developer-hub/backstage-plugin-extensions-common'; import { readYamlFiles } from '../utils/file-utils'; import { JsonFileData } from '../types'; import path from 'path'; @@ -61,7 +62,36 @@ export abstract class BaseEntityProvider abstract getProviderName(): string; abstract getKind(): string; - private addProviderAnnotations(entity: T): T { + /** + * The default source identifier used for entities that do not reside + * under an `extra//` subdirectory — i.e. entities from the + * primary catalog index image. + */ + static readonly DEFAULT_CATALOG_SOURCE = 'primary'; + + /** + * Derives a catalog source identifier from a file path. + * + * When `install-dynamic-plugins` extracts extra catalog index images + * (via `EXTRA_CATALOG_INDEX_IMAGES`), entities are placed under + * `/extra//catalog-entities/…`. This method + * computes the relative path from `extensionsRoot` and checks whether + * the first segment is `extra/` — if so, the second segment is the + * source identifier. All other paths return `"primary"`. + */ + static deriveCatalogSource(filePath: string, extensionsRoot: string): string { + const relative = path.relative(extensionsRoot, filePath); + const [first, second] = relative.split(path.sep); + return first === 'extra' && second + ? second + : BaseEntityProvider.DEFAULT_CATALOG_SOURCE; + } + + private addProviderAnnotations( + entity: T, + filePath: string, + extensionsRoot: string, + ): T { return { ...entity, metadata: { @@ -70,12 +100,17 @@ export abstract class BaseEntityProvider ...entity.metadata.annotations, [ANNOTATION_LOCATION]: `file:${this.getProviderName()}`, [ANNOTATION_ORIGIN_LOCATION]: `file:${this.getProviderName()}`, + [ExtensionsAnnotation.CATALOG_SOURCE]: + BaseEntityProvider.deriveCatalogSource(filePath, extensionsRoot), }, }, }; } - getEntities(allEntities: JsonFileData[]): T[] { + getEntities( + allEntities: JsonFileData[], + extensionsRoot: string = '', + ): T[] { if (allEntities.length === 0) { return []; } @@ -116,8 +151,9 @@ export abstract class BaseEntityProvider ); } - return Array.from(entitiesByEntityRef.values()).map(({ entity }) => - this.addProviderAnnotations(entity), + return Array.from(entitiesByEntityRef.values()).map( + ({ entity, filePath }) => + this.addProviderAnnotations(entity, filePath, extensionsRoot), ); } @@ -213,7 +249,7 @@ export abstract class BaseEntityProvider } } - const entities: T[] = this.getEntities(yamlData); + const entities: T[] = this.getEntities(yamlData, extensionsFilePath ?? ''); await this.connection.applyMutation({ type: 'full', @@ -222,5 +258,9 @@ export abstract class BaseEntityProvider locationKey: `file:${this.getProviderName()}`, })), }); + + this.logger?.info( + `${this.getProviderName()} applied ${entities.length} entities to the catalog.`, + ); } } diff --git a/workspaces/extensions/plugins/extensions-common/report.api.md b/workspaces/extensions/plugins/extensions-common/report.api.md index 86e6a486ec5..451ff4ab274 100644 --- a/workspaces/extensions/plugins/extensions-common/report.api.md +++ b/workspaces/extensions/plugins/extensions-common/report.api.md @@ -96,6 +96,8 @@ export const EXTENSIONS_API_VERSION = 'extensions.backstage.io/v1alpha1'; // @public (undocumented) export enum ExtensionsAnnotation { + // (undocumented) + CATALOG_SOURCE = 'extensions.backstage.io/catalog-source', // (undocumented) PRE_INSTALLED = 'extensions.backstage.io/pre-installed', } diff --git a/workspaces/extensions/plugins/extensions-common/src/annotations.ts b/workspaces/extensions/plugins/extensions-common/src/annotations.ts index 8fa3e920285..d3f509c572f 100644 --- a/workspaces/extensions/plugins/extensions-common/src/annotations.ts +++ b/workspaces/extensions/plugins/extensions-common/src/annotations.ts @@ -19,4 +19,5 @@ */ export enum ExtensionsAnnotation { PRE_INSTALLED = 'extensions.backstage.io/pre-installed', + CATALOG_SOURCE = 'extensions.backstage.io/catalog-source', } diff --git a/workspaces/extensions/yarn.lock b/workspaces/extensions/yarn.lock index d42a2c7bf82..9528789ba01 100644 --- a/workspaces/extensions/yarn.lock +++ b/workspaces/extensions/yarn.lock @@ -6649,6 +6649,7 @@ __metadata: resolution: "@internal/extensions@workspace:." dependencies: "@backstage/cli": "npm:^0.36.3" + "@backstage/cli-defaults": "npm:^0.1.3" "@backstage/e2e-test-utils": "npm:^0.1.2" "@backstage/repo-tools": "npm:^0.17.3" "@changesets/cli": "npm:^2.27.1"