From 5962c22c1e92da7c366fe2d8be8bb96ef61320c0 Mon Sep 17 00:00:00 2001 From: gazarenkov Date: Fri, 16 Jan 2026 12:32:25 +0200 Subject: [PATCH 1/3] initial flavor and namespaced config --- api/v1alpha3/zz_generated.deepcopy.go | 2 +- api/v1alpha4/zz_generated.deepcopy.go | 2 +- api/v1alpha5/backstage_types.go | 6 + api/v1alpha5/zz_generated.deepcopy.go | 2 +- .../crd/bases/rhdh.redhat.com_backstages.yaml | 6 + .../default-config/flavors/ai/app-config.yaml | 204 +++++++++++ .../flavors/ai/dynamic-plugins.yaml | 322 ++++++++++++++++++ .../flavors/orchestrator/dynamic-plugins.yaml | 61 ++++ config/profile/rhdh/kustomization.yaml | 11 +- .../rhdh/patches/deployment-patch.yaml | 17 +- examples/bs1.yaml | 2 + internal/controller/backstage_controller.go | 38 ++- pkg/model/appconfig_test.go | 7 +- pkg/model/configmapenvs_test.go | 12 +- pkg/model/configmapfiles_test.go | 14 +- pkg/model/db-secret_test.go | 6 +- pkg/model/db-statefulset_test.go | 43 +-- pkg/model/deployment_test.go | 22 +- pkg/model/dynamic-plugins_test.go | 25 +- pkg/model/model_tests.go | 11 +- pkg/model/namespacedconfig.go | 34 ++ pkg/model/pvcs_test.go | 10 +- pkg/model/route_test.go | 18 +- pkg/model/runtime.go | 35 +- pkg/model/runtime_test.go | 18 +- pkg/model/secretenvs.go | 10 + pkg/model/secretenvs_test.go | 12 +- pkg/model/secretfiles_test.go | 18 +- pkg/utils/utils.go | 27 +- 29 files changed, 848 insertions(+), 147 deletions(-) create mode 100644 config/profile/rhdh/default-config/flavors/ai/app-config.yaml create mode 100644 config/profile/rhdh/default-config/flavors/ai/dynamic-plugins.yaml create mode 100644 config/profile/rhdh/default-config/flavors/orchestrator/dynamic-plugins.yaml create mode 100644 pkg/model/namespacedconfig.go diff --git a/api/v1alpha3/zz_generated.deepcopy.go b/api/v1alpha3/zz_generated.deepcopy.go index b315b789e..6613a8896 100644 --- a/api/v1alpha3/zz_generated.deepcopy.go +++ b/api/v1alpha3/zz_generated.deepcopy.go @@ -5,7 +5,7 @@ package v1alpha3 import ( - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) diff --git a/api/v1alpha4/zz_generated.deepcopy.go b/api/v1alpha4/zz_generated.deepcopy.go index c9c126a0f..9c86c46ee 100644 --- a/api/v1alpha4/zz_generated.deepcopy.go +++ b/api/v1alpha4/zz_generated.deepcopy.go @@ -5,7 +5,7 @@ package v1alpha4 import ( - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) diff --git a/api/v1alpha5/backstage_types.go b/api/v1alpha5/backstage_types.go index cdd14bec7..348b7a3fe 100644 --- a/api/v1alpha5/backstage_types.go +++ b/api/v1alpha5/backstage_types.go @@ -23,6 +23,12 @@ type BackstageSpec struct { // Configuration for Backstage. Optional. Application *Application `json:"application,omitempty"` + // Flavor specifies a pre-configured template for Backstage deployment (e.g., "orchestrator", "ai"). + // When specified, operator loads default configuration from the flavor template instead of standard defaults. + // Optional. + // +optional + Flavor string `json:"flavor,omitempty"` + // Raw Runtime RuntimeObjects configuration. For Advanced scenarios. RawRuntimeConfig *RuntimeConfig `json:"rawRuntimeConfig,omitempty"` diff --git a/api/v1alpha5/zz_generated.deepcopy.go b/api/v1alpha5/zz_generated.deepcopy.go index 9e8b769c9..bd2d60544 100644 --- a/api/v1alpha5/zz_generated.deepcopy.go +++ b/api/v1alpha5/zz_generated.deepcopy.go @@ -5,7 +5,7 @@ package v1alpha5 import ( - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) diff --git a/config/crd/bases/rhdh.redhat.com_backstages.yaml b/config/crd/bases/rhdh.redhat.com_backstages.yaml index a1e7f8d80..a2b920375 100644 --- a/config/crd/bases/rhdh.redhat.com_backstages.yaml +++ b/config/crd/bases/rhdh.redhat.com_backstages.yaml @@ -1332,6 +1332,12 @@ spec: Optional. x-kubernetes-preserve-unknown-fields: true type: object + flavor: + description: |- + Flavor specifies a pre-configured template for Backstage deployment (e.g., "orchestrator", "ai"). + When specified, operator loads default configuration from the flavor template instead of standard defaults. + Optional. + type: string monitoring: default: enabled: false diff --git a/config/profile/rhdh/default-config/flavors/ai/app-config.yaml b/config/profile/rhdh/default-config/flavors/ai/app-config.yaml new file mode 100644 index 000000000..26c29bda3 --- /dev/null +++ b/config/profile/rhdh/default-config/flavors/ai/app-config.yaml @@ -0,0 +1,204 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: ai-app-config +data: + default.app-config.yaml: | + app: + title: AI Rolling Demo Developer Hub + baseUrl: "${RHDH_BASE_URL}" + analytics: + adoptionInsights: + maxBufferSize: 20 + flushInterval: 5000 + debug: false + licensedUsers: 50 + auth: + environment: production + session: + secret: "${BACKEND_SECRET}" + providers: + oidc: + production: + metadataUrl: "${KEYCLOAK_METADATA_URL}" + clientId: "${KEYCLOAK_CLIENT_ID}" + clientSecret: "${KEYCLOAK_CLIENT_SECRET}" + callbackUrl: "${RHDH_CALLBACK_URL}" + prompt: auto + signIn: + resolvers: + - resolver: preferredUsernameMatchingUserEntityName + development: + metadataUrl: "${KEYCLOAK_METADATA_URL}" + clientId: "${KEYCLOAK_CLIENT_ID}" + clientSecret: "${KEYCLOAK_CLIENT_SECRET}" + callbackUrl: "${RHDH_CALLBACK_URL}" + prompt: auto + signIn: + resolvers: + - resolver: preferredUsernameMatchingUserEntityName + backend: + actions: + pluginSources: + - 'software-catalog-mcp-tool' + - 'techdocs-mcp-tool' + auth: + externalAccess: + - type: static + options: + token: ${ADMIN_TOKEN} + subject: admin-curl-access + - type: static + options: + token: ${MCP_TOKEN} + subject: mcp-clients + keys: + - secret: "${BACKEND_SECRET}" + baseUrl: "${RHDH_BASE_URL}" + database: + connection: + password: ${POSTGRESQL_ADMIN_PASSWORD} + user: postgres + cors: + origin: "${RHDH_BASE_URL}" + # AI Experience config + csp: + upgrade-insecure-requests: false + img-src: + - "'self'" + - "data:" + - https://img.freepik.com + - https://cdn.dribbble.com + - https://upload.wikimedia.org + - https://podman-desktop.io + - https://argo-cd.readthedocs.io + - https://instructlab.ai + - https://quay.io + - https://news.mit.edu + script-src: + - "'self'" + - "'unsafe-eval'" + - https://cdn.jsdelivr.net + reading: + allow: + - host: example.com + - host: '*.mozilla.org' + - host: '*.openshift.com' + - host: '*.openshiftapps.com' + - host: '10.*:9090' + - host: '127.0.0.1:9090' + - host: '127.0.0.1:8888' + - host: '127.0.0.1:7070' + - host: 'localhost:9090' + - host: 'localhost:8888' + - host: 'localhost:7070' + signInPage: oidc + catalog: + rules: + - allow: [User, Group, System, Domain, Component, Resource, Location, Template, API] + locations: + - target: https://github.com/benwilcock/rhdh-techdocs/blob/main/rhdh-catalog-info.yaml + rules: + - allow: [Component, System] + type: url + - target: https://github.com/redhat-ai-dev/ai-lab-template/blob/ai-rolling-demo-1_8/all.yaml + type: url + providers: + modelCatalog: + development: + baseUrl: http://localhost:9090 + github: + providerId: + organization: "ai-rolling-demo" + schedule: + frequency: + minutes: 15 + initialDelay: + seconds: 15 + timeout: + minutes: 15 + githubOrg: + githubUrl: https://github.com + orgs: ["ai-rolling-demo"] + schedule: + frequency: + minutes: 15 + initialDelay: + seconds: 15 + timeout: + minutes: 15 + keycloakOrg: + default: + baseUrl: "${KEYCLOAK_BASE_URL}" + loginRealm: "${KEYCLOAK_REALM}" + realm: "${KEYCLOAK_REALM}" + clientId: "${KEYCLOAK_CLIENT_ID}" + clientSecret: "${KEYCLOAK_CLIENT_SECRET}" + schedule: + frequency: { minutes: 1 } + timeout: { minutes: 1 } + initialDelay: { seconds: 15 } + signIn: + resolvers: + - resolver: emailMatchingUserEntityProfileEmail + lightspeed: + mcpServers: + - name: mcp-integration-tools + token: ${MCP_TOKEN} + integrations: + github: + - apps: + - appId: ${GITHUB_APP_APP_ID} + clientId: ${GITHUB_APP_CLIENT_ID} + clientSecret: ${GITHUB_APP_CLIENT_SECRET} + webhookUrl: ${GITHUB_APP_WEBHOOK_URL} + webhookSecret: ${GITHUB_APP_WEBHOOK_SECRET} + privateKey: | + ${GITHUB_APP_PRIVATE_KEY} + host: github.com + kubernetes: + clusterLocatorMethods: + - clusters: + - authProvider: serviceAccount + name: default + serviceAccountToken: ${K8S_CLUSTER_TOKEN} + skipTLSVerify: true + url: https://kubernetes.default.svc + type: config + customResources: + - apiVersion: v1beta1 + group: tekton.dev + plural: pipelines + - apiVersion: v1beta1 + group: tekton.dev + plural: pipelineruns + - apiVersion: v1beta1 + group: tekton.dev + plural: taskruns + - apiVersion: v1 + group: route.openshift.io + plural: routes + serviceLocatorMethod: + type: multiTenant + argocd: + username: ${ARGOCD_USER} + password: ${ARGOCD_PASSWORD} + waitCycles: 25 + appLocatorMethods: + - type: 'config' + instances: + - name: default + url: https://${ARGOCD_HOSTNAME} + token: ${ARGOCD_API_TOKEN} + proxy: + endpoints: + "/developer-hub": + target: https://raw.githubusercontent.com + pathRewrite: + "^/api/proxy/developer-hub/learning-paths": "/redhat-developer/rhdh-plugins/refs/heads/main/workspaces/ai-integrations/plugins/ai-experience/src/learning-paths/data.json" + changeOrigin: true + secure: false + "/ai-rssfeed": + target: "https://news.mit.edu/topic/mitartificial-intelligence2-rss.xml" + changeOrigin: true + followRedirects: true diff --git a/config/profile/rhdh/default-config/flavors/ai/dynamic-plugins.yaml b/config/profile/rhdh/default-config/flavors/ai/dynamic-plugins.yaml new file mode 100644 index 000000000..97637b259 --- /dev/null +++ b/config/profile/rhdh/default-config/flavors/ai/dynamic-plugins.yaml @@ -0,0 +1,322 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: default-dynamic-plugins +data: + dynamic-plugins.yaml: | + includes: + - "dynamic-plugins.default.yaml" + plugins: + - package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-dynamic-home-page + disabled: true + - package: oci://quay.io/tpetkos/customized-sign-in-page:v0.1.0!red-hat-developer-hub-backstage-plugin-customized-sign-in-page + disabled: false + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-customized-sign-in-page: + signInPage: + importName: CustomizedSignInPage + - package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-global-header + disabled: false + pluginConfig: + dynamicPlugins: + frontend: + default.main-menu-items: + menuItems: + default.create: + title: '' + default.admin: + title: Administration + textKey: menuItem.administration + icon: admin + red-hat-developer-hub.backstage-plugin-global-header: + mountPoints: + - mountPoint: application/header + importName: GlobalHeader + config: + position: above-main-content # above-main-content | below-main-content + + - mountPoint: global.header/component + importName: SearchComponent + config: + priority: 100 + + - mountPoint: global.header/component + importName: Spacer + config: + priority: 99 + props: + growFactor: 0 + + - mountPoint: global.header/component + importName: HeaderIconButton + config: + priority: 90 + props: + title: Create... + icon: add + to: create + + - mountPoint: global.header/component + importName: StarredDropdown + config: + priority: 85 + + - mountPoint: global.header/component + importName: ApplicationLauncherDropdown + config: + priority: 82 + + - mountPoint: global.header/component + importName: SupportButton + config: + priority: 80 + + - mountPoint: global.header/component + importName: NotificationButton + config: + priority: 70 + + - mountPoint: global.header/component + importName: Divider + config: + priority: 50 + + - mountPoint: global.header/component + importName: ProfileDropdown + config: + priority: 10 + + - mountPoint: global.header/profile + importName: MenuItemLink + config: + priority: 100 + props: + title: Settings + link: /settings + icon: manageAccounts + + - mountPoint: global.header/profile + importName: LogoutButton + config: + priority: 10 + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Red Hat AI + sectionLink: https://www.redhat.com/en/products/ai + sectionLinkLabel: Read more + priority: 200 + props: + title: Podman Desktop + icon: https://podman-desktop.io/img/logo.svg + link: https://podman-desktop.io/ + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Red Hat AI + sectionLinkLabel: Read more + priority: 170 + props: + title: OpenShift AI + icon: https://upload.wikimedia.org/wikipedia/commons/d/d8/Red_Hat_logo.svg + link: https://rhods-dashboard-redhat-ods-applications.apps.rosa.redhat-ai-dev.m6no.p3.openshiftapps.com/ + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Red Hat AI + sectionLinkLabel: Read more + priority: 160 + props: + title: RHEL AI + icon: https://upload.wikimedia.org/wikipedia/commons/d/d8/Red_Hat_logo.svg + link: https://www.redhat.com/en/products/ai/enterprise-linux-ai + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Red Hat AI + sectionLinkLabel: Read more + priority: 150 + props: + title: Instructlab + icon: https://instructlab.ai/logo.png + link: https://instructlab.ai/ + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Quick Links + priority: 150 + props: + title: MCP Tools Guide + icon: https://upload.wikimedia.org/wikipedia/commons/0/01/Google_Docs_logo_%282014-2020%29.svg + link: https://docs.google.com/document/d/1o9qRYCAszGzTwW2mRjC4c9wFwJIx8J3XF1kGPFy5F1s/edit?tab=t.0 + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Quick Links + priority: 150 + props: + title: Quay.io + icon: https://quay.io/static/img/quay_favicon.png + link: https://quay.io + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Quick Links + priority: 140 + props: + title: Slack + icon: https://upload.wikimedia.org/wikipedia/commons/d/d5/Slack_icon_2019.svg + link: https://slack.com/ + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Quick Links + priority: 130 + props: + title: ArgoCD + icon: https://argo-cd.readthedocs.io/en/stable/assets/logo.png + link: https://argo-cd.readthedocs.io/en/stable/ + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Quick Links + priority: 120 + props: + title: Openshift + icon: https://upload.wikimedia.org/wikipedia/commons/d/d8/Red_Hat_logo.svg + link: https://www.redhat.com/en/technologies/cloud-computing/openshift + - package: oci://quay.io/karthik_jk/ai-experience:1.6.1!red-hat-developer-hub-backstage-plugin-ai-experience + disabled: false + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-ai-experience: + appIcons: + - name: aiNewsIcon + importName: AiNewsIcon + dynamicRoutes: + - path: / + importName: AiExperiencePage + - path: /ai-news + importName: AiNewsPage + menuItem: + icon: aiNewsIcon + text: AI News + - package: oci://quay.io/karthik_jk/ai-experience:1.6.1!red-hat-developer-hub-backstage-plugin-ai-experience-backend-dynamic + disabled: false + - package: ./dynamic-plugins/dist/backstage-plugin-kubernetes-backend-dynamic + disabled: false + - package: ./dynamic-plugins/dist/backstage-plugin-kubernetes + disabled: false + - package: ./dynamic-plugins/dist/backstage-community-plugin-catalog-backend-module-keycloak-dynamic + disabled: false + - package: ./dynamic-plugins/dist/backstage-community-plugin-redhat-argocd + disabled: false + pluginConfig: + dynamicPlugins: + frontend: + backstage-community.plugin-redhat-argocd: + mountPoints: + - mountPoint: entity.page.overview/cards + importName: ArgocdDeploymentSummary + config: + layout: + gridColumnEnd: + lg: "span 8" + xs: "span 12" + if: + allOf: + - isArgocdConfigured + - mountPoint: entity.page.cd/cards + importName: ArgocdDeploymentLifecycle + config: + layout: + gridColumn: '1 / -1' + if: + allOf: + - isArgocdConfigured + - disabled: false + package: ./dynamic-plugins/dist/roadiehq-backstage-plugin-argo-cd-backend-dynamic + - disabled: false + package: ./dynamic-plugins/dist/roadiehq-scaffolder-backend-argocd-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-techdocs-backend-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-techdocs + - disabled: false + package: ./dynamic-plugins/dist/backstage-community-plugin-topology + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-github-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-github-org-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-scaffolder-backend-module-github-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-scaffolder-backend-module-gitlab-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-gitlab-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-gitlab-org-dynamic + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-lightspeed:bs_1.42.5__1.0.3!red-hat-developer-hub-backstage-plugin-lightspeed + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-lightspeed: + translationResources: + - importName: lightspeedTranslations + module: Alpha + ref: lightspeedTranslationRef + appIcons: + - name: LightspeedIcon + module: LightspeedPlugin + importName: LightspeedIcon + dynamicRoutes: + - path: /lightspeed + importName: LightspeedPage + module: LightspeedPlugin + menuItem: + icon: LightspeedIcon + text: Lightspeed + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-lightspeed-backend:bs_1.42.5__1.0.3!red-hat-developer-hub-backstage-plugin-lightspeed-backend + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-plugin-mcp-actions-backend:next__0.1.2!backstage-plugin-mcp-actions-backend + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-software-catalog-mcp-tool:bs_1.42.5__0.2.3!red-hat-developer-hub-backstage-plugin-software-catalog-mcp-tool + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-techdocs-mcp-tool:bs_1.42.5__0.3.0!red-hat-developer-hub-backstage-plugin-techdocs-mcp-tool + - disabled: true + package: ./dynamic-plugins/dist/backstage-community-plugin-analytics-provider-segment + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-catalog-backend-module-model-catalog:bs_1.42.5__0.7.0!red-hat-developer-hub-backstage-plugin-catalog-backend-module-model-catalog + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-catalog-techdoc-url-reader-backend:bs_1.42.5__0.3.0!red-hat-developer-hub-backstage-plugin-catalog-techdoc-url-reader-backend + - disabled: false + package: ./dynamic-plugins/dist/backstage-community-plugin-tekton + pluginConfig: + dynamicPlugins: + frontend: + backstage-community.plugin-tekton: + mountPoints: + - config: + if: + allOf: + - isTektonCIAvailable + layout: + gridColumn: 1 / -1 + gridRowStart: 1 + importName: TektonCI + mountPoint: entity.page.ci/cards + diff --git a/config/profile/rhdh/default-config/flavors/orchestrator/dynamic-plugins.yaml b/config/profile/rhdh/default-config/flavors/orchestrator/dynamic-plugins.yaml new file mode 100644 index 000000000..5bfca70bc --- /dev/null +++ b/config/profile/rhdh/default-config/flavors/orchestrator/dynamic-plugins.yaml @@ -0,0 +1,61 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: default-dynamic-plugins +data: + dynamic-plugins.yaml: | + includes: + - dynamic-plugins.default.yaml + plugins: + - disabled: false + package: "@redhat/backstage-plugin-orchestrator@1.8.2" + integrity: sha512-rnUA6iZ2JVAyASfwS4P9HeFmpqCgH6FQouzzg4s6lCPAsYUFvu6tifJ3df5lThXPUTJ2cDvvQgamU+4DiHP2jw== + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-orchestrator: + appIcons: + - importName: OrchestratorIcon + name: orchestratorIcon + dynamicRoutes: + - importName: OrchestratorPage + menuItem: + icon: orchestratorIcon + text: Orchestrator + path: /orchestrator + entityTabs: + - path: /workflows + title: Workflows + mountPoint: entity.page.workflows + mountPoints: + - mountPoint: entity.page.workflows/cards + importName: OrchestratorCatalogTab + config: + layout: + gridColumn: '1 / -1' + if: + anyOf: + - IsOrchestratorCatalogTabAvailable + - disabled: false + package: "@redhat/backstage-plugin-orchestrator-backend-dynamic@1.8.2" + integrity: sha512-6G0YguzCM5nCDpOrIGJpLTXVMr6EBdIVqSXtsLH9RvBH25RTuFpfJ7q6eEp26DqveaiqUCfBpJ51smdjcsEzFQ== + pluginConfig: + orchestrator: + dataIndexService: + url: http://sonataflow-platform-data-index-service + dependencies: + - ref: sonataflow + - disabled: false + package: "@redhat/backstage-plugin-scaffolder-backend-module-orchestrator-dynamic@1.8.2" + integrity: sha512-N2hCn9RI/QVEoK56FAkGkSDbvfQCOIzVsJTwDX0kf//npO++2crRSJpB1Lr/m2UtYxfaXZX53p8sPcK3g8yWkQ== + pluginConfig: + orchestrator: + dataIndexService: + url: http://sonataflow-platform-data-index-service + - disabled: false + package: "@redhat/backstage-plugin-orchestrator-form-widgets@1.8.2" + integrity: sha512-Pe0dn3g+YTK3jbl36E8nt4zdyH/3w+MWgRyFWPc2B0eV4/L/aRfRC4KxcktmHPdamRGXTIaXL6cFae8TZl8Htw== + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-orchestrator-form-widgets: { } \ No newline at end of file diff --git a/config/profile/rhdh/kustomization.yaml b/config/profile/rhdh/kustomization.yaml index 34f8168ab..9601834f2 100644 --- a/config/profile/rhdh/kustomization.yaml +++ b/config/profile/rhdh/kustomization.yaml @@ -14,8 +14,8 @@ resources: images: - name: controller - newName: quay.io/rhdh/rhdh-rhel9-operator - newTag: "1.9" + newName: quay.io/rhdh-community/operator + newTag: next patches: - path: patches/deployment-patch.yaml @@ -46,3 +46,10 @@ configMapGenerator: - plugin-deps/argocd.yaml - plugin-deps/tekton.yaml name: plugin-deps +- files: + - default-config/flavors/orchestrator/dynamic-plugins.yaml + name: flavor-orchestrator +- files: + - default-config/flavors/ai/dynamic-plugins.yaml + - default-config/flavors/ai/app-config.yaml + name: flavor-ai \ No newline at end of file diff --git a/config/profile/rhdh/patches/deployment-patch.yaml b/config/profile/rhdh/patches/deployment-patch.yaml index 788c85337..3923aeba3 100644 --- a/config/profile/rhdh/patches/deployment-patch.yaml +++ b/config/profile/rhdh/patches/deployment-patch.yaml @@ -30,4 +30,19 @@ spec: value: quay.io/rhdh-community/rhdh:next - name: RELATED_IMAGE_catalog_index value: quay.io/rhdh/plugin-catalog-index:1.9 - + # flavor volume mounts + volumeMounts: + - mountPath: /default-config/flavors/orchestrator + name: flavor-orchestrator + - mountPath: /default-config/flavors/ai + name: flavor-ai + # flavor volumes + volumes: + - name: flavor-orchestrator + configMap: + name: flavor-orchestrator + optional: true + - name: flavor-ai + configMap: + name: flavor-ai + optional: true diff --git a/examples/bs1.yaml b/examples/bs1.yaml index 64fabf33d..771cbd7d1 100644 --- a/examples/bs1.yaml +++ b/examples/bs1.yaml @@ -2,3 +2,5 @@ apiVersion: rhdh.redhat.com/v1alpha5 kind: Backstage metadata: name: bs1 + annotations: + rhdh.redhat.com/flavor: "ai" diff --git a/internal/controller/backstage_controller.go b/internal/controller/backstage_controller.go index 7206335fa..874d63fb4 100644 --- a/internal/controller/backstage_controller.go +++ b/internal/controller/backstage_controller.go @@ -94,8 +94,14 @@ func (r *BackstageReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( setStatusCondition(&backstage, bs.BackstageConditionTypeDeployed, metav1.ConditionFalse, bs.BackstageConditionReasonInProgress, "Deployment process started") } - // 1. Preliminary read and prepare external config objects from the specs (configMaps, Secrets) - // 2. Make some validation to fail fast + // Discover namespaced config (user-provided defaults via labels) + namespacedConfig, err := r.discoverNamespacedConfig(ctx, backstage.Namespace, backstage.Spec.Flavor) + if err != nil { + return ctrl.Result{}, errorAndStatus(&backstage, "failed to discover namespaced config", err) + } + + // Preliminary read and prepare external config objects from the specs (configMaps, Secrets) + // Make some validation to fail fast externalConfig, err := r.preprocessSpec(ctx, backstage) if err != nil { return ctrl.Result{}, errorAndStatus(&backstage, "failed to preprocess backstage spec", err) @@ -106,8 +112,8 @@ func (r *BackstageReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{}, errorAndStatus(&backstage, "failed to apply ServiceMonitor", err) } - // This creates array of model objects to be reconsiled - bsModel, err := model.InitObjects(ctx, backstage, externalConfig, r.Platform, r.Scheme) + // This creates array of model objects to be reconciled + bsModel, err := model.InitObjects(ctx, backstage, externalConfig, namespacedConfig, r.Platform, r.Scheme) if err != nil { return ctrl.Result{}, errorAndStatus(&backstage, "failed to initialize backstage model", err) } @@ -231,6 +237,30 @@ func (r *BackstageReconciler) tryToDelete(ctx context.Context, obj client.Object return nil } +// discoverNamespacedConfig finds user-provided resources in the namespace via labels +// Returns secrets with label rhdh.redhat.com/default-config="" (generic) or matching flavor +func (r *BackstageReconciler) discoverNamespacedConfig(ctx context.Context, namespace string, flavor string) (model.NamespacedConfig, error) { + + namespacedConfig := model.NewNamespacedConfig() + + // List all secrets with default-config label (any value) + secretList := &corev1.SecretList{} + if err := r.List(ctx, secretList, namespacedConfig.ListOptions(namespace)); err != nil { + return namespacedConfig, fmt.Errorf("failed to list default config secrets: %w", err) + } + + for _, secret := range secretList.Items { + + labelValue := secret.Labels[model.DefaultConfigLabel] + // Include if generic (empty value) or matches flavor + if labelValue == "" || labelValue == flavor { + namespacedConfig.EnvSecrets = append(namespacedConfig.EnvSecrets, &secret) + } + } + + return namespacedConfig, nil +} + // SetupWithManager sets up the controller with the Manager. func (r *BackstageReconciler) SetupWithManager(mgr ctrl.Manager) error { diff --git a/pkg/model/appconfig_test.go b/pkg/model/appconfig_test.go index ccd3cbad4..60afebaee 100644 --- a/pkg/model/appconfig_test.go +++ b/pkg/model/appconfig_test.go @@ -65,7 +65,7 @@ func TestDefaultAppConfig(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("app-config.yaml", "raw-app-config.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Kubernetes, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Kubernetes, testObj.scheme) assert.NoError(t, err) assert.True(t, len(model.RuntimeObjects) > 0) @@ -97,8 +97,7 @@ func TestSpecifiedAppConfig(t *testing.T) { testObj.externalConfig.AppConfigKeys = map[string][]string{appConfigTestCm.Name: maps.Keys(appConfigTestCm.Data), appConfigTestCm2.Name: maps.Keys(appConfigTestCm2.Data), appConfigTestCm3.Name: maps.Keys(appConfigTestCm3.Data)} - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, - platform.Kubernetes, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Kubernetes, testObj.scheme) assert.NoError(t, err) assert.True(t, len(model.RuntimeObjects) > 0) @@ -131,7 +130,7 @@ func TestDefaultAndSpecifiedAppConfig(t *testing.T) { testObj.externalConfig.AppConfigKeys = map[string][]string{appConfigTestCm.Name: maps.Keys(appConfigTestCm.Data)} - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.True(t, len(model.RuntimeObjects) > 0) diff --git a/pkg/model/configmapenvs_test.go b/pkg/model/configmapenvs_test.go index 5f8eb55a1..30d5ef074 100644 --- a/pkg/model/configmapenvs_test.go +++ b/pkg/model/configmapenvs_test.go @@ -31,7 +31,7 @@ func TestDefaultConfigMapEnvFrom(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("configmap-envs.yaml", "raw-cm-envs.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) @@ -71,7 +71,7 @@ func TestSpecifiedConfigMapEnvs(t *testing.T) { testObj.externalConfig.ExtraEnvConfigMapKeys = map[string]DataObjectKeys{} testObj.externalConfig.ExtraEnvConfigMapKeys["mapName"] = NewDataObjectKeys(map[string]string{"mapName": "ENV1"}, nil) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) @@ -111,7 +111,7 @@ func TestDefaultAndSpecifiedConfigMapEnvFrom(t *testing.T) { testObj.externalConfig.ExtraEnvConfigMapKeys = map[string]DataObjectKeys{} testObj.externalConfig.ExtraEnvConfigMapKeys["mapName"] = NewDataObjectKeys(map[string]string{"mapName": "ENV1"}, nil) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) @@ -143,7 +143,7 @@ func TestSpecifiedCMEnvsWithContainers(t *testing.T) { testObj.externalConfig.ExtraEnvSecretKeys = map[string]DataObjectKeys{} testObj.externalConfig.ExtraEnvSecretKeys["cmName"] = NewDataObjectKeys(map[string]string{"cmName": "ENV1"}, nil) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) @@ -171,7 +171,7 @@ func TestSpecifiedCMEnvsWithContainers(t *testing.T) { testObj.externalConfig.ExtraEnvSecretKeys = map[string]DataObjectKeys{} testObj.externalConfig.ExtraEnvSecretKeys["cmName"] = NewDataObjectKeys(map[string]string{"cmName": "ENV1"}, nil) - model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) @@ -200,7 +200,7 @@ func TestCMEnvsWithNonExistedContainerFailed(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true) - _, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + _, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.ErrorContains(t, err, "not found") diff --git a/pkg/model/configmapfiles_test.go b/pkg/model/configmapfiles_test.go index eafd56274..f7f31ac10 100644 --- a/pkg/model/configmapfiles_test.go +++ b/pkg/model/configmapfiles_test.go @@ -37,7 +37,7 @@ func TestDefaultConfigMapFiles(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("configmap-files.yaml", "raw-cm-files.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) @@ -64,7 +64,7 @@ func TestSpecifiedConfigMapFiles(t *testing.T) { testObj.externalConfig.ExtraFileConfigMapKeys["cm2"] = NewDataObjectKeys(map[string]string{"conf2.yaml": "data"}, nil) testObj.externalConfig.ExtraFileConfigMapKeys["cm3"] = NewDataObjectKeys(map[string]string{"conf3.yaml": "data"}, nil) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.True(t, len(model.RuntimeObjects) > 0) @@ -100,7 +100,7 @@ func TestDefaultAndSpecifiedConfigMapFiles(t *testing.T) { testObj.externalConfig.ExtraFileConfigMapKeys = map[string]DataObjectKeys{} testObj.externalConfig.ExtraFileConfigMapKeys[appConfigTestCm.Name] = NewDataObjectKeys(nil, map[string][]byte{"conf1.yaml": []byte("data")}) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.True(t, len(model.RuntimeObjects) > 0) @@ -125,7 +125,7 @@ func TestSpecifiedConfigMapFilesWithBinaryData(t *testing.T) { testObj.externalConfig.ExtraFileConfigMapKeys = map[string]DataObjectKeys{} testObj.externalConfig.ExtraFileConfigMapKeys["cm1"] = NewDataObjectKeys(nil, map[string][]byte{"conf1.yaml": []byte("data")}) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.True(t, len(model.RuntimeObjects) > 0) @@ -154,7 +154,7 @@ func TestSpecifiedCMFilesWithContainers(t *testing.T) { testObj.externalConfig.ExtraFileConfigMapKeys["cm2"] = NewDataObjectKeys(map[string]string{"conf2.yaml": "data"}, nil) testObj.externalConfig.ExtraFileConfigMapKeys["cm3"] = NewDataObjectKeys(map[string]string{"conf3.yaml": "data"}, nil) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.True(t, len(model.RuntimeObjects) > 0) @@ -184,7 +184,7 @@ func TestCMFilesWithNonExistedContainerFailed(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true) - _, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + _, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.ErrorContains(t, err, "not found") @@ -201,7 +201,7 @@ func TestReplaceFiles(t *testing.T) { testObj.externalConfig.ExtraFileConfigMapKeys = map[string]DataObjectKeys{} testObj.externalConfig.ExtraFileConfigMapKeys[appConfigTestCm.Name] = NewDataObjectKeys(nil, map[string][]byte{"dynamic-plugins123.yaml": []byte("data")}) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) diff --git a/pkg/model/db-secret_test.go b/pkg/model/db-secret_test.go index 8058e278e..f1c30ace6 100644 --- a/pkg/model/db-secret_test.go +++ b/pkg/model/db-secret_test.go @@ -35,7 +35,7 @@ func TestEmptyDbSecret(t *testing.T) { // expected generatePassword = false (default db-secret defined) will come from preprocess testObj := createBackstageTest(bs).withDefaultConfig(true).withLocalDb().addToDefaultConfig("db-secret.yaml", "db-empty-secret.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model.LocalDbSecret) @@ -54,7 +54,7 @@ func TestDefaultWithGeneratedSecrets(t *testing.T) { // expected generatePassword = true (no db-secret defined) will come from preprocess testObj := createBackstageTest(bs).withDefaultConfig(true).withLocalDb().addToDefaultConfig("db-secret.yaml", "db-generated-secret.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.Equal(t, fmt.Sprintf("backstage-psql-secret-%s", bs.Name), model.LocalDbSecret.secret.Name) @@ -75,7 +75,7 @@ func TestSpecifiedSecret(t *testing.T) { // expected generatePassword = false (db-secret defined in the spec) will come from preprocess testObj := createBackstageTest(bs).withDefaultConfig(true).withLocalDb().addToDefaultConfig("db-secret.yaml", "db-generated-secret.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.Nil(t, model.LocalDbSecret) diff --git a/pkg/model/db-statefulset_test.go b/pkg/model/db-statefulset_test.go index e63841a50..ec79c5581 100644 --- a/pkg/model/db-statefulset_test.go +++ b/pkg/model/db-statefulset_test.go @@ -34,7 +34,7 @@ func TestDefault(t *testing.T) { bs := *dbStatefulSetBackstage.DeepCopy() testObj := createBackstageTest(bs).withDefaultConfig(true) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.Equal(t, model.LocalDbService.service.Name, model.localDbStatefulSet.statefulSet.Spec.ServiceName) @@ -52,47 +52,8 @@ func TestOverrideDbImage(t *testing.T) { _ = os.Setenv(LocalDbImageEnvVar, "dummy") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.Equal(t, "dummy", model.localDbStatefulSet.statefulSet.Spec.Template.Spec.Containers[0].Image) } - -// test bs.Spec.Application.ImagePullSecrets shared with StatefulSet -//func TestImagePullSecretSpec(t *testing.T) { -// //bs := *dbStatefulSetBackstage.DeepCopy() -// //bs.Spec.Application.ImagePullSecrets = []string{"my-secret1", "my-secret2"} -// // -// //testObj := createBackstageTest(bs).withDefaultConfig(true) -// //model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) -// //assert.NoError(t, err) -// // -// //assert.Equal(t, 2, len(model.localDbStatefulSet.statefulSet.Spec.Template.Spec.ImagePullSecrets)) -// //assert.Equal(t, "my-secret1", model.localDbStatefulSet.statefulSet.Spec.Template.Spec.ImagePullSecrets[0].Name) -// //assert.Equal(t, "my-secret2", model.localDbStatefulSet.statefulSet.Spec.Template.Spec.ImagePullSecrets[1].Name) -// -// // no image pull secrets specified -// bs := *dbStatefulSetBackstage.DeepCopy() -// testObj := createBackstageTest(bs).withDefaultConfig(true). -// addToDefaultConfig("db-statefulset.yaml", "ips-db-statefulset.yaml") -// -// model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) -// if assert.NoError(t, err) { -// // if imagepullsecrets not defined - default used -// assert.Equal(t, 2, len(model.localDbStatefulSet.statefulSet.Spec.Template.Spec.ImagePullSecrets)) -// assert.Equal(t, "ips-db1", model.localDbStatefulSet.statefulSet.Spec.Template.Spec.ImagePullSecrets[0].Name) -// assert.Equal(t, "ips-db2", model.localDbStatefulSet.statefulSet.Spec.Template.Spec.ImagePullSecrets[1].Name) -// } -// -// // empty list of image pull secrets -// //bs = *dbStatefulSetBackstage.DeepCopy() -// //bs.Spec.Application.ImagePullSecrets = []string{} -// // -// //testObj = createBackstageTest(bs).withDefaultConfig(true). -// // addToDefaultConfig("db-statefulset.yaml", "ips-db-statefulset.yaml") -// // -// //model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) -// //if assert.NoError(t, err) { -// // assert.Equal(t, 0, len(model.localDbStatefulSet.statefulSet.Spec.Template.Spec.ImagePullSecrets)) -// //} -//} diff --git a/pkg/model/deployment_test.go b/pkg/model/deployment_test.go index c566d6029..d66106882 100644 --- a/pkg/model/deployment_test.go +++ b/pkg/model/deployment_test.go @@ -37,7 +37,7 @@ func TestWorkingDirMount(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true). addToDefaultConfig("deployment.yaml", "working-dir-mount.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.Equal(t, "/my/home", model.backstageDeployment.defaultMountPath()) @@ -61,7 +61,7 @@ func TestOverrideBackstageImage(t *testing.T) { t.Setenv(BackstageImageEnvVar, "dummy") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.Equal(t, 2, len(model.backstageDeployment.podSpec().Containers)) @@ -77,7 +77,7 @@ func TestSpecImagePullSecrets(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true). addToDefaultConfig("deployment.yaml", "ips-deployment.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.OpenShift, testObj.scheme) assert.NoError(t, err) // if imagepullsecrets not defined - default used @@ -133,7 +133,7 @@ spec: testObj := createBackstageTest(bs).withDefaultConfig(true). addToDefaultConfig("deployment.yaml", "rhdh-deployment.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.OpenShift, testObj.scheme) assert.NoError(t, err) // label added @@ -177,12 +177,12 @@ spec: testObj := createBackstageTest(bs).withDefaultConfig(true) - model, err := InitObjects(context.TODO(), bsv1.Backstage{}, testObj.externalConfig, platform.OpenShift, testObj.scheme) + model, err := InitObjects(context.TODO(), bsv1.Backstage{}, testObj.externalConfig, testObj.namespacedConfig, platform.OpenShift, testObj.scheme) assert.NoError(t, err) // make sure env var works assert.Equal(t, "envvar-image", model.backstageDeployment.container().Image) - model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) + model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.OpenShift, testObj.scheme) assert.NoError(t, err) // make sure image defined in CR overrides assert.Equal(t, "cr-image", model.backstageDeployment.container().Image) @@ -207,7 +207,7 @@ spec: testObj := createBackstageTest(bs).withDefaultConfig(true) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model.backstageDeployment) d := model.backstageDeployment @@ -260,7 +260,7 @@ spec: testObj := createBackstageTest(bs).withDefaultConfig(true) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model.backstageDeployment) sidecar := model.backstageDeployment.containerByName("sidecar") @@ -277,14 +277,14 @@ func TestDeploymentKind(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) depPodSpec := model.backstageDeployment.podSpec() bs.Spec.Deployment.Kind = "StatefulSet" testObj = createBackstageTest(bs).withDefaultConfig(true) - model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.Equal(t, "StatefulSet", model.backstageDeployment.deployable.GetObject().GetObjectKind().GroupVersionKind().Kind) @@ -305,7 +305,7 @@ spec: } testObj := createBackstageTest(bs).withDefaultConfig(true) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.Equal(t, "StatefulSet", model.backstageDeployment.deployable.GetObject().GetObjectKind().GroupVersionKind().Kind) diff --git a/pkg/model/dynamic-plugins_test.go b/pkg/model/dynamic-plugins_test.go index be770b9d2..2aad93334 100644 --- a/pkg/model/dynamic-plugins_test.go +++ b/pkg/model/dynamic-plugins_test.go @@ -41,7 +41,7 @@ func TestDynamicPluginsValidationFailed(t *testing.T) { testObj := createBackstageTest(*bs).withDefaultConfig(true). addToDefaultConfig("dynamic-plugins.yaml", "raw-dynamic-plugins.yaml") - _, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, platform.Default, testObj.scheme) + _, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) //"failed object validation, reason: failed to find initContainer named install-dynamic-plugins") assert.Error(t, err) @@ -62,7 +62,7 @@ func TestDynamicPluginsInvalidKeyName(t *testing.T) { Data: map[string]string{"WrongKeyName.yml": "tt"}, } - _, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, platform.Default, testObj.scheme) + _, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.Error(t, err) //assert.Contains(t, err.Error(), "expects exactly one Data key named 'dynamic-plugins.yaml'") @@ -79,7 +79,7 @@ func TestDefaultDynamicPlugins(t *testing.T) { addToDefaultConfig("dynamic-plugins.yaml", "raw-dynamic-plugins.yaml"). addToDefaultConfig("deployment.yaml", "rhdh-deployment.yaml") - model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model.backstageDeployment) @@ -117,7 +117,7 @@ func TestDefaultAndSpecifiedDynamicPlugins(t *testing.T) { Data: map[string]string{DynamicPluginsFile: "dynamic-plugins.yaml: | \n plugins: []"}, } - model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) @@ -149,7 +149,7 @@ func TestSpecifiedOnlyDynamicPlugins(t *testing.T) { Data: map[string]string{DynamicPluginsFile: "dynamic-plugins.yaml: | \n plugins: []"}, } - model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) @@ -176,7 +176,7 @@ func TestDynamicPluginsFailOnArbitraryDepl(t *testing.T) { testObj := createBackstageTest(*bs).withDefaultConfig(true). addToDefaultConfig("dynamic-plugins.yaml", "raw-dynamic-plugins.yaml") - _, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, platform.Default, testObj.scheme) + _, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.Error(t, err) } @@ -188,7 +188,7 @@ func TestNotConfiguredDPsNotInTheModel(t *testing.T) { testObj := createBackstageTest(*bs).withDefaultConfig(true) - m, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, platform.Default, testObj.scheme) + m, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) for _, obj := range m.RuntimeObjects { @@ -221,7 +221,7 @@ plugins: Data: map[string]string{DynamicPluginsFile: yamlData}, } - model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) @@ -248,7 +248,6 @@ func initContainer(model *BackstageModel) *corev1.Container { return nil } - // TestCatalogIndexImageFromDefaultConfig verifies that the operator sets CATALOG_INDEX_IMAGE // on the install-dynamic-plugins init container from the default config by default func TestCatalogIndexImageFromDefaultConfig(t *testing.T) { @@ -258,7 +257,7 @@ func TestCatalogIndexImageFromDefaultConfig(t *testing.T) { addToDefaultConfig("dynamic-plugins.yaml", "raw-dynamic-plugins.yaml"). addToDefaultConfig("deployment.yaml", "rhdh-deployment.yaml") - model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model.backstageDeployment) @@ -286,7 +285,7 @@ func TestCatalogIndexImageOverridesDefaultConfig(t *testing.T) { // Set RELATED_IMAGE_catalog_index to a DIFFERENT value - this should override the default config t.Setenv(CatalogIndexImageEnvVar, "quay.io/fake-reg/img:1.2.3") - model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model.backstageDeployment) @@ -326,7 +325,7 @@ spec: // Set RELATED_IMAGE_catalog_index - but user's patch should take precedence t.Setenv(CatalogIndexImageEnvVar, "quay.io/rhdh/plugin-catalog-index:related-image") - model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model.backstageDeployment) @@ -358,7 +357,7 @@ func TestCatalogIndexImageExtraEnvsOverride(t *testing.T) { // This should NOT override the user's extraEnvs value t.Setenv(CatalogIndexImageEnvVar, "quay.io/rhdh/plugin-catalog-index:related-image") - model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), *bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model.backstageDeployment) diff --git a/pkg/model/model_tests.go b/pkg/model/model_tests.go index a7d497218..b79756d22 100644 --- a/pkg/model/model_tests.go +++ b/pkg/model/model_tests.go @@ -25,9 +25,10 @@ import ( // withDefaultConfig(useDef bool) // addToDefaultConfig(key, fileName) type testBackstageObject struct { - backstage bsv1.Backstage - externalConfig ExternalConfig - scheme *runtime.Scheme + backstage bsv1.Backstage + externalConfig ExternalConfig + namespacedConfig NamespacedConfig + scheme *runtime.Scheme } // initialises testBackstageObject object @@ -35,7 +36,9 @@ func createBackstageTest(bs bsv1.Backstage) *testBackstageObject { ec := ExternalConfig{ RawConfig: map[string]string{}, } - b := &testBackstageObject{backstage: bs, externalConfig: ec, scheme: runtime.NewScheme()} + nc := NewNamespacedConfig() + + b := &testBackstageObject{backstage: bs, externalConfig: ec, namespacedConfig: nc, scheme: runtime.NewScheme()} utilruntime.Must(bsv1.AddToScheme(b.scheme)) utilruntime.Must(clientgoscheme.AddToScheme(b.scheme)) utilruntime.Must(openshift.Install(b.scheme)) diff --git a/pkg/model/namespacedconfig.go b/pkg/model/namespacedconfig.go new file mode 100644 index 000000000..a28b1bdf6 --- /dev/null +++ b/pkg/model/namespacedconfig.go @@ -0,0 +1,34 @@ +package model + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// NamespacedConfig holds user-provided resources discovered via labels in the namespace +// These are shared defaults (not instance-specific) that complement operator's built-in defaults +type NamespacedConfig struct { + // Secrets to be injected as environment variables + // Discovered via rhdh.redhat.com/default-config label (empty value or flavor name) + EnvSecrets []*corev1.Secret +} + +func NewNamespacedConfig() NamespacedConfig { + return NamespacedConfig{ + EnvSecrets: []*corev1.Secret{}, + } +} + +// ListOptions returns client.ListOptions configured to select secrets with default-config label +func (nc *NamespacedConfig) ListOptions(namespace string) *client.ListOptions { + selector := labels.NewSelector() + requirement, _ := labels.NewRequirement(DefaultConfigLabel, selection.Exists, nil) + selector = selector.Add(*requirement) + + return &client.ListOptions{ + Namespace: namespace, + LabelSelector: selector, + } +} diff --git a/pkg/model/pvcs_test.go b/pkg/model/pvcs_test.go index 2d4f5978d..b7f3ba7d8 100644 --- a/pkg/model/pvcs_test.go +++ b/pkg/model/pvcs_test.go @@ -40,7 +40,7 @@ func TestDefaultPvcs(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("pvcs.yaml", "multi-pvc.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.OpenShift, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) @@ -72,7 +72,7 @@ func TestMultiContainersPvc(t *testing.T) { } testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("deployment.yaml", "multicontainer-deployment.yaml").addToDefaultConfig("pvcs.yaml", "multi-pvc-containers.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.OpenShift, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) assert.Equal(t, 4, len(model.backstageDeployment.allContainers())) @@ -115,7 +115,7 @@ func TestSpecifiedPvcs(t *testing.T) { testObj.externalConfig.ExtraPvcKeys = []string{"my-pvc1", "my-pvc2"} - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.OpenShift, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) d := model.backstageDeployment @@ -155,7 +155,7 @@ func TestSpecifiedPvcsWithContainers(t *testing.T) { testObj.externalConfig.ExtraPvcKeys = []string{"my-pvc1", "my-pvc2"} - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.OpenShift, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) d := model.backstageDeployment @@ -183,7 +183,7 @@ func TestPvcsWithNonExistedContainerFailed(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true) - _, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + _, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.ErrorContains(t, err, "not found") diff --git a/pkg/model/route_test.go b/pkg/model/route_test.go index 7081a81b7..de7356601 100644 --- a/pkg/model/route_test.go +++ b/pkg/model/route_test.go @@ -36,7 +36,7 @@ func TestDefaultRoute(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("route.yaml", "raw-route.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.OpenShift, testObj.scheme) assert.NoError(t, err) @@ -74,7 +74,7 @@ func TestSpecifiedRoute(t *testing.T) { // Test w/o default route configured testObjNoDef := createBackstageTest(bs).withDefaultConfig(true) - model, err := InitObjects(context.TODO(), bs, testObjNoDef.externalConfig, platform.OpenShift, testObjNoDef.scheme) + model, err := InitObjects(context.TODO(), bs, testObjNoDef.externalConfig, testObjNoDef.namespacedConfig, platform.OpenShift, testObjNoDef.scheme) assert.NoError(t, err) assert.NotNil(t, model.route) @@ -85,7 +85,7 @@ func TestSpecifiedRoute(t *testing.T) { // Test with default route configured testObjWithDef := testObjNoDef.addToDefaultConfig("route.yaml", "raw-route.yaml") - model, err = InitObjects(context.TODO(), bs, testObjWithDef.externalConfig, platform.OpenShift, testObjWithDef.scheme) + model, err = InitObjects(context.TODO(), bs, testObjWithDef.externalConfig, testObjWithDef.namespacedConfig, platform.OpenShift, testObjWithDef.scheme) assert.NoError(t, err) assert.NotNil(t, model.route) @@ -118,13 +118,13 @@ func TestDisabledRoute(t *testing.T) { // With def route config testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("route.yaml", "raw-route.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.OpenShift, testObj.scheme) assert.NoError(t, err) assert.Nil(t, model.route) // W/o def route config testObj = createBackstageTest(bs).withDefaultConfig(true) - model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) + model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.OpenShift, testObj.scheme) assert.NoError(t, err) assert.Nil(t, model.route) @@ -143,13 +143,13 @@ func TestExcludedRoute(t *testing.T) { // With def route config - create default route testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("route.yaml", "raw-route.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.OpenShift, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model.route) // W/o def route config - do not create route testObj = createBackstageTest(bs).withDefaultConfig(true) - model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) + model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.OpenShift, testObj.scheme) assert.NoError(t, err) assert.Nil(t, model.route) } @@ -170,13 +170,13 @@ func TestEnabledRoute(t *testing.T) { // With def route config testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("route.yaml", "raw-route.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.OpenShift, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model.route) // W/o def route config testObj = createBackstageTest(bs).withDefaultConfig(true) - model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) + model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.OpenShift, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model.route) diff --git a/pkg/model/runtime.go b/pkg/model/runtime.go index 108bdc952..60522bd76 100644 --- a/pkg/model/runtime.go +++ b/pkg/model/runtime.go @@ -25,10 +25,13 @@ import ( "github.com/redhat-developer/rhdh-operator/pkg/utils" ) -const BackstageAppLabel = "rhdh.redhat.com/app" -const ConfiguredNameAnnotation = "rhdh.redhat.com/configured-name" -const DefaultMountPathAnnotation = "rhdh.redhat.com/mount-path" -const ContainersAnnotation = "rhdh.redhat.com/containers" +const ( + BackstageAppLabel = "rhdh.redhat.com/app" + ConfiguredNameAnnotation = "rhdh.redhat.com/configured-name" + DefaultMountPathAnnotation = "rhdh.redhat.com/mount-path" + ContainersAnnotation = "rhdh.redhat.com/containers" + DefaultConfigLabel = "rhdh.redhat.com/default-config" +) // Backstage configuration scaffolding with empty BackstageObjects. // There are all possible objects for configuration @@ -53,6 +56,8 @@ type BackstageModel struct { RuntimeObjects []RuntimeObject ExternalConfig ExternalConfig + + NamespacedConfig NamespacedConfig } func (m *BackstageModel) setRuntimeObject(object RuntimeObject) { @@ -98,7 +103,7 @@ func registerConfig(key string, factory ObjectFactory, multiple bool) { } // InitObjects performs a main loop for configuring and making the array of objects to reconcile -func InitObjects(ctx context.Context, backstage bsv1.Backstage, externalConfig ExternalConfig, platform platform.Platform, scheme *runtime.Scheme) (*BackstageModel, error) { +func InitObjects(ctx context.Context, backstage bsv1.Backstage, externalConfig ExternalConfig, namespacedConfig NamespacedConfig, platform platform.Platform, scheme *runtime.Scheme) (*BackstageModel, error) { // 3 phases of Backstage configuration: // 1- load from Operator defaults, modify metadata (labels, selectors..) and namespace as needed @@ -109,7 +114,17 @@ func InitObjects(ctx context.Context, backstage bsv1.Backstage, externalConfig E lg := log.FromContext(ctx) lg.V(1) - model := &BackstageModel{RuntimeObjects: make([]RuntimeObject, 0), ExternalConfig: externalConfig, localDbEnabled: backstage.Spec.IsLocalDbEnabled(), isOpenshift: platform.IsOpenshift(), DynamicPlugins: DynamicPlugins{}} + flavor := backstage.Spec.Flavor + if flavor != "" { + lg.Info("initializing with flavor", "flavor", flavor) + } + + model := &BackstageModel{RuntimeObjects: make([]RuntimeObject, 0), + ExternalConfig: externalConfig, + NamespacedConfig: namespacedConfig, + localDbEnabled: backstage.Spec.IsLocalDbEnabled(), + isOpenshift: platform.IsOpenshift(), + DynamicPlugins: DynamicPlugins{}} ecs := make([]ExternalConfigContributor, 0) // looping through the registered runtimeConfig objects initializing the model @@ -118,8 +133,12 @@ func InitObjects(ctx context.Context, backstage bsv1.Backstage, externalConfig E // creating the instance of backstageObject backstageObject := conf.ObjectFactory.newBackstageObject() - //var templ = backstageObject.EmptyObject() - if objs, err := utils.ReadYamlFiles(utils.DefFile(conf.Key), *scheme, platform.Extension); err != nil { + defFile, err := utils.DefFile(conf.Key, flavor) + if err != nil { + return nil, fmt.Errorf("failed to get config file path: %w", err) + } + + if objs, err := utils.ReadYamlFiles(defFile, *scheme, platform.Extension); err != nil { if !errors.Is(err, os.ErrNotExist) { return nil, fmt.Errorf("failed to read default value for the key %s, reason: %s", conf.Key, err) } diff --git a/pkg/model/runtime_test.go b/pkg/model/runtime_test.go index 614677934..b7be9a9c7 100644 --- a/pkg/model/runtime_test.go +++ b/pkg/model/runtime_test.go @@ -44,7 +44,7 @@ func TestInitDefaultDeploy(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) bsDeployment := model.backstageDeployment @@ -83,7 +83,7 @@ func TestIfEmptyObjectIsValid(t *testing.T) { assert.False(t, bs.Spec.IsLocalDbEnabled()) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.Equal(t, 2, len(model.RuntimeObjects)) @@ -105,7 +105,7 @@ func TestAddToModel(t *testing.T) { } testObj := createBackstageTest(bs).withDefaultConfig(true) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) assert.NotNil(t, model.RuntimeObjects) @@ -158,14 +158,14 @@ spec: } // No raw config - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model.backstageService) assert.Equal(t, "true", model.backstageService.service.GetLabels()["default"]) assert.Empty(t, model.backstageService.service.GetLabels()["raw"]) // Put raw config - model, err = InitObjects(context.TODO(), bs, extConfig, platform.Default, testObj.scheme) + model, err = InitObjects(context.TODO(), bs, extConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model.backstageService) assert.Equal(t, "true", model.backstageService.service.GetLabels()["raw"]) @@ -175,7 +175,7 @@ spec: func TestMultiobject(t *testing.T) { bs := bsv1.Backstage{} testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("pvcs.yaml", "multi-pvc.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) found := false @@ -192,7 +192,7 @@ func TestMultiobject(t *testing.T) { func TestSingleMultiobject(t *testing.T) { bs := bsv1.Backstage{} testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("pvcs.yaml", "single-pvc.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) found := false @@ -209,14 +209,14 @@ func TestSingleMultiobject(t *testing.T) { func TestSingleFailedWithMultiDefinition(t *testing.T) { bs := bsv1.Backstage{} testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("service.yaml", "multi-service-err.yaml") - _, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + _, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.EqualError(t, err, "failed to initialize object: multiple objects not expected for: service.yaml") } func TestInvalidObjectKind(t *testing.T) { bs := bsv1.Backstage{} testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("service.yaml", "invalid-service-type.yaml") - _, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + _, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.ErrorContains(t, err, "failed to read default value for the key service.yaml") } diff --git a/pkg/model/secretenvs.go b/pkg/model/secretenvs.go index f7de4c806..5223cd307 100644 --- a/pkg/model/secretenvs.go +++ b/pkg/model/secretenvs.go @@ -30,6 +30,16 @@ func init() { } func (p *SecretEnvs) addExternalConfig(spec bsv1.BackstageSpec) error { + + // namespaced config secrets first (all keys as env vars) + for _, secret := range p.model.NamespacedConfig.EnvSecrets { + err := p.model.backstageDeployment.addEnvVarsFrom(containersFilter{}, SecretObjectKind, secret.Name, "") + if err != nil { + return fmt.Errorf("failed to add namespaced config secret env vars %s: %w", secret.Name, err) + } + } + + // spec defined secrets if spec.Application == nil || spec.Application.ExtraEnvs == nil || spec.Application.ExtraEnvs.Secrets == nil { return nil } diff --git a/pkg/model/secretenvs_test.go b/pkg/model/secretenvs_test.go index f1456ec37..c98ceeba8 100644 --- a/pkg/model/secretenvs_test.go +++ b/pkg/model/secretenvs_test.go @@ -35,7 +35,7 @@ func TestDefaultSecretEnvFrom(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig(SecretEnvsObjectKey, "raw-sec-envs.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) @@ -55,7 +55,7 @@ func TestDefaultMultiSecretEnv(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("deployment.yaml", "multicontainer-deployment.yaml"). addToDefaultConfig(SecretEnvsObjectKey, "raw-multi-secret.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) @@ -85,7 +85,7 @@ func TestSpecifiedSecretEnvs(t *testing.T) { testObj.externalConfig.ExtraEnvConfigMapKeys = map[string]DataObjectKeys{} testObj.externalConfig.ExtraEnvConfigMapKeys["secName"] = NewDataObjectKeys(map[string]string{"secName": "ENV1"}, nil) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) @@ -118,7 +118,7 @@ func TestSpecifiedSecretEnvsWithContainers(t *testing.T) { testObj.externalConfig.ExtraEnvSecretKeys = map[string]DataObjectKeys{} testObj.externalConfig.ExtraEnvSecretKeys["secName"] = NewDataObjectKeys(map[string]string{"secName": "ENV1"}, nil) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) @@ -146,7 +146,7 @@ func TestSpecifiedSecretEnvsWithContainers(t *testing.T) { testObj.externalConfig.ExtraEnvSecretKeys = map[string]DataObjectKeys{} testObj.externalConfig.ExtraEnvSecretKeys["secName"] = NewDataObjectKeys(map[string]string{"secName": "ENV1"}, nil) - model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) @@ -175,7 +175,7 @@ func TestSecretEnvsWithNonExistedContainerFailed(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true) - _, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + _, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.ErrorContains(t, err, "not found") diff --git a/pkg/model/secretfiles_test.go b/pkg/model/secretfiles_test.go index b4383b585..64319febb 100644 --- a/pkg/model/secretfiles_test.go +++ b/pkg/model/secretfiles_test.go @@ -41,7 +41,7 @@ func TestDefaultSecretFiles(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig(SecretFilesObjectKey, "raw-secret-files.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) @@ -69,7 +69,7 @@ func TestDefaultMultiSecretFiles(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("deployment.yaml", "multicontainer-deployment.yaml"). addToDefaultConfig(SecretFilesObjectKey, "raw-multi-secret.yaml") - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.NotNil(t, model) @@ -102,7 +102,7 @@ func TestSpecifiedSecretFiles(t *testing.T) { testObj.externalConfig.ExtraFileSecretKeys["secret2"] = NewDataObjectKeys(map[string]string{"conf.yaml": "data"}, nil) testObj.externalConfig.ExtraFileSecretKeys["secret.dot"] = NewDataObjectKeys(nil, map[string][]byte{"conf3.yaml": []byte("data")}) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.True(t, len(model.RuntimeObjects) > 0) @@ -136,7 +136,7 @@ func TestFailedValidation(t *testing.T) { *sf = append(*sf, bsv1.FileObjectRef{Name: "secret1"}) testObj := createBackstageTest(bs).withDefaultConfig(true) - _, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + _, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.ErrorContains(t, err, "key or mountPath has to be specified for secret secret1") } @@ -150,7 +150,7 @@ func TestDefaultAndSpecifiedSecretFiles(t *testing.T) { testObj.externalConfig.ExtraFileSecretKeys = map[string]DataObjectKeys{"secret1": NewDataObjectKeys(map[string]string{"conf.yaml": ""}, nil)} - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.True(t, len(model.RuntimeObjects) > 0) @@ -173,7 +173,7 @@ func TestSpecifiedSecretFilesWithDataAndKey(t *testing.T) { testObj.externalConfig.ExtraFileSecretKeys = map[string]DataObjectKeys{"secret1": NewDataObjectKeys(nil, map[string][]byte{"conf.yaml": []byte("")})} - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.True(t, len(model.RuntimeObjects) > 0) @@ -204,7 +204,7 @@ func TestSpecifiedSecretFilesWithContainers(t *testing.T) { testObj.externalConfig.ExtraFileSecretKeys["secret2"] = NewDataObjectKeys(map[string]string{"conf2.yaml": "data"}, nil) testObj.externalConfig.ExtraFileSecretKeys["secret3"] = NewDataObjectKeys(map[string]string{"conf3.yaml": "data"}, nil) - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) assert.True(t, len(model.RuntimeObjects) > 0) @@ -234,7 +234,7 @@ func TestSecretFilesWithNonExistedContainerFailed(t *testing.T) { testObj := createBackstageTest(bs).withDefaultConfig(true) - _, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + _, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.ErrorContains(t, err, "not found") @@ -250,7 +250,7 @@ func TestReplaceSecretFiles(t *testing.T) { testObj.externalConfig.ExtraFileSecretKeys = map[string]DataObjectKeys{"secret1": NewDataObjectKeys(map[string]string{"dynamic-plugins321.yaml": "new"}, nil)} - model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) + model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, testObj.namespacedConfig, platform.Default, testObj.scheme) assert.NoError(t, err) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index c153cdef7..7a6afdfc1 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -239,8 +239,31 @@ func GetObjectKind(object client.Object, scheme *runtime.Scheme) *schema.GroupVe return &gvks[0] } -func DefFile(key string) string { - return filepath.Join(os.Getenv("LOCALBIN"), "default-config", key) +func DefFile(key string, flavor string) (string, error) { + localBin := os.Getenv("LOCALBIN") + if localBin == "" { + localBin = "." + } + + if flavor != "" { + // Check flavor directory exists + flavorDir := filepath.Join(localBin, "default-config", "flavors", flavor) + if _, err := os.Stat(flavorDir); err != nil { + if os.IsNotExist(err) { + return "", fmt.Errorf("flavor '%s' does not exist: directory not found at %s", flavor, flavorDir) + } + return "", fmt.Errorf("failed to check flavor directory '%s': %w", flavor, err) + } + + // Try flavor file first + flavorPath := filepath.Join(flavorDir, key) + if _, err := os.Stat(flavorPath); err == nil { + return flavorPath, nil + } + } + + // Fallback to default-config + return filepath.Join(localBin, "default-config", key), nil } func GeneratePassword(length int) (string, error) { From 8835b633763f171b89a4952bcb3e60d9c0eacd1f Mon Sep 17 00:00:00 2001 From: gazarenkov Date: Fri, 16 Jan 2026 21:19:32 +0200 Subject: [PATCH 2/3] fix --- examples/bs1.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/bs1.yaml b/examples/bs1.yaml index 771cbd7d1..f606359eb 100644 --- a/examples/bs1.yaml +++ b/examples/bs1.yaml @@ -2,5 +2,4 @@ apiVersion: rhdh.redhat.com/v1alpha5 kind: Backstage metadata: name: bs1 - annotations: - rhdh.redhat.com/flavor: "ai" + From 227254123b93c88f277202e3da37a55e8a104ebd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 19 Jan 2026 14:28:32 +0000 Subject: [PATCH 3/3] Regenerate bundle/installer manifests Co-authored-by: gazarenkov --- api/v1alpha3/zz_generated.deepcopy.go | 2 +- api/v1alpha4/zz_generated.deepcopy.go | 2 +- api/v1alpha5/zz_generated.deepcopy.go | 2 +- ...kstage-operator.clusterserviceversion.yaml | 2 +- .../manifests/rhdh.redhat.com_backstages.yaml | 6 + ...kstage-operator.clusterserviceversion.yaml | 14 +- .../rhdh-flavor-ai_v1_configmap.yaml | 533 +++++++++++++++ ...rhdh-flavor-orchestrator_v1_configmap.yaml | 67 ++ .../manifests/rhdh.redhat.com_backstages.yaml | 6 + config/profile/rhdh/kustomization.yaml | 6 +- dist/backstage.io/install.yaml | 6 + dist/rhdh/install.yaml | 622 ++++++++++++++++++ 12 files changed, 1260 insertions(+), 8 deletions(-) create mode 100644 bundle/rhdh/manifests/rhdh-flavor-ai_v1_configmap.yaml create mode 100644 bundle/rhdh/manifests/rhdh-flavor-orchestrator_v1_configmap.yaml diff --git a/api/v1alpha3/zz_generated.deepcopy.go b/api/v1alpha3/zz_generated.deepcopy.go index 6613a8896..b315b789e 100644 --- a/api/v1alpha3/zz_generated.deepcopy.go +++ b/api/v1alpha3/zz_generated.deepcopy.go @@ -5,7 +5,7 @@ package v1alpha3 import ( - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) diff --git a/api/v1alpha4/zz_generated.deepcopy.go b/api/v1alpha4/zz_generated.deepcopy.go index 9c86c46ee..c9c126a0f 100644 --- a/api/v1alpha4/zz_generated.deepcopy.go +++ b/api/v1alpha4/zz_generated.deepcopy.go @@ -5,7 +5,7 @@ package v1alpha4 import ( - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) diff --git a/api/v1alpha5/zz_generated.deepcopy.go b/api/v1alpha5/zz_generated.deepcopy.go index bd2d60544..9e8b769c9 100644 --- a/api/v1alpha5/zz_generated.deepcopy.go +++ b/api/v1alpha5/zz_generated.deepcopy.go @@ -5,7 +5,7 @@ package v1alpha5 import ( - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) diff --git a/bundle/backstage.io/manifests/backstage-operator.clusterserviceversion.yaml b/bundle/backstage.io/manifests/backstage-operator.clusterserviceversion.yaml index ecf85f229..502ac2969 100644 --- a/bundle/backstage.io/manifests/backstage-operator.clusterserviceversion.yaml +++ b/bundle/backstage.io/manifests/backstage-operator.clusterserviceversion.yaml @@ -35,7 +35,7 @@ metadata: } } ] - createdAt: "2026-01-16T15:03:29Z" + createdAt: "2026-01-19T14:28:25Z" description: Backstage Operator operators.operatorframework.io/builder: operator-sdk-v1.37.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 diff --git a/bundle/backstage.io/manifests/rhdh.redhat.com_backstages.yaml b/bundle/backstage.io/manifests/rhdh.redhat.com_backstages.yaml index 975d11ed2..db6be4578 100644 --- a/bundle/backstage.io/manifests/rhdh.redhat.com_backstages.yaml +++ b/bundle/backstage.io/manifests/rhdh.redhat.com_backstages.yaml @@ -1306,6 +1306,12 @@ spec: Optional. x-kubernetes-preserve-unknown-fields: true type: object + flavor: + description: |- + Flavor specifies a pre-configured template for Backstage deployment (e.g., "orchestrator", "ai"). + When specified, operator loads default configuration from the flavor template instead of standard defaults. + Optional. + type: string monitoring: default: enabled: false diff --git a/bundle/rhdh/manifests/backstage-operator.clusterserviceversion.yaml b/bundle/rhdh/manifests/backstage-operator.clusterserviceversion.yaml index 956b9dff4..a5d6ee6dc 100644 --- a/bundle/rhdh/manifests/backstage-operator.clusterserviceversion.yaml +++ b/bundle/rhdh/manifests/backstage-operator.clusterserviceversion.yaml @@ -39,7 +39,7 @@ metadata: categories: Developer Tools certified: "true" containerImage: registry.redhat.io/rhdh/rhdh-rhel9-operator:1.9 - createdAt: "2026-01-16T15:03:30Z" + createdAt: "2026-01-19T14:28:27Z" description: Red Hat Developer Hub is a Red Hat supported version of Backstage. It comes with pre-built plug-ins and configuration settings, supports use of an external database, and can help streamline the process of setting up a self-managed @@ -374,6 +374,10 @@ spec: - ALL readOnlyRootFilesystem: true volumeMounts: + - mountPath: /default-config/flavors/orchestrator + name: flavor-orchestrator + - mountPath: /default-config/flavors/ai + name: flavor-ai - mountPath: /default-config name: default-config - mountPath: /plugin-deps @@ -383,6 +387,14 @@ spec: serviceAccountName: rhdh-controller-manager terminationGracePeriodSeconds: 10 volumes: + - configMap: + name: rhdh-flavor-orchestrator + optional: true + name: flavor-orchestrator + - configMap: + name: rhdh-flavor-ai + optional: true + name: flavor-ai - configMap: name: rhdh-default-config name: default-config diff --git a/bundle/rhdh/manifests/rhdh-flavor-ai_v1_configmap.yaml b/bundle/rhdh/manifests/rhdh-flavor-ai_v1_configmap.yaml new file mode 100644 index 000000000..c5f3fab98 --- /dev/null +++ b/bundle/rhdh/manifests/rhdh-flavor-ai_v1_configmap.yaml @@ -0,0 +1,533 @@ +apiVersion: v1 +data: + app-config.yaml: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: ai-app-config + data: + default.app-config.yaml: | + app: + title: AI Rolling Demo Developer Hub + baseUrl: "${RHDH_BASE_URL}" + analytics: + adoptionInsights: + maxBufferSize: 20 + flushInterval: 5000 + debug: false + licensedUsers: 50 + auth: + environment: production + session: + secret: "${BACKEND_SECRET}" + providers: + oidc: + production: + metadataUrl: "${KEYCLOAK_METADATA_URL}" + clientId: "${KEYCLOAK_CLIENT_ID}" + clientSecret: "${KEYCLOAK_CLIENT_SECRET}" + callbackUrl: "${RHDH_CALLBACK_URL}" + prompt: auto + signIn: + resolvers: + - resolver: preferredUsernameMatchingUserEntityName + development: + metadataUrl: "${KEYCLOAK_METADATA_URL}" + clientId: "${KEYCLOAK_CLIENT_ID}" + clientSecret: "${KEYCLOAK_CLIENT_SECRET}" + callbackUrl: "${RHDH_CALLBACK_URL}" + prompt: auto + signIn: + resolvers: + - resolver: preferredUsernameMatchingUserEntityName + backend: + actions: + pluginSources: + - 'software-catalog-mcp-tool' + - 'techdocs-mcp-tool' + auth: + externalAccess: + - type: static + options: + token: ${ADMIN_TOKEN} + subject: admin-curl-access + - type: static + options: + token: ${MCP_TOKEN} + subject: mcp-clients + keys: + - secret: "${BACKEND_SECRET}" + baseUrl: "${RHDH_BASE_URL}" + database: + connection: + password: ${POSTGRESQL_ADMIN_PASSWORD} + user: postgres + cors: + origin: "${RHDH_BASE_URL}" + # AI Experience config + csp: + upgrade-insecure-requests: false + img-src: + - "'self'" + - "data:" + - https://img.freepik.com + - https://cdn.dribbble.com + - https://upload.wikimedia.org + - https://podman-desktop.io + - https://argo-cd.readthedocs.io + - https://instructlab.ai + - https://quay.io + - https://news.mit.edu + script-src: + - "'self'" + - "'unsafe-eval'" + - https://cdn.jsdelivr.net + reading: + allow: + - host: example.com + - host: '*.mozilla.org' + - host: '*.openshift.com' + - host: '*.openshiftapps.com' + - host: '10.*:9090' + - host: '127.0.0.1:9090' + - host: '127.0.0.1:8888' + - host: '127.0.0.1:7070' + - host: 'localhost:9090' + - host: 'localhost:8888' + - host: 'localhost:7070' + signInPage: oidc + catalog: + rules: + - allow: [User, Group, System, Domain, Component, Resource, Location, Template, API] + locations: + - target: https://github.com/benwilcock/rhdh-techdocs/blob/main/rhdh-catalog-info.yaml + rules: + - allow: [Component, System] + type: url + - target: https://github.com/redhat-ai-dev/ai-lab-template/blob/ai-rolling-demo-1_8/all.yaml + type: url + providers: + modelCatalog: + development: + baseUrl: http://localhost:9090 + github: + providerId: + organization: "ai-rolling-demo" + schedule: + frequency: + minutes: 15 + initialDelay: + seconds: 15 + timeout: + minutes: 15 + githubOrg: + githubUrl: https://github.com + orgs: ["ai-rolling-demo"] + schedule: + frequency: + minutes: 15 + initialDelay: + seconds: 15 + timeout: + minutes: 15 + keycloakOrg: + default: + baseUrl: "${KEYCLOAK_BASE_URL}" + loginRealm: "${KEYCLOAK_REALM}" + realm: "${KEYCLOAK_REALM}" + clientId: "${KEYCLOAK_CLIENT_ID}" + clientSecret: "${KEYCLOAK_CLIENT_SECRET}" + schedule: + frequency: { minutes: 1 } + timeout: { minutes: 1 } + initialDelay: { seconds: 15 } + signIn: + resolvers: + - resolver: emailMatchingUserEntityProfileEmail + lightspeed: + mcpServers: + - name: mcp-integration-tools + token: ${MCP_TOKEN} + integrations: + github: + - apps: + - appId: ${GITHUB_APP_APP_ID} + clientId: ${GITHUB_APP_CLIENT_ID} + clientSecret: ${GITHUB_APP_CLIENT_SECRET} + webhookUrl: ${GITHUB_APP_WEBHOOK_URL} + webhookSecret: ${GITHUB_APP_WEBHOOK_SECRET} + privateKey: | + ${GITHUB_APP_PRIVATE_KEY} + host: github.com + kubernetes: + clusterLocatorMethods: + - clusters: + - authProvider: serviceAccount + name: default + serviceAccountToken: ${K8S_CLUSTER_TOKEN} + skipTLSVerify: true + url: https://kubernetes.default.svc + type: config + customResources: + - apiVersion: v1beta1 + group: tekton.dev + plural: pipelines + - apiVersion: v1beta1 + group: tekton.dev + plural: pipelineruns + - apiVersion: v1beta1 + group: tekton.dev + plural: taskruns + - apiVersion: v1 + group: route.openshift.io + plural: routes + serviceLocatorMethod: + type: multiTenant + argocd: + username: ${ARGOCD_USER} + password: ${ARGOCD_PASSWORD} + waitCycles: 25 + appLocatorMethods: + - type: 'config' + instances: + - name: default + url: https://${ARGOCD_HOSTNAME} + token: ${ARGOCD_API_TOKEN} + proxy: + endpoints: + "/developer-hub": + target: https://raw.githubusercontent.com + pathRewrite: + "^/api/proxy/developer-hub/learning-paths": "/redhat-developer/rhdh-plugins/refs/heads/main/workspaces/ai-integrations/plugins/ai-experience/src/learning-paths/data.json" + changeOrigin: true + secure: false + "/ai-rssfeed": + target: "https://news.mit.edu/topic/mitartificial-intelligence2-rss.xml" + changeOrigin: true + followRedirects: true + dynamic-plugins.yaml: |+ + apiVersion: v1 + kind: ConfigMap + metadata: + name: default-dynamic-plugins + data: + dynamic-plugins.yaml: | + includes: + - "dynamic-plugins.default.yaml" + plugins: + - package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-dynamic-home-page + disabled: true + - package: oci://quay.io/tpetkos/customized-sign-in-page:v0.1.0!red-hat-developer-hub-backstage-plugin-customized-sign-in-page + disabled: false + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-customized-sign-in-page: + signInPage: + importName: CustomizedSignInPage + - package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-global-header + disabled: false + pluginConfig: + dynamicPlugins: + frontend: + default.main-menu-items: + menuItems: + default.create: + title: '' + default.admin: + title: Administration + textKey: menuItem.administration + icon: admin + red-hat-developer-hub.backstage-plugin-global-header: + mountPoints: + - mountPoint: application/header + importName: GlobalHeader + config: + position: above-main-content # above-main-content | below-main-content + + - mountPoint: global.header/component + importName: SearchComponent + config: + priority: 100 + + - mountPoint: global.header/component + importName: Spacer + config: + priority: 99 + props: + growFactor: 0 + + - mountPoint: global.header/component + importName: HeaderIconButton + config: + priority: 90 + props: + title: Create... + icon: add + to: create + + - mountPoint: global.header/component + importName: StarredDropdown + config: + priority: 85 + + - mountPoint: global.header/component + importName: ApplicationLauncherDropdown + config: + priority: 82 + + - mountPoint: global.header/component + importName: SupportButton + config: + priority: 80 + + - mountPoint: global.header/component + importName: NotificationButton + config: + priority: 70 + + - mountPoint: global.header/component + importName: Divider + config: + priority: 50 + + - mountPoint: global.header/component + importName: ProfileDropdown + config: + priority: 10 + + - mountPoint: global.header/profile + importName: MenuItemLink + config: + priority: 100 + props: + title: Settings + link: /settings + icon: manageAccounts + + - mountPoint: global.header/profile + importName: LogoutButton + config: + priority: 10 + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Red Hat AI + sectionLink: https://www.redhat.com/en/products/ai + sectionLinkLabel: Read more + priority: 200 + props: + title: Podman Desktop + icon: https://podman-desktop.io/img/logo.svg + link: https://podman-desktop.io/ + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Red Hat AI + sectionLinkLabel: Read more + priority: 170 + props: + title: OpenShift AI + icon: https://upload.wikimedia.org/wikipedia/commons/d/d8/Red_Hat_logo.svg + link: https://rhods-dashboard-redhat-ods-applications.apps.rosa.redhat-ai-dev.m6no.p3.openshiftapps.com/ + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Red Hat AI + sectionLinkLabel: Read more + priority: 160 + props: + title: RHEL AI + icon: https://upload.wikimedia.org/wikipedia/commons/d/d8/Red_Hat_logo.svg + link: https://www.redhat.com/en/products/ai/enterprise-linux-ai + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Red Hat AI + sectionLinkLabel: Read more + priority: 150 + props: + title: Instructlab + icon: https://instructlab.ai/logo.png + link: https://instructlab.ai/ + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Quick Links + priority: 150 + props: + title: MCP Tools Guide + icon: https://upload.wikimedia.org/wikipedia/commons/0/01/Google_Docs_logo_%282014-2020%29.svg + link: https://docs.google.com/document/d/1o9qRYCAszGzTwW2mRjC4c9wFwJIx8J3XF1kGPFy5F1s/edit?tab=t.0 + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Quick Links + priority: 150 + props: + title: Quay.io + icon: https://quay.io/static/img/quay_favicon.png + link: https://quay.io + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Quick Links + priority: 140 + props: + title: Slack + icon: https://upload.wikimedia.org/wikipedia/commons/d/d5/Slack_icon_2019.svg + link: https://slack.com/ + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Quick Links + priority: 130 + props: + title: ArgoCD + icon: https://argo-cd.readthedocs.io/en/stable/assets/logo.png + link: https://argo-cd.readthedocs.io/en/stable/ + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Quick Links + priority: 120 + props: + title: Openshift + icon: https://upload.wikimedia.org/wikipedia/commons/d/d8/Red_Hat_logo.svg + link: https://www.redhat.com/en/technologies/cloud-computing/openshift + - package: oci://quay.io/karthik_jk/ai-experience:1.6.1!red-hat-developer-hub-backstage-plugin-ai-experience + disabled: false + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-ai-experience: + appIcons: + - name: aiNewsIcon + importName: AiNewsIcon + dynamicRoutes: + - path: / + importName: AiExperiencePage + - path: /ai-news + importName: AiNewsPage + menuItem: + icon: aiNewsIcon + text: AI News + - package: oci://quay.io/karthik_jk/ai-experience:1.6.1!red-hat-developer-hub-backstage-plugin-ai-experience-backend-dynamic + disabled: false + - package: ./dynamic-plugins/dist/backstage-plugin-kubernetes-backend-dynamic + disabled: false + - package: ./dynamic-plugins/dist/backstage-plugin-kubernetes + disabled: false + - package: ./dynamic-plugins/dist/backstage-community-plugin-catalog-backend-module-keycloak-dynamic + disabled: false + - package: ./dynamic-plugins/dist/backstage-community-plugin-redhat-argocd + disabled: false + pluginConfig: + dynamicPlugins: + frontend: + backstage-community.plugin-redhat-argocd: + mountPoints: + - mountPoint: entity.page.overview/cards + importName: ArgocdDeploymentSummary + config: + layout: + gridColumnEnd: + lg: "span 8" + xs: "span 12" + if: + allOf: + - isArgocdConfigured + - mountPoint: entity.page.cd/cards + importName: ArgocdDeploymentLifecycle + config: + layout: + gridColumn: '1 / -1' + if: + allOf: + - isArgocdConfigured + - disabled: false + package: ./dynamic-plugins/dist/roadiehq-backstage-plugin-argo-cd-backend-dynamic + - disabled: false + package: ./dynamic-plugins/dist/roadiehq-scaffolder-backend-argocd-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-techdocs-backend-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-techdocs + - disabled: false + package: ./dynamic-plugins/dist/backstage-community-plugin-topology + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-github-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-github-org-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-scaffolder-backend-module-github-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-scaffolder-backend-module-gitlab-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-gitlab-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-gitlab-org-dynamic + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-lightspeed:bs_1.42.5__1.0.3!red-hat-developer-hub-backstage-plugin-lightspeed + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-lightspeed: + translationResources: + - importName: lightspeedTranslations + module: Alpha + ref: lightspeedTranslationRef + appIcons: + - name: LightspeedIcon + module: LightspeedPlugin + importName: LightspeedIcon + dynamicRoutes: + - path: /lightspeed + importName: LightspeedPage + module: LightspeedPlugin + menuItem: + icon: LightspeedIcon + text: Lightspeed + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-lightspeed-backend:bs_1.42.5__1.0.3!red-hat-developer-hub-backstage-plugin-lightspeed-backend + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-plugin-mcp-actions-backend:next__0.1.2!backstage-plugin-mcp-actions-backend + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-software-catalog-mcp-tool:bs_1.42.5__0.2.3!red-hat-developer-hub-backstage-plugin-software-catalog-mcp-tool + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-techdocs-mcp-tool:bs_1.42.5__0.3.0!red-hat-developer-hub-backstage-plugin-techdocs-mcp-tool + - disabled: true + package: ./dynamic-plugins/dist/backstage-community-plugin-analytics-provider-segment + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-catalog-backend-module-model-catalog:bs_1.42.5__0.7.0!red-hat-developer-hub-backstage-plugin-catalog-backend-module-model-catalog + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-catalog-techdoc-url-reader-backend:bs_1.42.5__0.3.0!red-hat-developer-hub-backstage-plugin-catalog-techdoc-url-reader-backend + - disabled: false + package: ./dynamic-plugins/dist/backstage-community-plugin-tekton + pluginConfig: + dynamicPlugins: + frontend: + backstage-community.plugin-tekton: + mountPoints: + - config: + if: + allOf: + - isTektonCIAvailable + layout: + gridColumn: 1 / -1 + gridRowStart: 1 + importName: TektonCI + mountPoint: entity.page.ci/cards + +kind: ConfigMap +metadata: + name: rhdh-flavor-ai diff --git a/bundle/rhdh/manifests/rhdh-flavor-orchestrator_v1_configmap.yaml b/bundle/rhdh/manifests/rhdh-flavor-orchestrator_v1_configmap.yaml new file mode 100644 index 000000000..8df7da837 --- /dev/null +++ b/bundle/rhdh/manifests/rhdh-flavor-orchestrator_v1_configmap.yaml @@ -0,0 +1,67 @@ +apiVersion: v1 +data: + dynamic-plugins.yaml: |- + apiVersion: v1 + kind: ConfigMap + metadata: + name: default-dynamic-plugins + data: + dynamic-plugins.yaml: | + includes: + - dynamic-plugins.default.yaml + plugins: + - disabled: false + package: "@redhat/backstage-plugin-orchestrator@1.8.2" + integrity: sha512-rnUA6iZ2JVAyASfwS4P9HeFmpqCgH6FQouzzg4s6lCPAsYUFvu6tifJ3df5lThXPUTJ2cDvvQgamU+4DiHP2jw== + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-orchestrator: + appIcons: + - importName: OrchestratorIcon + name: orchestratorIcon + dynamicRoutes: + - importName: OrchestratorPage + menuItem: + icon: orchestratorIcon + text: Orchestrator + path: /orchestrator + entityTabs: + - path: /workflows + title: Workflows + mountPoint: entity.page.workflows + mountPoints: + - mountPoint: entity.page.workflows/cards + importName: OrchestratorCatalogTab + config: + layout: + gridColumn: '1 / -1' + if: + anyOf: + - IsOrchestratorCatalogTabAvailable + - disabled: false + package: "@redhat/backstage-plugin-orchestrator-backend-dynamic@1.8.2" + integrity: sha512-6G0YguzCM5nCDpOrIGJpLTXVMr6EBdIVqSXtsLH9RvBH25RTuFpfJ7q6eEp26DqveaiqUCfBpJ51smdjcsEzFQ== + pluginConfig: + orchestrator: + dataIndexService: + url: http://sonataflow-platform-data-index-service + dependencies: + - ref: sonataflow + - disabled: false + package: "@redhat/backstage-plugin-scaffolder-backend-module-orchestrator-dynamic@1.8.2" + integrity: sha512-N2hCn9RI/QVEoK56FAkGkSDbvfQCOIzVsJTwDX0kf//npO++2crRSJpB1Lr/m2UtYxfaXZX53p8sPcK3g8yWkQ== + pluginConfig: + orchestrator: + dataIndexService: + url: http://sonataflow-platform-data-index-service + - disabled: false + package: "@redhat/backstage-plugin-orchestrator-form-widgets@1.8.2" + integrity: sha512-Pe0dn3g+YTK3jbl36E8nt4zdyH/3w+MWgRyFWPc2B0eV4/L/aRfRC4KxcktmHPdamRGXTIaXL6cFae8TZl8Htw== + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-orchestrator-form-widgets: { } +kind: ConfigMap +metadata: + name: rhdh-flavor-orchestrator diff --git a/bundle/rhdh/manifests/rhdh.redhat.com_backstages.yaml b/bundle/rhdh/manifests/rhdh.redhat.com_backstages.yaml index 975d11ed2..db6be4578 100644 --- a/bundle/rhdh/manifests/rhdh.redhat.com_backstages.yaml +++ b/bundle/rhdh/manifests/rhdh.redhat.com_backstages.yaml @@ -1306,6 +1306,12 @@ spec: Optional. x-kubernetes-preserve-unknown-fields: true type: object + flavor: + description: |- + Flavor specifies a pre-configured template for Backstage deployment (e.g., "orchestrator", "ai"). + When specified, operator loads default configuration from the flavor template instead of standard defaults. + Optional. + type: string monitoring: default: enabled: false diff --git a/config/profile/rhdh/kustomization.yaml b/config/profile/rhdh/kustomization.yaml index 9601834f2..5734348c2 100644 --- a/config/profile/rhdh/kustomization.yaml +++ b/config/profile/rhdh/kustomization.yaml @@ -14,8 +14,8 @@ resources: images: - name: controller - newName: quay.io/rhdh-community/operator - newTag: next + newName: quay.io/rhdh/rhdh-rhel9-operator + newTag: "1.9" patches: - path: patches/deployment-patch.yaml @@ -52,4 +52,4 @@ configMapGenerator: - files: - default-config/flavors/ai/dynamic-plugins.yaml - default-config/flavors/ai/app-config.yaml - name: flavor-ai \ No newline at end of file + name: flavor-ai diff --git a/dist/backstage.io/install.yaml b/dist/backstage.io/install.yaml index 9678165c9..1457d47c4 100644 --- a/dist/backstage.io/install.yaml +++ b/dist/backstage.io/install.yaml @@ -1318,6 +1318,12 @@ spec: Optional. x-kubernetes-preserve-unknown-fields: true type: object + flavor: + description: |- + Flavor specifies a pre-configured template for Backstage deployment (e.g., "orchestrator", "ai"). + When specified, operator loads default configuration from the flavor template instead of standard defaults. + Optional. + type: string monitoring: default: enabled: false diff --git a/dist/rhdh/install.yaml b/dist/rhdh/install.yaml index 2a48e2be7..35cb12834 100644 --- a/dist/rhdh/install.yaml +++ b/dist/rhdh/install.yaml @@ -1318,6 +1318,12 @@ spec: Optional. x-kubernetes-preserve-unknown-fields: true type: object + flavor: + description: |- + Flavor specifies a pre-configured template for Backstage deployment (e.g., "orchestrator", "ai"). + When specified, operator loads default configuration from the flavor template instead of standard defaults. + Optional. + type: string monitoring: default: enabled: false @@ -2278,6 +2284,610 @@ metadata: namespace: rhdh-operator --- apiVersion: v1 +data: + app-config.yaml: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: ai-app-config + data: + default.app-config.yaml: | + app: + title: AI Rolling Demo Developer Hub + baseUrl: "${RHDH_BASE_URL}" + analytics: + adoptionInsights: + maxBufferSize: 20 + flushInterval: 5000 + debug: false + licensedUsers: 50 + auth: + environment: production + session: + secret: "${BACKEND_SECRET}" + providers: + oidc: + production: + metadataUrl: "${KEYCLOAK_METADATA_URL}" + clientId: "${KEYCLOAK_CLIENT_ID}" + clientSecret: "${KEYCLOAK_CLIENT_SECRET}" + callbackUrl: "${RHDH_CALLBACK_URL}" + prompt: auto + signIn: + resolvers: + - resolver: preferredUsernameMatchingUserEntityName + development: + metadataUrl: "${KEYCLOAK_METADATA_URL}" + clientId: "${KEYCLOAK_CLIENT_ID}" + clientSecret: "${KEYCLOAK_CLIENT_SECRET}" + callbackUrl: "${RHDH_CALLBACK_URL}" + prompt: auto + signIn: + resolvers: + - resolver: preferredUsernameMatchingUserEntityName + backend: + actions: + pluginSources: + - 'software-catalog-mcp-tool' + - 'techdocs-mcp-tool' + auth: + externalAccess: + - type: static + options: + token: ${ADMIN_TOKEN} + subject: admin-curl-access + - type: static + options: + token: ${MCP_TOKEN} + subject: mcp-clients + keys: + - secret: "${BACKEND_SECRET}" + baseUrl: "${RHDH_BASE_URL}" + database: + connection: + password: ${POSTGRESQL_ADMIN_PASSWORD} + user: postgres + cors: + origin: "${RHDH_BASE_URL}" + # AI Experience config + csp: + upgrade-insecure-requests: false + img-src: + - "'self'" + - "data:" + - https://img.freepik.com + - https://cdn.dribbble.com + - https://upload.wikimedia.org + - https://podman-desktop.io + - https://argo-cd.readthedocs.io + - https://instructlab.ai + - https://quay.io + - https://news.mit.edu + script-src: + - "'self'" + - "'unsafe-eval'" + - https://cdn.jsdelivr.net + reading: + allow: + - host: example.com + - host: '*.mozilla.org' + - host: '*.openshift.com' + - host: '*.openshiftapps.com' + - host: '10.*:9090' + - host: '127.0.0.1:9090' + - host: '127.0.0.1:8888' + - host: '127.0.0.1:7070' + - host: 'localhost:9090' + - host: 'localhost:8888' + - host: 'localhost:7070' + signInPage: oidc + catalog: + rules: + - allow: [User, Group, System, Domain, Component, Resource, Location, Template, API] + locations: + - target: https://github.com/benwilcock/rhdh-techdocs/blob/main/rhdh-catalog-info.yaml + rules: + - allow: [Component, System] + type: url + - target: https://github.com/redhat-ai-dev/ai-lab-template/blob/ai-rolling-demo-1_8/all.yaml + type: url + providers: + modelCatalog: + development: + baseUrl: http://localhost:9090 + github: + providerId: + organization: "ai-rolling-demo" + schedule: + frequency: + minutes: 15 + initialDelay: + seconds: 15 + timeout: + minutes: 15 + githubOrg: + githubUrl: https://github.com + orgs: ["ai-rolling-demo"] + schedule: + frequency: + minutes: 15 + initialDelay: + seconds: 15 + timeout: + minutes: 15 + keycloakOrg: + default: + baseUrl: "${KEYCLOAK_BASE_URL}" + loginRealm: "${KEYCLOAK_REALM}" + realm: "${KEYCLOAK_REALM}" + clientId: "${KEYCLOAK_CLIENT_ID}" + clientSecret: "${KEYCLOAK_CLIENT_SECRET}" + schedule: + frequency: { minutes: 1 } + timeout: { minutes: 1 } + initialDelay: { seconds: 15 } + signIn: + resolvers: + - resolver: emailMatchingUserEntityProfileEmail + lightspeed: + mcpServers: + - name: mcp-integration-tools + token: ${MCP_TOKEN} + integrations: + github: + - apps: + - appId: ${GITHUB_APP_APP_ID} + clientId: ${GITHUB_APP_CLIENT_ID} + clientSecret: ${GITHUB_APP_CLIENT_SECRET} + webhookUrl: ${GITHUB_APP_WEBHOOK_URL} + webhookSecret: ${GITHUB_APP_WEBHOOK_SECRET} + privateKey: | + ${GITHUB_APP_PRIVATE_KEY} + host: github.com + kubernetes: + clusterLocatorMethods: + - clusters: + - authProvider: serviceAccount + name: default + serviceAccountToken: ${K8S_CLUSTER_TOKEN} + skipTLSVerify: true + url: https://kubernetes.default.svc + type: config + customResources: + - apiVersion: v1beta1 + group: tekton.dev + plural: pipelines + - apiVersion: v1beta1 + group: tekton.dev + plural: pipelineruns + - apiVersion: v1beta1 + group: tekton.dev + plural: taskruns + - apiVersion: v1 + group: route.openshift.io + plural: routes + serviceLocatorMethod: + type: multiTenant + argocd: + username: ${ARGOCD_USER} + password: ${ARGOCD_PASSWORD} + waitCycles: 25 + appLocatorMethods: + - type: 'config' + instances: + - name: default + url: https://${ARGOCD_HOSTNAME} + token: ${ARGOCD_API_TOKEN} + proxy: + endpoints: + "/developer-hub": + target: https://raw.githubusercontent.com + pathRewrite: + "^/api/proxy/developer-hub/learning-paths": "/redhat-developer/rhdh-plugins/refs/heads/main/workspaces/ai-integrations/plugins/ai-experience/src/learning-paths/data.json" + changeOrigin: true + secure: false + "/ai-rssfeed": + target: "https://news.mit.edu/topic/mitartificial-intelligence2-rss.xml" + changeOrigin: true + followRedirects: true + dynamic-plugins.yaml: |+ + apiVersion: v1 + kind: ConfigMap + metadata: + name: default-dynamic-plugins + data: + dynamic-plugins.yaml: | + includes: + - "dynamic-plugins.default.yaml" + plugins: + - package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-dynamic-home-page + disabled: true + - package: oci://quay.io/tpetkos/customized-sign-in-page:v0.1.0!red-hat-developer-hub-backstage-plugin-customized-sign-in-page + disabled: false + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-customized-sign-in-page: + signInPage: + importName: CustomizedSignInPage + - package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-global-header + disabled: false + pluginConfig: + dynamicPlugins: + frontend: + default.main-menu-items: + menuItems: + default.create: + title: '' + default.admin: + title: Administration + textKey: menuItem.administration + icon: admin + red-hat-developer-hub.backstage-plugin-global-header: + mountPoints: + - mountPoint: application/header + importName: GlobalHeader + config: + position: above-main-content # above-main-content | below-main-content + + - mountPoint: global.header/component + importName: SearchComponent + config: + priority: 100 + + - mountPoint: global.header/component + importName: Spacer + config: + priority: 99 + props: + growFactor: 0 + + - mountPoint: global.header/component + importName: HeaderIconButton + config: + priority: 90 + props: + title: Create... + icon: add + to: create + + - mountPoint: global.header/component + importName: StarredDropdown + config: + priority: 85 + + - mountPoint: global.header/component + importName: ApplicationLauncherDropdown + config: + priority: 82 + + - mountPoint: global.header/component + importName: SupportButton + config: + priority: 80 + + - mountPoint: global.header/component + importName: NotificationButton + config: + priority: 70 + + - mountPoint: global.header/component + importName: Divider + config: + priority: 50 + + - mountPoint: global.header/component + importName: ProfileDropdown + config: + priority: 10 + + - mountPoint: global.header/profile + importName: MenuItemLink + config: + priority: 100 + props: + title: Settings + link: /settings + icon: manageAccounts + + - mountPoint: global.header/profile + importName: LogoutButton + config: + priority: 10 + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Red Hat AI + sectionLink: https://www.redhat.com/en/products/ai + sectionLinkLabel: Read more + priority: 200 + props: + title: Podman Desktop + icon: https://podman-desktop.io/img/logo.svg + link: https://podman-desktop.io/ + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Red Hat AI + sectionLinkLabel: Read more + priority: 170 + props: + title: OpenShift AI + icon: https://upload.wikimedia.org/wikipedia/commons/d/d8/Red_Hat_logo.svg + link: https://rhods-dashboard-redhat-ods-applications.apps.rosa.redhat-ai-dev.m6no.p3.openshiftapps.com/ + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Red Hat AI + sectionLinkLabel: Read more + priority: 160 + props: + title: RHEL AI + icon: https://upload.wikimedia.org/wikipedia/commons/d/d8/Red_Hat_logo.svg + link: https://www.redhat.com/en/products/ai/enterprise-linux-ai + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Red Hat AI + sectionLinkLabel: Read more + priority: 150 + props: + title: Instructlab + icon: https://instructlab.ai/logo.png + link: https://instructlab.ai/ + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Quick Links + priority: 150 + props: + title: MCP Tools Guide + icon: https://upload.wikimedia.org/wikipedia/commons/0/01/Google_Docs_logo_%282014-2020%29.svg + link: https://docs.google.com/document/d/1o9qRYCAszGzTwW2mRjC4c9wFwJIx8J3XF1kGPFy5F1s/edit?tab=t.0 + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Quick Links + priority: 150 + props: + title: Quay.io + icon: https://quay.io/static/img/quay_favicon.png + link: https://quay.io + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Quick Links + priority: 140 + props: + title: Slack + icon: https://upload.wikimedia.org/wikipedia/commons/d/d5/Slack_icon_2019.svg + link: https://slack.com/ + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Quick Links + priority: 130 + props: + title: ArgoCD + icon: https://argo-cd.readthedocs.io/en/stable/assets/logo.png + link: https://argo-cd.readthedocs.io/en/stable/ + + - mountPoint: global.header/application-launcher + importName: MenuItemLink + config: + section: Quick Links + priority: 120 + props: + title: Openshift + icon: https://upload.wikimedia.org/wikipedia/commons/d/d8/Red_Hat_logo.svg + link: https://www.redhat.com/en/technologies/cloud-computing/openshift + - package: oci://quay.io/karthik_jk/ai-experience:1.6.1!red-hat-developer-hub-backstage-plugin-ai-experience + disabled: false + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-ai-experience: + appIcons: + - name: aiNewsIcon + importName: AiNewsIcon + dynamicRoutes: + - path: / + importName: AiExperiencePage + - path: /ai-news + importName: AiNewsPage + menuItem: + icon: aiNewsIcon + text: AI News + - package: oci://quay.io/karthik_jk/ai-experience:1.6.1!red-hat-developer-hub-backstage-plugin-ai-experience-backend-dynamic + disabled: false + - package: ./dynamic-plugins/dist/backstage-plugin-kubernetes-backend-dynamic + disabled: false + - package: ./dynamic-plugins/dist/backstage-plugin-kubernetes + disabled: false + - package: ./dynamic-plugins/dist/backstage-community-plugin-catalog-backend-module-keycloak-dynamic + disabled: false + - package: ./dynamic-plugins/dist/backstage-community-plugin-redhat-argocd + disabled: false + pluginConfig: + dynamicPlugins: + frontend: + backstage-community.plugin-redhat-argocd: + mountPoints: + - mountPoint: entity.page.overview/cards + importName: ArgocdDeploymentSummary + config: + layout: + gridColumnEnd: + lg: "span 8" + xs: "span 12" + if: + allOf: + - isArgocdConfigured + - mountPoint: entity.page.cd/cards + importName: ArgocdDeploymentLifecycle + config: + layout: + gridColumn: '1 / -1' + if: + allOf: + - isArgocdConfigured + - disabled: false + package: ./dynamic-plugins/dist/roadiehq-backstage-plugin-argo-cd-backend-dynamic + - disabled: false + package: ./dynamic-plugins/dist/roadiehq-scaffolder-backend-argocd-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-techdocs-backend-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-techdocs + - disabled: false + package: ./dynamic-plugins/dist/backstage-community-plugin-topology + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-github-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-github-org-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-scaffolder-backend-module-github-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-scaffolder-backend-module-gitlab-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-gitlab-dynamic + - disabled: false + package: ./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-gitlab-org-dynamic + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-lightspeed:bs_1.42.5__1.0.3!red-hat-developer-hub-backstage-plugin-lightspeed + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-lightspeed: + translationResources: + - importName: lightspeedTranslations + module: Alpha + ref: lightspeedTranslationRef + appIcons: + - name: LightspeedIcon + module: LightspeedPlugin + importName: LightspeedIcon + dynamicRoutes: + - path: /lightspeed + importName: LightspeedPage + module: LightspeedPlugin + menuItem: + icon: LightspeedIcon + text: Lightspeed + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-lightspeed-backend:bs_1.42.5__1.0.3!red-hat-developer-hub-backstage-plugin-lightspeed-backend + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-plugin-mcp-actions-backend:next__0.1.2!backstage-plugin-mcp-actions-backend + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-software-catalog-mcp-tool:bs_1.42.5__0.2.3!red-hat-developer-hub-backstage-plugin-software-catalog-mcp-tool + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-techdocs-mcp-tool:bs_1.42.5__0.3.0!red-hat-developer-hub-backstage-plugin-techdocs-mcp-tool + - disabled: true + package: ./dynamic-plugins/dist/backstage-community-plugin-analytics-provider-segment + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-catalog-backend-module-model-catalog:bs_1.42.5__0.7.0!red-hat-developer-hub-backstage-plugin-catalog-backend-module-model-catalog + - disabled: false + package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-catalog-techdoc-url-reader-backend:bs_1.42.5__0.3.0!red-hat-developer-hub-backstage-plugin-catalog-techdoc-url-reader-backend + - disabled: false + package: ./dynamic-plugins/dist/backstage-community-plugin-tekton + pluginConfig: + dynamicPlugins: + frontend: + backstage-community.plugin-tekton: + mountPoints: + - config: + if: + allOf: + - isTektonCIAvailable + layout: + gridColumn: 1 / -1 + gridRowStart: 1 + importName: TektonCI + mountPoint: entity.page.ci/cards + +kind: ConfigMap +metadata: + name: rhdh-flavor-ai + namespace: rhdh-operator +--- +apiVersion: v1 +data: + dynamic-plugins.yaml: |- + apiVersion: v1 + kind: ConfigMap + metadata: + name: default-dynamic-plugins + data: + dynamic-plugins.yaml: | + includes: + - dynamic-plugins.default.yaml + plugins: + - disabled: false + package: "@redhat/backstage-plugin-orchestrator@1.8.2" + integrity: sha512-rnUA6iZ2JVAyASfwS4P9HeFmpqCgH6FQouzzg4s6lCPAsYUFvu6tifJ3df5lThXPUTJ2cDvvQgamU+4DiHP2jw== + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-orchestrator: + appIcons: + - importName: OrchestratorIcon + name: orchestratorIcon + dynamicRoutes: + - importName: OrchestratorPage + menuItem: + icon: orchestratorIcon + text: Orchestrator + path: /orchestrator + entityTabs: + - path: /workflows + title: Workflows + mountPoint: entity.page.workflows + mountPoints: + - mountPoint: entity.page.workflows/cards + importName: OrchestratorCatalogTab + config: + layout: + gridColumn: '1 / -1' + if: + anyOf: + - IsOrchestratorCatalogTabAvailable + - disabled: false + package: "@redhat/backstage-plugin-orchestrator-backend-dynamic@1.8.2" + integrity: sha512-6G0YguzCM5nCDpOrIGJpLTXVMr6EBdIVqSXtsLH9RvBH25RTuFpfJ7q6eEp26DqveaiqUCfBpJ51smdjcsEzFQ== + pluginConfig: + orchestrator: + dataIndexService: + url: http://sonataflow-platform-data-index-service + dependencies: + - ref: sonataflow + - disabled: false + package: "@redhat/backstage-plugin-scaffolder-backend-module-orchestrator-dynamic@1.8.2" + integrity: sha512-N2hCn9RI/QVEoK56FAkGkSDbvfQCOIzVsJTwDX0kf//npO++2crRSJpB1Lr/m2UtYxfaXZX53p8sPcK3g8yWkQ== + pluginConfig: + orchestrator: + dataIndexService: + url: http://sonataflow-platform-data-index-service + - disabled: false + package: "@redhat/backstage-plugin-orchestrator-form-widgets@1.8.2" + integrity: sha512-Pe0dn3g+YTK3jbl36E8nt4zdyH/3w+MWgRyFWPc2B0eV4/L/aRfRC4KxcktmHPdamRGXTIaXL6cFae8TZl8Htw== + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-orchestrator-form-widgets: { } +kind: ConfigMap +metadata: + name: rhdh-flavor-orchestrator + namespace: rhdh-operator +--- +apiVersion: v1 data: argocd.yaml: |- --- @@ -2962,6 +3572,10 @@ spec: - ALL readOnlyRootFilesystem: true volumeMounts: + - mountPath: /default-config/flavors/orchestrator + name: flavor-orchestrator + - mountPath: /default-config/flavors/ai + name: flavor-ai - mountPath: /default-config name: default-config - mountPath: /plugin-deps @@ -2971,6 +3585,14 @@ spec: serviceAccountName: rhdh-controller-manager terminationGracePeriodSeconds: 10 volumes: + - configMap: + name: rhdh-flavor-orchestrator + optional: true + name: flavor-orchestrator + - configMap: + name: rhdh-flavor-ai + optional: true + name: flavor-ai - configMap: name: rhdh-default-config name: default-config