├── .dockerignore ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── README.workflows.adoc ├── renovate.json └── workflows │ ├── next-container-build.yaml │ ├── nightly-upgrade-test.yaml │ ├── nightly.yaml │ ├── pr-bundle-diff-checks.yaml │ ├── pr-container-build.yaml │ ├── pr.yaml │ ├── renovate-checks.yaml │ └── scripts-checks.yaml ├── .gitignore ├── .rhdh ├── bundle │ └── licenses │ │ └── LICENSE ├── docker │ ├── Dockerfile │ └── bundle.Dockerfile ├── docs │ ├── airgap.adoc │ ├── images │ │ ├── airgap │ │ │ ├── rhdh_catalog_operatorhub.png │ │ │ └── rhdh_operator_install_ok.png │ │ ├── app-config_in_cr.png │ │ ├── checking_list_of_plugins.png │ │ ├── dynamic_plugins_in_cr.png │ │ ├── edit_backstage_cr_instance.png │ │ ├── rhdh_from_operator.png │ │ └── rhdh_without_local_db.png │ ├── installing-ci-builds.adoc │ └── openshift.adoc └── scripts │ ├── install-rhdh-catalog-source.sh │ └── prepare-restricted-environment.sh ├── Dockerfile ├── LICENSE ├── Makefile ├── OWNERS ├── PROJECT ├── README.md ├── api ├── v1alpha1 │ ├── backstage_types.go │ ├── groupversion_info.go │ └── zz_generated.deepcopy.go ├── v1alpha2 │ ├── backstage_types.go │ ├── groupversion_info.go │ └── zz_generated.deepcopy.go └── v1alpha3 │ ├── backstage_types.go │ ├── groupversion_info.go │ └── zz_generated.deepcopy.go ├── bundle ├── backstage.io │ ├── bundle.Dockerfile │ ├── manifests │ │ ├── backstage-backstage-editor-role_rbac.authorization.k8s.io_v1_clusterrole.yaml │ │ ├── backstage-backstage-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml │ │ ├── backstage-controller-manager-metrics-service_v1_service.yaml │ │ ├── backstage-default-config_v1_configmap.yaml │ │ ├── backstage-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml │ │ ├── backstage-operator.clusterserviceversion.yaml │ │ └── rhdh.redhat.com_backstages.yaml │ ├── metadata │ │ └── annotations.yaml │ └── tests │ │ └── scorecard │ │ └── config.yaml └── rhdh │ ├── bundle.Dockerfile │ ├── manifests │ ├── backstage-operator.clusterserviceversion.yaml │ ├── rhdh-backstage-editor-role_rbac.authorization.k8s.io_v1_clusterrole.yaml │ ├── rhdh-backstage-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml │ ├── rhdh-controller-manager-metrics-service_v1_service.yaml │ ├── rhdh-default-config_v1_configmap.yaml │ ├── rhdh-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml │ ├── rhdh-plugin-deps_v1_configmap.yaml │ └── rhdh.redhat.com_backstages.yaml │ ├── metadata │ └── annotations.yaml │ └── tests │ └── scorecard │ └── config.yaml ├── cmd └── main.go ├── config ├── crd │ ├── bases │ │ └── rhdh.redhat.com_backstages.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_backstages.yaml │ │ └── webhook_in_backstages.yaml ├── manager │ ├── deployment.yaml │ └── kustomization.yaml ├── manifests │ ├── backstage.io │ │ ├── bases │ │ │ └── backstage-operator.clusterserviceversion.yaml │ │ └── kustomization.yaml │ └── rhdh │ │ ├── bases │ │ └── backstage-operator.clusterserviceversion.yaml │ │ └── kustomization.yaml ├── profile │ ├── backstage.io │ │ ├── default-config │ │ │ ├── app-config.yaml │ │ │ ├── db-secret.yaml │ │ │ ├── db-service.yaml │ │ │ ├── db-statefulset.yaml │ │ │ ├── deployment.yaml │ │ │ ├── route.yaml │ │ │ └── service.yaml │ │ ├── kustomization.yaml │ │ └── namespace.yaml │ ├── external │ │ └── kustomization.yaml │ └── rhdh │ │ ├── default-config │ │ ├── app-config.yaml │ │ ├── db-secret.yaml │ │ ├── db-service.yaml │ │ ├── db-statefulset.yaml │ │ ├── db-statefulset.yaml.k8s │ │ ├── deployment.yaml │ │ ├── deployment.yaml.k8s │ │ ├── dynamic-plugins.yaml │ │ ├── route.yaml │ │ ├── service.yaml │ │ └── service.yaml.k8s │ │ ├── kustomization.yaml │ │ ├── namespace.yaml │ │ ├── patches │ │ └── deployment-patch.yaml │ │ ├── plugin-deps │ │ └── sonataflow.yaml │ │ ├── plugin-infra │ │ ├── kustomization.yaml │ │ └── orchestrator │ │ │ ├── infra-serverless.yaml │ │ │ └── infra-sonataflow.yaml │ │ └── plugin-rbac │ │ ├── kustomization.yaml │ │ └── rbac-sonataflow.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── backstage_editor_role.yaml │ ├── backstage_viewer_role.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── metrics_auth_role.yaml │ ├── metrics_auth_role_binding.yaml │ ├── metrics_reader.yaml │ ├── metrics_service.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml ├── samples │ ├── _v1alpha1_backstage.yaml │ ├── _v1alpha2_backstage.yaml │ ├── _v1alpha3_backstage.yaml │ ├── catalog-operator-group.yaml │ ├── catalog-source-template.yaml │ ├── catalog-subscription-template.yaml │ └── kustomization.yaml └── scorecard │ ├── bases │ └── config.yaml │ ├── kustomization.yaml │ └── patches │ ├── basic.config.yaml │ └── olm.config.yaml ├── dist ├── backstage.io │ └── install.yaml └── rhdh │ └── install.yaml ├── docs ├── admin.md ├── configuration.md ├── db_migration.md ├── design.md ├── developer.md ├── dynamic-plugins.md ├── external-db.md ├── images │ ├── backstage_admin_configmap_and_cr.jpg │ ├── backstage_application_advanced_config.jpg │ ├── backstage_kubernetes_runtime.jpg │ └── backstage_operator_configuration_layers.jpg └── profiles.md ├── examples ├── bs-existing-secret.yaml ├── bs-route-disabled.yaml ├── bs-route.yaml ├── bs1.yaml ├── filemounts.yaml ├── orchestrator.yaml ├── raw-runtime-config.yaml ├── rhdh-cr-with-app-configs.yaml └── rhdh-cr.yaml ├── go.mod ├── go.sum ├── hack └── db_copy.sh ├── integration_tests ├── README.md ├── config-refresh_test.go ├── cr-config_test.go ├── db_test.go ├── default-config_test.go ├── matchers.go ├── pvcs_test.go ├── rhdh-config_test.go ├── route_test.go ├── suite_test.go ├── testdata │ ├── raw-deployment.yaml │ ├── raw-pvcs.yaml │ ├── raw-pvcs2.yaml │ ├── raw-statefulset.yaml │ ├── rhdh-replace-dynaplugin-root.yaml │ └── spec-deployment.yaml └── utils.go ├── internal └── controller │ ├── backstage_controller.go │ ├── mock_client.go │ ├── platform_detector.go │ ├── plugin-deps.go │ ├── preprocessor_test.go │ ├── spec_preprocessor.go │ └── watchers.go ├── pkg ├── model │ ├── appconfig.go │ ├── appconfig_test.go │ ├── configmapenvs.go │ ├── configmapenvs_test.go │ ├── configmapfiles.go │ ├── configmapfiles_test.go │ ├── db-secret.go │ ├── db-secret_test.go │ ├── db-service.go │ ├── db-statefulset.go │ ├── db-statefulset_test.go │ ├── deployment.go │ ├── deployment_test.go │ ├── dynamic-plugins.go │ ├── dynamic-plugins_test.go │ ├── externalconfig.go │ ├── interfaces.go │ ├── model_tests.go │ ├── multiobject │ │ └── multiobject.go │ ├── plugin_deps.go │ ├── plugin_deps_test.go │ ├── pvcs.go │ ├── pvcs_test.go │ ├── route.go │ ├── route_test.go │ ├── runtime.go │ ├── runtime_test.go │ ├── secretenvs.go │ ├── secretenvs_test.go │ ├── secretfiles.go │ ├── secretfiles_test.go │ ├── service.go │ └── testdata │ │ ├── db-defined-secret.yaml │ │ ├── db-empty-secret.yaml │ │ ├── db-generated-secret.yaml │ │ ├── default-config │ │ ├── db-secret.yaml │ │ ├── db-service.yaml │ │ ├── db-statefulset.yaml │ │ ├── deployment.yaml │ │ └── service.yaml │ │ ├── dynamic-plugins-deps.yaml │ │ ├── invalid-service-type.yaml │ │ ├── ips-db-statefulset.yaml │ │ ├── ips-deployment.yaml │ │ ├── janus-db-statefulset.yaml │ │ ├── janus-deployment.yaml │ │ ├── multi-pvc-containers.yaml │ │ ├── multi-pvc.yaml │ │ ├── multi-service-err.yaml │ │ ├── multicontainer-deployment.yaml │ │ ├── raw-app-config.yaml │ │ ├── raw-cm-envs.yaml │ │ ├── raw-cm-files.yaml │ │ ├── raw-dynamic-plugins.yaml │ │ ├── raw-multi-secret.yaml │ │ ├── raw-route.yaml │ │ ├── raw-sec-envs.yaml │ │ ├── raw-secret-files.yaml │ │ ├── sidecar-deployment.yaml │ │ ├── single-pvc.yaml │ │ └── working-dir-mount.yaml ├── platform │ └── platform.go └── utils │ ├── pod-mutator.go │ ├── testdata │ ├── deployment.yaml │ └── deployment.yaml.k8s │ ├── utils.go │ ├── utils_test.go │ ├── yaml.go │ └── yaml_test.go └── tests ├── e2e ├── README.md ├── e2e_suite_test.go ├── e2e_test.go ├── e2e_upgrade_test.go └── testdata │ └── rhdh-operator-1.5.yaml └── helper ├── helper_backstage.go └── utils.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ## Description 6 | 9 | 10 | ## Which issue(s) does this PR fix or relate to 11 | 12 | - Fixes #_issue_number_ 13 | 14 | ## PR acceptance criteria 15 | 16 | - [ ] Tests 17 | - [ ] Documentation 18 | 19 | ## How to test changes / Special notes to the reviewer 20 | 23 | -------------------------------------------------------------------------------- /.github/README.workflows.adoc: -------------------------------------------------------------------------------- 1 | NOTE: If you rename any of the tests in this folder, or want to add new required ones, please remember to submit a PR against the link:https://github.com/openshift/release[openshift/releases] repo with the updated list of `required_status_checks` for pull requests. 2 | 3 | For example: 4 | 5 | * link:https://github.com/openshift/release/tree/master/core-services/prow/02_config/janus-idp/[core-services/prow/02_config/janus-idp/] 6 | * link:https://github.com/openshift/release/tree/master/core-services/prow/02_config/redhat-developer[core-services/prow/02_config/redhat-developer] 7 | 8 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:best-practices", 4 | ":gitSignOff", 5 | ":rebaseStalePrs", 6 | "docker:disableMajor", 7 | "default:pinDigestsDisabled", 8 | "helpers:pinGitHubActionDigests" 9 | ], 10 | "labels": [ 11 | "kind/dependency upgrade" 12 | ], 13 | "baseBranches": [ 14 | "main", 15 | "/^release-1\\..*/" 16 | ], 17 | "constraints": { 18 | "go": "1.23" 19 | }, 20 | "kustomize": { 21 | "enabled": false 22 | }, 23 | "postUpdateOptions": [ 24 | "gomodTidy" 25 | ], 26 | "packageRules": [ 27 | { 28 | "matchBaseBranches": [ 29 | "release-1.4" 30 | ], 31 | "enabled": false 32 | }, 33 | { 34 | "matchBaseBranches": [ 35 | "release-1.3" 36 | ], 37 | "enabled": false 38 | }, 39 | { 40 | "enabled": false, 41 | "groupName": "all minor dependencies", 42 | "groupSlug": "all-minor", 43 | "matchPackageNames": [ 44 | "*" 45 | ], 46 | "matchUpdateTypes": [ 47 | "minor" 48 | ] 49 | }, 50 | { 51 | "groupName": "all patch dependencies", 52 | "groupSlug": "all-patch", 53 | "matchPackageNames": [ 54 | "*" 55 | ], 56 | "matchUpdateTypes": [ 57 | "patch" 58 | ] 59 | }, 60 | { 61 | "description": "Do NOT generate PRs to pin or apply digests to dockerfiles", 62 | "enabled": false, 63 | "matchDatasources": [ 64 | "docker" 65 | ], 66 | "matchUpdateTypes": [ 67 | "pin", 68 | "pinDigest", 69 | "digest" 70 | ], 71 | "automerge": false 72 | }, 73 | { 74 | "description": "Do NOT generate PRs for minor dockerfile updates in 1.y ", 75 | "enabled": false, 76 | "matchDatasources": [ 77 | "docker" 78 | ], 79 | "matchUpdateTypes": [ 80 | "minor" 81 | ], 82 | "matchBaseBranches": [ 83 | "/^release-1\\..*/" 84 | ], 85 | "automerge": false 86 | }, 87 | { 88 | "description": "Do automerge patch updates to dockerfiles", 89 | "enabled": true, 90 | "matchDatasources": [ 91 | "docker" 92 | ], 93 | "matchUpdateTypes": [ 94 | "patch" 95 | ], 96 | "additionalBranchPrefix": "dockerfile ", 97 | "groupName": "All dockerfile images", 98 | "automerge": true, 99 | "pinDigests": false 100 | }, 101 | { 102 | "description": "Do NOT generate PRs for major go dependency updates ", 103 | "enabled": false, 104 | "matchDatasources": [ 105 | "go" 106 | ], 107 | "matchUpdateTypes": [ 108 | "major" 109 | ], 110 | "automerge": false 111 | }, 112 | { 113 | "description": "Do automerge go dependency patch updates, except for versions starting with 0", 114 | "enabled": true, 115 | "matchDatasources": [ 116 | "go" 117 | ], 118 | "matchUpdateTypes": [ 119 | "patch" 120 | ], 121 | "matchCurrentVersion": "!/^0/", 122 | "automerge": true 123 | }, 124 | { 125 | "description": "Do generate PRs for golang version patch bumps, keeping x.yy version the same", 126 | "enabled": true, 127 | "matchDatasources": [ 128 | "golang-version" 129 | ], 130 | "matchUpdateTypes": [ 131 | "patch" 132 | ], 133 | "automerge": false 134 | }, 135 | { 136 | "description": "Do automerge and pin actions in GH workflows, except for versions starting with 0", 137 | "enabled": true, 138 | "matchDatasources": [ 139 | "github-runners" 140 | ], 141 | "matchUpdateTypes": [ 142 | "minor", 143 | "patch" 144 | ], 145 | "matchCurrentVersion": "!/^0/", 146 | "automerge": true 147 | } 148 | ], 149 | "vulnerabilityAlerts": { 150 | "enabled": true, 151 | "addLabels": [ 152 | "kind/security" 153 | ] 154 | }, 155 | "osvVulnerabilityAlerts": true 156 | } 157 | -------------------------------------------------------------------------------- /.github/workflows/renovate-checks.yaml: -------------------------------------------------------------------------------- 1 | name: PR Renovate Config Validator 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '.github/renovate.json' 7 | # Renovate always uses the config from the repository default branch 8 | # https://docs.renovatebot.com/configuration-options/ 9 | branches: [ 'main' ] 10 | 11 | jobs: 12 | renovate-config-validator: 13 | runs-on: ubuntu-latest 14 | name: Renovate Config Validator 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 17 | - name: Validate config 18 | # See https://docs.renovatebot.com/config-validation/ 19 | run: | 20 | npx --yes --package renovate -- renovate-config-validator --strict .github/renovate.json 21 | -------------------------------------------------------------------------------- /.github/workflows/scripts-checks.yaml: -------------------------------------------------------------------------------- 1 | name: Differential ShellCheck 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**.sh' 7 | - '.github/workflows/scripts-checks.yaml' 8 | branches: [ main ] 9 | pull_request: 10 | paths: 11 | - '**.sh' 12 | - '.github/workflows/scripts-checks.yaml' 13 | branches: [ 'main' ] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | shellcheck-lint: 20 | runs-on: ubuntu-latest 21 | 22 | permissions: 23 | security-events: write 24 | 25 | steps: 26 | - name: Repository checkout 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 28 | with: 29 | # Differential ShellCheck requires full git history 30 | fetch-depth: 0 31 | 32 | - id: ShellCheck 33 | name: Differential ShellCheck 34 | uses: redhat-plumbers-in-action/differential-shellcheck@929381c602ed76daa9b523e001ee29b82bd6d8e9 # v5 35 | with: 36 | token: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - if: always() 39 | name: Upload artifact with ShellCheck defects in SARIF format 40 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 41 | with: 42 | name: Differential ShellCheck SARIF 43 | path: ${{ steps.ShellCheck.outputs.sarif }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | bin/* 8 | bundle_tmp* 9 | testbin/* 10 | Dockerfile.cross 11 | __debug_bin* 12 | 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Go workspace file 20 | go.work 21 | 22 | # Kubernetes Generated files - skip generated files, except for vendored files 23 | !vendor/**/zz_generated.* 24 | 25 | # editor and IDE paraphernalia 26 | .idea 27 | .vscode 28 | *.swp 29 | *.swo 30 | *~ 31 | 32 | .scripts/ 33 | .DS_Store 34 | database/ 35 | index.Dockerfile 36 | gosec.sarif 37 | 38 | # from prepare-restricted-environment script 39 | catalog_mirror.log 40 | manifests-rhdh-index-* 41 | rhdh-operator-*.yaml 42 | # manifests used for the upgrade path E2E tests 43 | !tests/e2e/testdata/rhdh-operator-*.yaml 44 | rhdh-disconnected-install/ 45 | rhdh-disconnected-install.Dockerfile 46 | 47 | .oc-mirror.log -------------------------------------------------------------------------------- /.rhdh/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # THIS IS USED BY Konflux builds >= 1.4 with Cachi2 enabled 2 | 3 | #@follow_tag(registry.redhat.io/rhel9/go-toolset:latest) 4 | # https://registry.access.redhat.com/ubi9/go-toolset 5 | FROM registry.access.redhat.com/ubi9/go-toolset:9.6-1749636489@sha256:2a88121395084eaa575e5758b903fffb43dbf9d9586b2878e51678f63235b587 AS builder 6 | ARG TARGETOS 7 | ARG TARGETARCH 8 | # hadolint ignore=DL3002 9 | USER 0 10 | ENV GOPATH=/go/ 11 | 12 | # '(micro)dnf update -y' not allowed in Konflux+Cachi2: instead use renovate or https://github.com/konflux-ci/rpm-lockfile-prototype to update the rpms.lock.yaml file 13 | # Downstream comment 14 | # RUN dnf -q -y update 15 | #/ Downstream comment 16 | 17 | ENV EXTERNAL_SOURCE=. 18 | ENV CONTAINER_SOURCE=/opt/app-root/src 19 | WORKDIR $CONTAINER_SOURCE 20 | 21 | # cache deps before building and copying source so that we don't need to re-download as much 22 | # and so that source changes don't invalidate our downloaded layer 23 | COPY $EXTERNAL_SOURCE/go.mod $CONTAINER_SOURCE/go.mod 24 | COPY $EXTERNAL_SOURCE/go.sum $CONTAINER_SOURCE/go.sum 25 | RUN go mod download 26 | 27 | COPY $EXTERNAL_SOURCE $CONTAINER_SOURCE 28 | 29 | # Build 30 | # hadolint ignore=SC3010 31 | # Build 32 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 33 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 34 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 35 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 36 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go 37 | 38 | # Install openssl for FIPS support 39 | #@follow_tag(registry.redhat.io/ubi9/ubi-minimal:latest) 40 | # https://registry.access.redhat.com/ubi9/ubi-minimal 41 | FROM registry.access.redhat.com/ubi9-minimal:9.6-1749489516@sha256:f172b3082a3d1bbe789a1057f03883c1113243564f01cd3020e27548b911d3f8 AS runtime 42 | 43 | # Downstream uncomment 44 | RUN cat /cachi2/cachi2.env 45 | #/ Downstream uncomment 46 | 47 | # '(micro)dnf update -y' not allowed in Konflux+Cachi2: instead use renovate or https://github.com/konflux-ci/rpm-lockfile-prototype to update the rpms.lock.yaml file 48 | # Downstream comment 49 | # RUN microdnf update --setopt=install_weak_deps=0 -y 50 | #/ Downstream comment 51 | 52 | RUN microdnf install -y openssl; microdnf clean -y all 53 | 54 | ENV EXTERNAL_SOURCE=. 55 | ENV CONTAINER_SOURCE=/opt/app-root/src 56 | 57 | # RHIDP-4220 - make Konflux preflight and EC checks happy - [check-container] Create a directory named /licenses and include all relevant licensing 58 | COPY $EXTERNAL_SOURCE/LICENSE /licenses/ 59 | 60 | ENV HOME=/ \ 61 | USER_NAME=backstage \ 62 | USER_UID=1001 63 | 64 | RUN echo "${USER_NAME}:x:${USER_UID}:0:${USER_NAME} user:${HOME}:/sbin/nologin" >> /etc/passwd 65 | 66 | # Copy manager binary 67 | COPY --from=builder $CONTAINER_SOURCE/manager . 68 | 69 | USER ${USER_UID} 70 | 71 | WORKDIR ${HOME} 72 | 73 | ENTRYPOINT ["/manager"] 74 | -------------------------------------------------------------------------------- /.rhdh/docker/bundle.Dockerfile: -------------------------------------------------------------------------------- 1 | # THIS IS USED BY Konflux builds >= 1.4 2 | 3 | # RHIDP-4220 - make Konflux preflight and EC checks happy - need some layer with RPMs even if not doing any pre-processing work 4 | FROM registry.access.redhat.com/ubi9/ubi-minimal:latest as builder-runner 5 | USER 1001 6 | 7 | FROM scratch 8 | 9 | # Copy files to locations specified by labels. 10 | COPY manifests /manifests/ 11 | COPY metadata /metadata/ 12 | 13 | # RHIDP-4220 - make Konflux preflight and EC checks happy - [check-container] Create a directory named /licenses and include all relevant licensing 14 | COPY licenses /licenses/ 15 | 16 | # append Brew metadata here 17 | -------------------------------------------------------------------------------- /.rhdh/docs/images/airgap/rhdh_catalog_operatorhub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer/rhdh-operator/9bf2e1de70d39f91103ba724e5da3058eb0abcdb/.rhdh/docs/images/airgap/rhdh_catalog_operatorhub.png -------------------------------------------------------------------------------- /.rhdh/docs/images/airgap/rhdh_operator_install_ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer/rhdh-operator/9bf2e1de70d39f91103ba724e5da3058eb0abcdb/.rhdh/docs/images/airgap/rhdh_operator_install_ok.png -------------------------------------------------------------------------------- /.rhdh/docs/images/app-config_in_cr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer/rhdh-operator/9bf2e1de70d39f91103ba724e5da3058eb0abcdb/.rhdh/docs/images/app-config_in_cr.png -------------------------------------------------------------------------------- /.rhdh/docs/images/checking_list_of_plugins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer/rhdh-operator/9bf2e1de70d39f91103ba724e5da3058eb0abcdb/.rhdh/docs/images/checking_list_of_plugins.png -------------------------------------------------------------------------------- /.rhdh/docs/images/dynamic_plugins_in_cr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer/rhdh-operator/9bf2e1de70d39f91103ba724e5da3058eb0abcdb/.rhdh/docs/images/dynamic_plugins_in_cr.png -------------------------------------------------------------------------------- /.rhdh/docs/images/edit_backstage_cr_instance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer/rhdh-operator/9bf2e1de70d39f91103ba724e5da3058eb0abcdb/.rhdh/docs/images/edit_backstage_cr_instance.png -------------------------------------------------------------------------------- /.rhdh/docs/images/rhdh_from_operator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer/rhdh-operator/9bf2e1de70d39f91103ba724e5da3058eb0abcdb/.rhdh/docs/images/rhdh_from_operator.png -------------------------------------------------------------------------------- /.rhdh/docs/images/rhdh_without_local_db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer/rhdh-operator/9bf2e1de70d39f91103ba724e5da3058eb0abcdb/.rhdh/docs/images/rhdh_without_local_db.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # THIS IS USED BY Konflux builds >= 1.4 with Cachi2 enabled 2 | 3 | #@follow_tag(registry.redhat.io/rhel9/go-toolset:latest) 4 | # https://registry.access.redhat.com/ubi9/go-toolset 5 | FROM registry.access.redhat.com/ubi9/go-toolset:9.6-1749636489@sha256:2a88121395084eaa575e5758b903fffb43dbf9d9586b2878e51678f63235b587 AS builder 6 | ARG TARGETOS 7 | ARG TARGETARCH 8 | # hadolint ignore=DL3002 9 | USER 0 10 | ENV GOPATH=/go/ 11 | 12 | # '(micro)dnf update -y' not allowed in Konflux+Cachi2: instead use renovate or https://github.com/konflux-ci/rpm-lockfile-prototype to update the rpms.lock.yaml file 13 | # Downstream comment 14 | RUN dnf -q -y update 15 | #/ Downstream comment 16 | 17 | ENV EXTERNAL_SOURCE=. 18 | ENV CONTAINER_SOURCE=/opt/app-root/src 19 | WORKDIR $CONTAINER_SOURCE 20 | 21 | # cache deps before building and copying source so that we don't need to re-download as much 22 | # and so that source changes don't invalidate our downloaded layer 23 | COPY $EXTERNAL_SOURCE/go.mod $CONTAINER_SOURCE/go.mod 24 | COPY $EXTERNAL_SOURCE/go.sum $CONTAINER_SOURCE/go.sum 25 | RUN go mod download 26 | 27 | COPY $EXTERNAL_SOURCE $CONTAINER_SOURCE 28 | 29 | # Build 30 | # hadolint ignore=SC3010 31 | # Build 32 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 33 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 34 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 35 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 36 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go 37 | 38 | # Install openssl for FIPS support 39 | #@follow_tag(registry.redhat.io/ubi9/ubi-minimal:latest) 40 | # https://registry.access.redhat.com/ubi9/ubi-minimal 41 | FROM registry.access.redhat.com/ubi9-minimal:9.6-1749489516@sha256:f172b3082a3d1bbe789a1057f03883c1113243564f01cd3020e27548b911d3f8 AS runtime 42 | 43 | # Downstream uncomment 44 | # RUN cat /cachi2/cachi2.env 45 | #/ Downstream uncomment 46 | 47 | # '(micro)dnf update -y' not allowed in Konflux+Cachi2: instead use renovate or https://github.com/konflux-ci/rpm-lockfile-prototype to update the rpms.lock.yaml file 48 | # Downstream comment 49 | RUN microdnf update --setopt=install_weak_deps=0 -y 50 | #/ Downstream comment 51 | 52 | RUN microdnf install -y openssl; microdnf clean -y all 53 | 54 | ENV EXTERNAL_SOURCE=. 55 | ENV CONTAINER_SOURCE=/opt/app-root/src 56 | 57 | # RHIDP-4220 - make Konflux preflight and EC checks happy - [check-container] Create a directory named /licenses and include all relevant licensing 58 | COPY $EXTERNAL_SOURCE/LICENSE /licenses/ 59 | 60 | ENV HOME=/ \ 61 | USER_NAME=backstage \ 62 | USER_UID=1001 63 | 64 | RUN echo "${USER_NAME}:x:${USER_UID}:0:${USER_NAME} user:${HOME}:/sbin/nologin" >> /etc/passwd 65 | 66 | # Copy manager binary 67 | COPY --from=builder $CONTAINER_SOURCE/manager . 68 | 69 | USER ${USER_UID} 70 | 71 | WORKDIR ${HOME} 72 | 73 | ENTRYPOINT ["/manager"] 74 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # if adding/removing approvers, remember to update the pr*.yaml files to add/remove them from the authorize job 2 | approvers: 3 | - gazarenkov 4 | - rm3l 5 | - nickboldt 6 | - kim-tsao 7 | - kadel 8 | 9 | reviewers: 10 | - gazarenkov 11 | - rm3l 12 | - Fortune-Ndlovu 13 | - subhashkhileri 14 | - zdrapela 15 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: rhdh.redhat.com 6 | layout: 7 | - go.kubebuilder.io/v4 8 | plugins: 9 | manifests.sdk.operatorframework.io/v2: {} 10 | scorecard.sdk.operatorframework.io/v2: {} 11 | projectName: backstage-operator 12 | repo: github.com/redhat-developer/rhdh-operator 13 | resources: 14 | - api: 15 | crdVersion: v1 16 | namespaced: true 17 | controller: true 18 | domain: rhdh.redhat.com 19 | kind: Backstage 20 | path: github.com/redhat-developer/rhdh-operator/api/v1alpha3 21 | version: v1alpha3 22 | version: "3" 23 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // Package v1alpha1 contains API Schema definitions for the v1alpha1 API group 2 | // +kubebuilder:object:generate=true 3 | // +groupName=rhdh.redhat.com 4 | package v1alpha1 5 | 6 | import ( 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "sigs.k8s.io/controller-runtime/pkg/scheme" 9 | ) 10 | 11 | var ( 12 | // GroupVersion is group version used to register these objects 13 | GroupVersion = schema.GroupVersion{Group: "rhdh.redhat.com", Version: "v1alpha1"} 14 | 15 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 16 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 17 | 18 | // AddToScheme adds the types in this group-version to the given scheme. 19 | AddToScheme = SchemeBuilder.AddToScheme 20 | ) 21 | -------------------------------------------------------------------------------- /api/v1alpha2/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // Package v1alpha2 contains API Schema definitions for the v1alpha2 API group 2 | // +kubebuilder:object:generate=true 3 | // +groupName=rhdh.redhat.com 4 | package v1alpha2 5 | 6 | import ( 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "sigs.k8s.io/controller-runtime/pkg/scheme" 9 | ) 10 | 11 | var ( 12 | // GroupVersion is group version used to register these objects 13 | GroupVersion = schema.GroupVersion{Group: "rhdh.redhat.com", Version: "v1alpha2"} 14 | 15 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 16 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 17 | 18 | // AddToScheme adds the types in this group-version to the given scheme. 19 | AddToScheme = SchemeBuilder.AddToScheme 20 | ) 21 | -------------------------------------------------------------------------------- /api/v1alpha3/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // Package v1alpha3 contains API Schema definitions for the v1alpha3 API group 2 | // +kubebuilder:object:generate=true 3 | // +groupName=rhdh.redhat.com 4 | package v1alpha3 5 | 6 | import ( 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "sigs.k8s.io/controller-runtime/pkg/scheme" 9 | ) 10 | 11 | var ( 12 | // GroupVersion is group version used to register these objects 13 | GroupVersion = schema.GroupVersion{Group: "rhdh.redhat.com", Version: "v1alpha3"} 14 | 15 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 16 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 17 | 18 | // AddToScheme adds the types in this group-version to the given scheme. 19 | AddToScheme = SchemeBuilder.AddToScheme 20 | ) 21 | -------------------------------------------------------------------------------- /bundle/backstage.io/bundle.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | # Core bundle labels. 4 | LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 5 | LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ 6 | LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ 7 | LABEL operators.operatorframework.io.bundle.package.v1=backstage-operator 8 | LABEL operators.operatorframework.io.bundle.channels.v1=alpha 9 | LABEL operators.operatorframework.io.bundle.channel.default.v1=alpha 10 | LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.37.0 11 | LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1 12 | LABEL operators.operatorframework.io.metrics.project_layout=go.kubebuilder.io/v4 13 | 14 | # Labels for testing. 15 | LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 16 | LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ 17 | 18 | # Copy files to locations specified by labels. 19 | COPY bundle/backstage.io/manifests /manifests/ 20 | COPY bundle/backstage.io/metadata /metadata/ 21 | COPY bundle/backstage.io/tests/scorecard /tests/scorecard/ 22 | -------------------------------------------------------------------------------- /bundle/backstage.io/manifests/backstage-backstage-editor-role_rbac.authorization.k8s.io_v1_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app.kubernetes.io/component: rbac 7 | app.kubernetes.io/created-by: backstage-operator 8 | app.kubernetes.io/instance: backstage-editor-role 9 | app.kubernetes.io/managed-by: kustomize 10 | app.kubernetes.io/name: clusterrole 11 | app.kubernetes.io/part-of: backstage-operator 12 | name: backstage-backstage-editor-role 13 | rules: 14 | - apiGroups: 15 | - rhdh.redhat.com 16 | resources: 17 | - backstages 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - rhdh.redhat.com 28 | resources: 29 | - backstages/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /bundle/backstage.io/manifests/backstage-backstage-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app.kubernetes.io/component: rbac 7 | app.kubernetes.io/created-by: backstage-operator 8 | app.kubernetes.io/instance: backstage-viewer-role 9 | app.kubernetes.io/managed-by: kustomize 10 | app.kubernetes.io/name: clusterrole 11 | app.kubernetes.io/part-of: backstage-operator 12 | name: backstage-backstage-viewer-role 13 | rules: 14 | - apiGroups: 15 | - rhdh.redhat.com 16 | resources: 17 | - backstages 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - rhdh.redhat.com 24 | resources: 25 | - backstages/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /bundle/backstage.io/manifests/backstage-controller-manager-metrics-service_v1_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app.kubernetes.io/component: metrics 7 | app.kubernetes.io/created-by: backstage-operator 8 | app.kubernetes.io/instance: controller-manager-metrics-service 9 | app.kubernetes.io/managed-by: kustomize 10 | app.kubernetes.io/name: service 11 | app.kubernetes.io/part-of: backstage-operator 12 | control-plane: controller-manager 13 | name: backstage-controller-manager-metrics-service 14 | spec: 15 | ports: 16 | - name: metrics 17 | port: 8443 18 | protocol: TCP 19 | targetPort: metrics 20 | selector: 21 | control-plane: controller-manager 22 | status: 23 | loadBalancer: {} 24 | -------------------------------------------------------------------------------- /bundle/backstage.io/manifests/backstage-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app.kubernetes.io/component: metrics-rbac 7 | app.kubernetes.io/created-by: backstage-operator 8 | app.kubernetes.io/instance: metrics-reader 9 | app.kubernetes.io/managed-by: kustomize 10 | app.kubernetes.io/name: clusterrole 11 | app.kubernetes.io/part-of: backstage-operator 12 | name: backstage-metrics-reader 13 | rules: 14 | - nonResourceURLs: 15 | - /metrics 16 | verbs: 17 | - get 18 | -------------------------------------------------------------------------------- /bundle/backstage.io/metadata/annotations.yaml: -------------------------------------------------------------------------------- 1 | annotations: 2 | # Core bundle annotations. 3 | operators.operatorframework.io.bundle.mediatype.v1: registry+v1 4 | operators.operatorframework.io.bundle.manifests.v1: manifests/ 5 | operators.operatorframework.io.bundle.metadata.v1: metadata/ 6 | operators.operatorframework.io.bundle.package.v1: backstage-operator 7 | operators.operatorframework.io.bundle.channels.v1: alpha 8 | operators.operatorframework.io.bundle.channel.default.v1: alpha 9 | operators.operatorframework.io.metrics.builder: operator-sdk-v1.37.0 10 | operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 11 | operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v4 12 | 13 | # Annotations for testing. 14 | operators.operatorframework.io.test.mediatype.v1: scorecard+v1 15 | operators.operatorframework.io.test.config.v1: tests/scorecard/ 16 | -------------------------------------------------------------------------------- /bundle/backstage.io/tests/scorecard/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: 8 | - entrypoint: 9 | - scorecard-test 10 | - basic-check-spec 11 | image: quay.io/operator-framework/scorecard-test:v1.37.0 12 | labels: 13 | suite: basic 14 | test: basic-check-spec-test 15 | storage: 16 | spec: 17 | mountPath: {} 18 | - entrypoint: 19 | - scorecard-test 20 | - olm-bundle-validation 21 | image: quay.io/operator-framework/scorecard-test:v1.37.0 22 | labels: 23 | suite: olm 24 | test: olm-bundle-validation-test 25 | storage: 26 | spec: 27 | mountPath: {} 28 | - entrypoint: 29 | - scorecard-test 30 | - olm-crds-have-validation 31 | image: quay.io/operator-framework/scorecard-test:v1.37.0 32 | labels: 33 | suite: olm 34 | test: olm-crds-have-validation-test 35 | storage: 36 | spec: 37 | mountPath: {} 38 | - entrypoint: 39 | - scorecard-test 40 | - olm-crds-have-resources 41 | image: quay.io/operator-framework/scorecard-test:v1.37.0 42 | labels: 43 | suite: olm 44 | test: olm-crds-have-resources-test 45 | storage: 46 | spec: 47 | mountPath: {} 48 | - entrypoint: 49 | - scorecard-test 50 | - olm-spec-descriptors 51 | image: quay.io/operator-framework/scorecard-test:v1.37.0 52 | labels: 53 | suite: olm 54 | test: olm-spec-descriptors-test 55 | storage: 56 | spec: 57 | mountPath: {} 58 | - entrypoint: 59 | - scorecard-test 60 | - olm-status-descriptors 61 | image: quay.io/operator-framework/scorecard-test:v1.37.0 62 | labels: 63 | suite: olm 64 | test: olm-status-descriptors-test 65 | storage: 66 | spec: 67 | mountPath: {} 68 | storage: 69 | spec: 70 | mountPath: {} 71 | -------------------------------------------------------------------------------- /bundle/rhdh/bundle.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | # Core bundle labels. 4 | LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 5 | LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ 6 | LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ 7 | LABEL operators.operatorframework.io.bundle.package.v1=rhdh 8 | LABEL operators.operatorframework.io.bundle.channels.v1=fast,fast-${CI_X_VERSION}.${CI_Y_VERSION} 9 | LABEL operators.operatorframework.io.bundle.channel.default.v1=fast 10 | LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.37.0 11 | LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1 12 | LABEL operators.operatorframework.io.metrics.project_layout=go.kubebuilder.io/v4 13 | 14 | # Labels for testing. 15 | LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 16 | LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ 17 | 18 | # Copy files to locations specified by labels. 19 | COPY bundle/rhdh/manifests /manifests/ 20 | COPY bundle/rhdh/metadata /metadata/ 21 | COPY bundle/rhdh/tests/scorecard /tests/scorecard/ 22 | -------------------------------------------------------------------------------- /bundle/rhdh/manifests/rhdh-backstage-editor-role_rbac.authorization.k8s.io_v1_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app.kubernetes.io/component: rbac 7 | app.kubernetes.io/created-by: backstage-operator 8 | app.kubernetes.io/instance: backstage-editor-role 9 | app.kubernetes.io/managed-by: kustomize 10 | app.kubernetes.io/name: clusterrole 11 | app.kubernetes.io/part-of: backstage-operator 12 | name: rhdh-backstage-editor-role 13 | rules: 14 | - apiGroups: 15 | - rhdh.redhat.com 16 | resources: 17 | - backstages 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - rhdh.redhat.com 28 | resources: 29 | - backstages/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /bundle/rhdh/manifests/rhdh-backstage-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app.kubernetes.io/component: rbac 7 | app.kubernetes.io/created-by: backstage-operator 8 | app.kubernetes.io/instance: backstage-viewer-role 9 | app.kubernetes.io/managed-by: kustomize 10 | app.kubernetes.io/name: clusterrole 11 | app.kubernetes.io/part-of: backstage-operator 12 | name: rhdh-backstage-viewer-role 13 | rules: 14 | - apiGroups: 15 | - rhdh.redhat.com 16 | resources: 17 | - backstages 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - rhdh.redhat.com 24 | resources: 25 | - backstages/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /bundle/rhdh/manifests/rhdh-controller-manager-metrics-service_v1_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app.kubernetes.io/component: metrics 7 | app.kubernetes.io/created-by: backstage-operator 8 | app.kubernetes.io/instance: controller-manager-metrics-service 9 | app.kubernetes.io/managed-by: kustomize 10 | app.kubernetes.io/name: service 11 | app.kubernetes.io/part-of: backstage-operator 12 | control-plane: controller-manager 13 | name: rhdh-controller-manager-metrics-service 14 | spec: 15 | ports: 16 | - name: metrics 17 | port: 8443 18 | protocol: TCP 19 | targetPort: metrics 20 | selector: 21 | control-plane: controller-manager 22 | status: 23 | loadBalancer: {} 24 | -------------------------------------------------------------------------------- /bundle/rhdh/manifests/rhdh-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app.kubernetes.io/component: metrics-rbac 7 | app.kubernetes.io/created-by: backstage-operator 8 | app.kubernetes.io/instance: metrics-reader 9 | app.kubernetes.io/managed-by: kustomize 10 | app.kubernetes.io/name: clusterrole 11 | app.kubernetes.io/part-of: backstage-operator 12 | name: rhdh-metrics-reader 13 | rules: 14 | - nonResourceURLs: 15 | - /metrics 16 | verbs: 17 | - get 18 | -------------------------------------------------------------------------------- /bundle/rhdh/metadata/annotations.yaml: -------------------------------------------------------------------------------- 1 | annotations: 2 | # Core bundle annotations. 3 | operators.operatorframework.io.bundle.mediatype.v1: registry+v1 4 | operators.operatorframework.io.bundle.manifests.v1: manifests/ 5 | operators.operatorframework.io.bundle.metadata.v1: metadata/ 6 | operators.operatorframework.io.bundle.package.v1: rhdh 7 | operators.operatorframework.io.bundle.channels.v1: fast,fast-${CI_X_VERSION}.${CI_Y_VERSION} 8 | operators.operatorframework.io.bundle.channel.default.v1: fast 9 | operators.operatorframework.io.metrics.builder: operator-sdk-v1.37.0 10 | operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 11 | operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v4 12 | 13 | # Annotations for testing. 14 | operators.operatorframework.io.test.mediatype.v1: scorecard+v1 15 | operators.operatorframework.io.test.config.v1: tests/scorecard/ 16 | -------------------------------------------------------------------------------- /bundle/rhdh/tests/scorecard/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: 8 | - entrypoint: 9 | - scorecard-test 10 | - basic-check-spec 11 | image: quay.io/operator-framework/scorecard-test:v1.37.0 12 | labels: 13 | suite: basic 14 | test: basic-check-spec-test 15 | storage: 16 | spec: 17 | mountPath: {} 18 | - entrypoint: 19 | - scorecard-test 20 | - olm-bundle-validation 21 | image: quay.io/operator-framework/scorecard-test:v1.37.0 22 | labels: 23 | suite: olm 24 | test: olm-bundle-validation-test 25 | storage: 26 | spec: 27 | mountPath: {} 28 | - entrypoint: 29 | - scorecard-test 30 | - olm-crds-have-validation 31 | image: quay.io/operator-framework/scorecard-test:v1.37.0 32 | labels: 33 | suite: olm 34 | test: olm-crds-have-validation-test 35 | storage: 36 | spec: 37 | mountPath: {} 38 | - entrypoint: 39 | - scorecard-test 40 | - olm-crds-have-resources 41 | image: quay.io/operator-framework/scorecard-test:v1.37.0 42 | labels: 43 | suite: olm 44 | test: olm-crds-have-resources-test 45 | storage: 46 | spec: 47 | mountPath: {} 48 | - entrypoint: 49 | - scorecard-test 50 | - olm-spec-descriptors 51 | image: quay.io/operator-framework/scorecard-test:v1.37.0 52 | labels: 53 | suite: olm 54 | test: olm-spec-descriptors-test 55 | storage: 56 | spec: 57 | mountPath: {} 58 | - entrypoint: 59 | - scorecard-test 60 | - olm-status-descriptors 61 | image: quay.io/operator-framework/scorecard-test:v1.37.0 62 | labels: 63 | suite: olm 64 | test: olm-status-descriptors-test 65 | storage: 66 | spec: 67 | mountPath: {} 68 | storage: 69 | spec: 70 | mountPath: {} 71 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/rhdh.redhat.com_backstages.yaml 6 | #+kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patches: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 12 | 13 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 14 | # patches here are for enabling the CA injection for each CRD 15 | #- path: patches/cainjection_in_immiches.yaml 16 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 17 | 18 | # [WEBHOOK] To enable webhook, uncomment the following section 19 | # the following config is for teaching kustomize how to do kustomization for CRDs. 20 | 21 | #configurations: 22 | #- kustomizeconfig.yaml 23 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_backstages.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: backstages.rhdh.redhat.com 8 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_backstages.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: backstages.rhdh.redhat.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/manager/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: operator 5 | labels: 6 | app: operator 7 | control-plane: controller-manager 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: operator 13 | strategy: 14 | type: RollingUpdate 15 | template: 16 | metadata: 17 | annotations: 18 | kubectl.kubernetes.io/default-container: manager 19 | labels: 20 | app: operator 21 | control-plane: controller-manager 22 | spec: 23 | # Required because the operator does not work without a Service Account Token 24 | automountServiceAccountToken: true # NOSONAR 25 | # Configure the nodeAffinity expression 26 | # according to the platforms which are supported by your solution. 27 | # It is considered best practice to support multiple architectures. You can 28 | # build your manager image using the makefile target docker-buildx. 29 | affinity: 30 | nodeAffinity: 31 | requiredDuringSchedulingIgnoredDuringExecution: 32 | nodeSelectorTerms: 33 | - matchExpressions: 34 | - key: kubernetes.io/arch 35 | operator: In 36 | values: 37 | - amd64 38 | # - arm64 39 | # - ppc64le 40 | # - s390x 41 | - key: kubernetes.io/os 42 | operator: In 43 | values: 44 | - linux 45 | securityContext: 46 | runAsNonRoot: true 47 | # (user): For common cases that do not require escalating privileges 48 | # it is recommended to ensure that all your Pods/Containers are restrictive. 49 | # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 50 | # Please uncomment the following code if your project does NOT have to work on old Kubernetes 51 | # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). 52 | # seccompProfile: 53 | # type: RuntimeDefault 54 | containers: 55 | - command: 56 | - /manager 57 | args: 58 | - --health-probe-bind-address=:8081 59 | - --metrics-bind-address=:8443 60 | - --metrics-secure=true 61 | - --leader-elect 62 | image: controller:latest 63 | name: manager 64 | securityContext: 65 | readOnlyRootFilesystem: true 66 | allowPrivilegeEscalation: false 67 | capabilities: 68 | drop: 69 | - "ALL" 70 | ports: 71 | - name: health 72 | containerPort: 8081 73 | - name: metrics 74 | containerPort: 8443 75 | livenessProbe: 76 | httpGet: 77 | path: /healthz 78 | port: health 79 | initialDelaySeconds: 15 80 | periodSeconds: 20 81 | readinessProbe: 82 | httpGet: 83 | path: /readyz 84 | port: health 85 | initialDelaySeconds: 5 86 | periodSeconds: 10 87 | resources: 88 | limits: 89 | cpu: 500m 90 | memory: 1Gi 91 | ephemeral-storage: 20Mi 92 | requests: 93 | cpu: 10m 94 | memory: 128Mi 95 | volumeMounts: 96 | - mountPath: /default-config 97 | name: default-config 98 | - mountPath: /plugin-deps 99 | name: plugin-deps 100 | 101 | serviceAccountName: controller-manager 102 | terminationGracePeriodSeconds: 10 103 | volumes: 104 | - name: default-config 105 | configMap: 106 | name: default-config 107 | - name: plugin-deps 108 | configMap: 109 | name: plugin-deps 110 | optional: true -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - deployment.yaml 6 | -------------------------------------------------------------------------------- /config/manifests/backstage.io/bases/backstage-operator.clusterserviceversion.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: ClusterServiceVersion 3 | metadata: 4 | annotations: 5 | description: Backstage Operator 6 | labels: 7 | operatorframework.io/arch.amd64: supported 8 | name: backstage-operator.v0.0.0 9 | spec: 10 | apiservicedefinitions: {} 11 | customresourcedefinitions: 12 | owned: 13 | - description: Backstage is the Schema for the Red Hat Developer Hub backstages 14 | API. It comes with pre-built plug-ins, configuration settings, and deployment 15 | mechanisms, which can help streamline the process of setting up a self-managed 16 | internal developer portal for adopters who are just starting out. 17 | displayName: Red Hat Developer Hub 18 | kind: Backstage 19 | name: backstages.rhdh.redhat.com 20 | version: v1alpha1 21 | - description: Backstage is the Schema for the Red Hat Developer Hub backstages 22 | API. It comes with pre-built plug-ins, configuration settings, and deployment 23 | mechanisms, which can help streamline the process of setting up a self-managed 24 | internal developer portal for adopters who are just starting out. 25 | displayName: Red Hat Developer Hub 26 | kind: Backstage 27 | name: backstages.rhdh.redhat.com 28 | version: v1alpha2 29 | - description: |- 30 | Backstage is the Schema for the Red Hat Developer Hub backstages API. 31 | It comes with pre-built plug-ins, configuration settings, and deployment mechanisms, 32 | which can help streamline the process of setting up a self-managed internal 33 | developer portal for adopters who are just starting out. 34 | displayName: Red Hat Developer Hub 35 | kind: Backstage 36 | name: backstages.rhdh.redhat.com 37 | version: v1alpha3 38 | description: | 39 | Backstage Operator 40 | displayName: Backstage Operator 41 | install: 42 | spec: 43 | deployments: null 44 | strategy: "" 45 | installModes: 46 | - supported: false 47 | type: OwnNamespace 48 | - supported: false 49 | type: SingleNamespace 50 | - supported: false 51 | type: MultiNamespace 52 | - supported: true 53 | type: AllNamespaces 54 | keywords: 55 | - Backstage 56 | links: 57 | - name: Backstage Operator 58 | url: https://github.com/redhat-developer/rhdh-operator 59 | maintainers: 60 | - email: asoro@redhat.com 61 | name: Armel Soro 62 | - email: cdaley@redhat.com 63 | name: Corey Daley 64 | - email: gazarenk@redhat.com 65 | name: Gennady Azarenkov 66 | - email: nboldt@redhat.com 67 | name: Nick Boldt 68 | maturity: alpha 69 | minKubeVersion: 1.25.0 70 | provider: 71 | name: Red Hat Inc. 72 | url: https://www.redhat.com/ 73 | version: 0.0.0 74 | -------------------------------------------------------------------------------- /config/manifests/backstage.io/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # These resources constitute the fully configured set of manifests 2 | # used to generate the 'manifests/' directory in a bundle. 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | # These resources constitute the fully configured set of manifests 6 | # used to generate the 'manifests/' directory in a bundle. 7 | resources: 8 | - bases/backstage-operator.clusterserviceversion.yaml 9 | - ../../profile/backstage.io 10 | - ../../samples 11 | - ../../scorecard 12 | -------------------------------------------------------------------------------- /config/manifests/rhdh/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | # These resources constitute the fully configured set of manifests 4 | # used to generate the 'manifests/' directory in a bundle. 5 | resources: 6 | - bases/backstage-operator.clusterserviceversion.yaml 7 | - ../../profile/rhdh 8 | - ../../samples 9 | - ../../scorecard 10 | 11 | patches: 12 | # Replace the name of the sample resources 13 | - target: 14 | kind: Backstage 15 | name: .* 16 | patch: |- 17 | - op: replace 18 | path: /metadata/name 19 | value: developer-hub 20 | -------------------------------------------------------------------------------- /config/profile/backstage.io/default-config/app-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: my-backstage-config-cm1 # placeholder for -default-appconfig 5 | data: 6 | default.app-config.yaml: | 7 | ########################################################################################################### 8 | # /!\ WARNING 9 | # 10 | # This is the default app-config file created and managed by the Operator for your CR. 11 | # Do NOT edit this manually in the Cluster, as your changes will be overridden by the Operator upon the 12 | # next reconciliation. 13 | # If you want to customize the application configuration, you should create your own app-config ConfigMap 14 | # and reference it in your CR. 15 | # See https://github.com/redhat-developer/rhdh-operator/blob/main/examples/rhdh-cr.yaml for an example. 16 | ########################################################################################################### 17 | backend: 18 | auth: 19 | externalAccess: 20 | - type: legacy 21 | options: 22 | subject: legacy-default-config 23 | # This is a default value, which you should change by providing your own app-config 24 | secret: "pl4s3Ch4ng3M3" 25 | -------------------------------------------------------------------------------- /config/profile/backstage.io/default-config/db-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: postgres-secrets # will be replaced 5 | type: Opaque 6 | #stringData: 7 | # POSTGRES_PASSWORD: 8 | # POSTGRES_PORT: "5432" 9 | # POSTGRES_USER: postgres 10 | # POSTGRESQL_ADMIN_PASSWORD: admin123 11 | # POSTGRES_HOST: bs1-db-service #placeholder -db-service -------------------------------------------------------------------------------- /config/profile/backstage.io/default-config/db-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: backstage-psql # placeholder for 'backstage-psql-' .NOTE: For the time it is static and linked to Secret-> postgres-secrets -> OSTGRES_HOST 5 | spec: 6 | selector: 7 | rhdh.redhat.com/app: backstage-psql-cr1 # placeholder for 'backstage-psql-' 8 | clusterIP: None 9 | ports: 10 | - port: 5432 11 | -------------------------------------------------------------------------------- /config/profile/backstage.io/default-config/db-statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: backstage-psql-cr1 # placeholder for 'backstage-psql-' 5 | spec: 6 | podManagementPolicy: OrderedReady 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | rhdh.redhat.com/app: backstage-psql-cr1 # placeholder for 'backstage-psql-' 11 | serviceName: backstage-psql-cr1-hl # placeholder for 'backstage-psql--hl' 12 | template: 13 | metadata: 14 | labels: 15 | rhdh.redhat.com/app: backstage-psql-cr1 # placeholder for 'backstage-psql-' 16 | spec: 17 | # fsGroup does not work for Openshift 18 | # AKS/EKS does not work w/o it 19 | #securityContext: 20 | # fsGroup: 26 21 | automountServiceAccountToken: false 22 | ## https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/ 23 | ## The optional .spec.persistentVolumeClaimRetentionPolicy field controls if and how PVCs are deleted during the lifecycle of a StatefulSet. 24 | ## You must enable the StatefulSetAutoDeletePVC feature gate on the API server and the controller manager to use this field. 25 | # persistentVolumeClaimRetentionPolicy: 26 | # whenDeleted: Retain 27 | # whenScaled: Retain 28 | containers: 29 | - env: 30 | - name: POSTGRESQL_PORT_NUMBER 31 | value: "5432" 32 | - name: POSTGRESQL_VOLUME_DIR 33 | value: /var/lib/pgsql/data 34 | - name: PGDATA 35 | value: /var/lib/pgsql/data/userdata 36 | image: quay.io/fedora/postgresql-15:latest # will be replaced with the actual image 37 | imagePullPolicy: IfNotPresent 38 | securityContext: 39 | # runAsUser:26 does not work for Openshift but looks work for AKS/EKS 40 | # runAsUser: 26 41 | runAsGroup: 0 42 | runAsNonRoot: true 43 | allowPrivilegeEscalation: false 44 | seccompProfile: 45 | type: RuntimeDefault 46 | capabilities: 47 | drop: 48 | - ALL 49 | livenessProbe: 50 | exec: 51 | command: 52 | - /bin/sh 53 | - -c 54 | - exec pg_isready -U ${POSTGRES_USER} -h 127.0.0.1 -p 5432 55 | failureThreshold: 6 56 | initialDelaySeconds: 30 57 | periodSeconds: 10 58 | successThreshold: 1 59 | timeoutSeconds: 5 60 | name: postgresql 61 | ports: 62 | - containerPort: 5432 63 | name: tcp-postgresql 64 | protocol: TCP 65 | readinessProbe: 66 | exec: 67 | command: 68 | - /bin/sh 69 | - -c 70 | - -e 71 | - | 72 | exec pg_isready -U ${POSTGRES_USER} -h 127.0.0.1 -p 5432 73 | failureThreshold: 6 74 | initialDelaySeconds: 5 75 | periodSeconds: 10 76 | successThreshold: 1 77 | timeoutSeconds: 5 78 | resources: 79 | requests: 80 | cpu: 250m 81 | memory: 256Mi 82 | limits: 83 | cpu: 250m 84 | memory: 1024Mi 85 | ephemeral-storage: 20Mi 86 | volumeMounts: 87 | - mountPath: /dev/shm 88 | name: dshm 89 | - mountPath: /var/lib/pgsql/data 90 | name: data 91 | restartPolicy: Always 92 | serviceAccountName: default 93 | volumes: 94 | - emptyDir: 95 | medium: Memory 96 | name: dshm 97 | updateStrategy: 98 | rollingUpdate: 99 | partition: 0 100 | type: RollingUpdate 101 | volumeClaimTemplates: 102 | - apiVersion: v1 103 | kind: PersistentVolumeClaim 104 | metadata: 105 | name: data 106 | spec: 107 | accessModes: 108 | - ReadWriteOnce 109 | resources: 110 | requests: 111 | storage: 1Gi -------------------------------------------------------------------------------- /config/profile/backstage.io/default-config/deployment.yaml: -------------------------------------------------------------------------------- 1 | # kubernetes/backstage.yaml 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: backstage 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: backstage 10 | template: 11 | metadata: 12 | labels: 13 | app: backstage 14 | spec: 15 | containers: 16 | - name: backstage-backend 17 | image: ghcr.io/backstage/backstage:1.38.1 18 | imagePullPolicy: IfNotPresent 19 | command: 20 | - "node" 21 | - "packages/backend" 22 | - "--no-node-snapshot" 23 | args: 24 | - "--config" 25 | - "app-config.yaml" 26 | - "--config" 27 | - "app-config.production.yaml" 28 | ports: 29 | - name: http 30 | containerPort: 7007 31 | # envFrom: 32 | # - secretRef: 33 | # name: postgres-secrets 34 | # - secretRef: 35 | # name: backstage-secrets -------------------------------------------------------------------------------- /config/profile/backstage.io/default-config/route.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: route.openshift.io/v1 2 | kind: Route 3 | metadata: 4 | name: route # placeholder for 'backstage-' 5 | spec: 6 | port: 7 | targetPort: http-backend 8 | path: / 9 | tls: 10 | insecureEdgeTerminationPolicy: Redirect 11 | termination: edge 12 | to: 13 | kind: Service 14 | name: # placeholder for 'backstage-' -------------------------------------------------------------------------------- /config/profile/backstage.io/default-config/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: backstage # placeholder for 'backstage-' 5 | spec: 6 | type: ClusterIP 7 | selector: 8 | rhdh.redhat.com/app: # placeholder for 'backstage-' 9 | ports: 10 | - name: http-backend 11 | port: 80 12 | targetPort: backend 13 | - name: http-metrics 14 | protocol: TCP 15 | port: 9464 16 | targetPort: 9464 -------------------------------------------------------------------------------- /config/profile/backstage.io/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namespace: backstage-system 5 | 6 | namePrefix: backstage- 7 | 8 | resources: 9 | - ../../crd 10 | - ../../rbac 11 | - ../../manager 12 | - namespace.yaml 13 | 14 | images: 15 | - name: controller 16 | newName: quay.io/rhdh-community/operator 17 | newTag: 0.7.0 18 | 19 | generatorOptions: 20 | disableNameSuffixHash: true 21 | 22 | configMapGenerator: 23 | - files: 24 | - default-config/app-config.yaml 25 | - default-config/db-secret.yaml 26 | - default-config/db-service.yaml 27 | - default-config/db-statefulset.yaml 28 | - default-config/deployment.yaml 29 | - default-config/service.yaml 30 | name: default-config 31 | -------------------------------------------------------------------------------- /config/profile/backstage.io/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: namespace 7 | app.kubernetes.io/instance: system 8 | app.kubernetes.io/component: manager 9 | app.kubernetes.io/created-by: backstage-operator 10 | app.kubernetes.io/part-of: backstage-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: system -------------------------------------------------------------------------------- /config/profile/external/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - ../../crd 6 | - ../../rbac 7 | - ../../manager 8 | 9 | images: 10 | - name: controller 11 | newName: quay.io/rhdh-community/operator 12 | newTag: 0.7.0 13 | -------------------------------------------------------------------------------- /config/profile/rhdh/default-config/app-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: my-backstage-config-cm1 # placeholder for -default-appconfig 5 | data: 6 | default.app-config.yaml: | 7 | ########################################################################################################### 8 | # /!\ WARNING 9 | # 10 | # This is the default app-config file created and managed by the Operator for your CR. 11 | # Do NOT edit this manually in the Cluster, as your changes will be overridden by the Operator upon the 12 | # next reconciliation. 13 | # If you want to customize the application configuration, you should create your own app-config ConfigMap 14 | # and reference it in your CR. 15 | # See https://docs.redhat.com/en/documentation/red_hat_developer_hub/1.4/html/configuring/provisioning-and-using-your-custom-configuration#provisioning-your-custom-configuration 16 | # for more details. 17 | ########################################################################################################### 18 | backend: 19 | auth: 20 | externalAccess: 21 | - type: legacy 22 | options: 23 | subject: legacy-default-config 24 | # This is a default value, which you should change by providing your own app-config 25 | secret: "pl4s3Ch4ng3M3" 26 | auth: 27 | providers: {} 28 | -------------------------------------------------------------------------------- /config/profile/rhdh/default-config/db-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: postgres-secrets # will be replaced 5 | type: Opaque 6 | #stringData: 7 | # POSTGRES_PASSWORD: 8 | # POSTGRES_PORT: "5432" 9 | # POSTGRES_USER: postgres 10 | # POSTGRESQL_ADMIN_PASSWORD: admin123 11 | # POSTGRES_HOST: bs1-db-service #placeholder -db-service -------------------------------------------------------------------------------- /config/profile/rhdh/default-config/db-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: backstage-psql # placeholder for 'backstage-psql-' .NOTE: For the time it is static and linked to Secret-> postgres-secrets -> OSTGRES_HOST 5 | spec: 6 | selector: 7 | rhdh.redhat.com/app: backstage-psql-cr1 # placeholder for 'backstage-psql-' 8 | clusterIP: None 9 | ports: 10 | - port: 5432 11 | -------------------------------------------------------------------------------- /config/profile/rhdh/default-config/db-statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: backstage-psql-cr1 # placeholder for 'backstage-psql-' 5 | spec: 6 | podManagementPolicy: OrderedReady 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | rhdh.redhat.com/app: backstage-psql-cr1 # placeholder for 'backstage-psql-' 11 | serviceName: backstage-psql-cr1-hl # placeholder for 'backstage-psql--hl' 12 | template: 13 | metadata: 14 | labels: 15 | rhdh.redhat.com/app: backstage-psql-cr1 # placeholder for 'backstage-psql-' 16 | spec: 17 | # fsGroup does not work for Openshift 18 | # AKS/EKS does not work w/o it 19 | #securityContext: 20 | # fsGroup: 26 21 | automountServiceAccountToken: false 22 | ## https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/ 23 | ## The optional .spec.persistentVolumeClaimRetentionPolicy field controls if and how PVCs are deleted during the lifecycle of a StatefulSet. 24 | ## You must enable the StatefulSetAutoDeletePVC feature gate on the API server and the controller manager to use this field. 25 | # persistentVolumeClaimRetentionPolicy: 26 | # whenDeleted: Retain 27 | # whenScaled: Retain 28 | containers: 29 | - env: 30 | - name: POSTGRESQL_PORT_NUMBER 31 | value: "5432" 32 | - name: POSTGRESQL_VOLUME_DIR 33 | value: /var/lib/pgsql/data 34 | - name: PGDATA 35 | value: /var/lib/pgsql/data/userdata 36 | image: quay.io/fedora/postgresql-15:latest # will be replaced with the actual image 37 | imagePullPolicy: IfNotPresent 38 | securityContext: 39 | # runAsUser:26 does not work for Openshift but looks work for AKS/EKS 40 | # runAsUser: 26 41 | runAsGroup: 0 42 | runAsNonRoot: true 43 | allowPrivilegeEscalation: false 44 | seccompProfile: 45 | type: RuntimeDefault 46 | capabilities: 47 | drop: 48 | - ALL 49 | livenessProbe: 50 | exec: 51 | command: 52 | - /bin/sh 53 | - -c 54 | - exec pg_isready -U ${POSTGRES_USER} -h 127.0.0.1 -p 5432 55 | failureThreshold: 6 56 | initialDelaySeconds: 30 57 | periodSeconds: 10 58 | successThreshold: 1 59 | timeoutSeconds: 5 60 | name: postgresql 61 | ports: 62 | - containerPort: 5432 63 | name: tcp-postgresql 64 | protocol: TCP 65 | readinessProbe: 66 | exec: 67 | command: 68 | - /bin/sh 69 | - -c 70 | - -e 71 | - | 72 | exec pg_isready -U ${POSTGRES_USER} -h 127.0.0.1 -p 5432 73 | failureThreshold: 6 74 | initialDelaySeconds: 5 75 | periodSeconds: 10 76 | successThreshold: 1 77 | timeoutSeconds: 5 78 | resources: 79 | requests: 80 | cpu: 250m 81 | memory: 256Mi 82 | limits: 83 | cpu: 250m 84 | memory: 1024Mi 85 | ephemeral-storage: 20Mi 86 | volumeMounts: 87 | - mountPath: /dev/shm 88 | name: dshm 89 | - mountPath: /var/lib/pgsql/data 90 | name: data 91 | restartPolicy: Always 92 | serviceAccountName: default 93 | volumes: 94 | - emptyDir: 95 | medium: Memory 96 | name: dshm 97 | updateStrategy: 98 | rollingUpdate: 99 | partition: 0 100 | type: RollingUpdate 101 | volumeClaimTemplates: 102 | - apiVersion: v1 103 | kind: PersistentVolumeClaim 104 | metadata: 105 | name: data 106 | spec: 107 | accessModes: 108 | - ReadWriteOnce 109 | resources: 110 | requests: 111 | storage: 1Gi -------------------------------------------------------------------------------- /config/profile/rhdh/default-config/db-statefulset.yaml.k8s: -------------------------------------------------------------------------------- 1 | # if securityContext not present in AKS/EKS, the error is like this: 2 | # Error: EACCES: permission denied, open '/dynamic-plugins-root/backstage-plugin-scaffolder-backend-module-github-dynamic-0.2.2.tgz' 3 | # fsGroup doesn not work for Openshift 4 | spec: 5 | template: 6 | spec: 7 | securityContext: 8 | # any group id 9 | fsGroup: 1001 -------------------------------------------------------------------------------- /config/profile/rhdh/default-config/deployment.yaml.k8s: -------------------------------------------------------------------------------- 1 | # if securityContext not present in AKS/EKS, the error is like this: 2 | # Error: EACCES: permission denied, open '/dynamic-plugins-root/backstage-plugin-scaffolder-backend-module-github-dynamic-0.2.2.tgz' 3 | # fsGroup doesn not work for Openshift 4 | spec: 5 | template: 6 | spec: 7 | securityContext: 8 | # any group id 9 | fsGroup: 1001 -------------------------------------------------------------------------------- /config/profile/rhdh/default-config/dynamic-plugins.yaml: -------------------------------------------------------------------------------- 1 | #apiVersion: v1 2 | #kind: ConfigMap 3 | #metadata: 4 | # name: default-dynamic-plugins # must be the same as (deployment.yaml).spec.template.spec.volumes.name.dynamic-plugins-conf.configMap.name 5 | #data: 6 | # "dynamic-plugins.yaml": | 7 | # ########################################################################################################### 8 | # # /!\ WARNING 9 | # # 10 | # # This is the default dynamic plugins configuration file created and managed by the Operator for your CR. 11 | # # Do NOT edit this manually in the Cluster, as your changes will be overridden by the Operator upon the 12 | # # next reconciliation. 13 | # # If you want to customize the dynamic plugins, you should create your own dynamic-plugins ConfigMap 14 | # # and reference it in your CR. 15 | # # See https://docs.redhat.com/en/documentation/red_hat_developer_hub/1.4/html/installing_and_viewing_plugins_in_red_hat_developer_hub/rhdh-installing-rhdh-plugins_title-plugins-rhdh-about#proc-config-dynamic-plugins-rhdh-operator_rhdh-installing-rhdh-plugins 16 | # # for more details or https://github.com/redhat-developer/rhdh-operator/blob/main/examples/rhdh-cr.yaml 17 | # # for an example. 18 | # ########################################################################################################### 19 | # includes: 20 | # - dynamic-plugins.default.yaml 21 | # plugins: [] 22 | #--- 23 | apiVersion: v1 24 | kind: ConfigMap 25 | metadata: 26 | name: default-dynamic-plugins 27 | data: 28 | dynamic-plugins.yaml: | 29 | includes: 30 | - dynamic-plugins.default.yaml 31 | plugins: 32 | - disabled: true 33 | package: "https://github.com/rhdhorchestrator/orchestrator-plugins-internal-release/releases/download/v1.5.1/backstage-plugin-orchestrator-1.5.1.tgz" 34 | integrity: sha512-7VOe+XGTUzrdO/av0DNHbydOjB3Lo+XdCs6fj3JVODLP7Ypd3GXHf/nssYxG5ZYC9F1t9MNeguE2bZOB6ckqTA== 35 | pluginConfig: 36 | dynamicPlugins: 37 | frontend: 38 | red-hat-developer-hub.backstage-plugin-orchestrator: 39 | appIcons: 40 | - importName: OrchestratorIcon 41 | module: OrchestratorPlugin 42 | name: orchestratorIcon 43 | dynamicRoutes: 44 | - importName: OrchestratorPage 45 | menuItem: 46 | icon: orchestratorIcon 47 | text: Orchestrator 48 | module: OrchestratorPlugin 49 | path: /orchestrator 50 | - disabled: true 51 | package: "https://github.com/rhdhorchestrator/orchestrator-plugins-internal-release/releases/download/v1.5.1/backstage-plugin-orchestrator-backend-dynamic-1.5.1.tgz" 52 | integrity: sha512-VIenFStdq9QvvmgmEMG8O7b2wqIebvEcqNeJ9SWZ8jen9t+efTK6D3Rde74LQ1no1QaHLx8RoxNCOuTUEF8O/g== 53 | pluginConfig: 54 | orchestrator: 55 | dataIndexService: 56 | url: http://sonataflow-platform-data-index-service 57 | dependencies: 58 | - ref: sonataflow 59 | - disabled: true 60 | package: "https://github.com/rhdhorchestrator/orchestrator-plugins-internal-release/releases/download/v1.5.1/backstage-plugin-scaffolder-backend-module-orchestrator-dynamic-1.5.1.tgz" 61 | integrity: sha512-bnVQjVsUZ470Vgm2kd5Lo/bVa2fF0q4GufBDc/8oTQsnP3zZJQqKFvFElBTCjY76RqkECydlvZ1UFybSzvockQ== 62 | pluginConfig: 63 | orchestrator: 64 | dataIndexService: 65 | url: http://sonataflow-platform-data-index-service 66 | -------------------------------------------------------------------------------- /config/profile/rhdh/default-config/route.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: route.openshift.io/v1 2 | kind: Route 3 | metadata: 4 | name: route # placeholder for 'backstage-' 5 | spec: 6 | port: 7 | targetPort: http-backend 8 | path: / 9 | tls: 10 | insecureEdgeTerminationPolicy: Redirect 11 | termination: edge 12 | to: 13 | kind: Service 14 | name: # placeholder for 'backstage-' -------------------------------------------------------------------------------- /config/profile/rhdh/default-config/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: backstage # placeholder for 'backstage-' 5 | spec: 6 | type: ClusterIP 7 | selector: 8 | rhdh.redhat.com/app: # placeholder for 'backstage-' 9 | ports: 10 | - name: http-backend 11 | port: 80 12 | targetPort: backend 13 | - name: http-metrics 14 | protocol: TCP 15 | port: 9464 16 | targetPort: 9464 -------------------------------------------------------------------------------- /config/profile/rhdh/default-config/service.yaml.k8s: -------------------------------------------------------------------------------- 1 | spec: 2 | type: NodePort 3 | -------------------------------------------------------------------------------- /config/profile/rhdh/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namespace: rhdh-operator 5 | 6 | namePrefix: rhdh- 7 | 8 | resources: 9 | - ../../crd 10 | - ../../rbac 11 | - ../../manager 12 | - namespace.yaml 13 | - plugin-rbac 14 | 15 | images: 16 | - name: controller 17 | newName: quay.io/rhdh/rhdh-rhel9-operator 18 | newTag: "1.7" 19 | 20 | patches: 21 | - path: patches/deployment-patch.yaml 22 | target: 23 | kind: Deployment 24 | name: operator 25 | 26 | generatorOptions: 27 | disableNameSuffixHash: true 28 | 29 | configMapGenerator: 30 | - files: 31 | - default-config/app-config.yaml 32 | - default-config/db-secret.yaml 33 | - default-config/db-service.yaml 34 | - default-config/db-statefulset.yaml 35 | - default-config/deployment.yaml 36 | - default-config/dynamic-plugins.yaml 37 | - default-config/route.yaml 38 | - default-config/service.yaml 39 | - default-config/deployment.yaml.k8s 40 | - default-config/db-statefulset.yaml.k8s 41 | - default-config/service.yaml.k8s 42 | name: default-config 43 | - files: 44 | - plugin-deps/sonataflow.yaml 45 | name: plugin-deps 46 | -------------------------------------------------------------------------------- /config/profile/rhdh/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: namespace 7 | app.kubernetes.io/instance: system 8 | app.kubernetes.io/component: manager 9 | app.kubernetes.io/created-by: backstage-operator 10 | app.kubernetes.io/part-of: backstage-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: rhdh-operator 13 | -------------------------------------------------------------------------------- /config/profile/rhdh/patches/deployment-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: operator 5 | labels: 6 | app: rhdh-operator 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: rhdh-operator 11 | template: 12 | metadata: 13 | labels: 14 | app: rhdh-operator 15 | app.kubernetes.io/component: rhdh-operator 16 | spec: 17 | containers: 18 | - name: manager 19 | env: 20 | - name: OPERATOR_NAME 21 | value: rhdh-operator 22 | - name: POD_NAME 23 | valueFrom: 24 | fieldRef: 25 | fieldPath: metadata.name 26 | - name: RELATED_IMAGE_postgresql 27 | value: quay.io/fedora/postgresql-15:latest 28 | - name: RELATED_IMAGE_backstage 29 | value: quay.io/rhdh/rhdh-hub-rhel9:next 30 | 31 | -------------------------------------------------------------------------------- /config/profile/rhdh/plugin-deps/sonataflow.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: NetworkPolicy 3 | metadata: 4 | name: allow-knative-to-sonataflow-and-workflows # hardcoded 5 | spec: 6 | podSelector: {} 7 | ingress: 8 | - from: 9 | - namespaceSelector: 10 | matchLabels: 11 | # Allow knative events to be delivered to workflows. 12 | kubernetes.io/metadata.name: knative-eventing 13 | - namespaceSelector: 14 | matchLabels: 15 | # Allow auxiliary knative function for workflow (such as m2k-save-transformation) 16 | kubernetes.io/metadata.name: knative-serving 17 | --- 18 | # NetworkPolicy to unblock incoming traffic to the namespace 19 | apiVersion: networking.k8s.io/v1 20 | kind: NetworkPolicy 21 | metadata: 22 | name: allow-external-communication # hardcoded 23 | spec: 24 | podSelector: {} 25 | ingress: 26 | - from: 27 | - namespaceSelector: 28 | matchLabels: 29 | # Allow knative events to be delivered to workflows. 30 | policy-group.network.openshift.io/ingress: "" 31 | --- 32 | apiVersion: networking.k8s.io/v1 33 | kind: NetworkPolicy 34 | metadata: 35 | name: allow-intra-network # hardcoded 36 | spec: 37 | # Apply this policy to all pods in the namespace 38 | podSelector: {} 39 | # Specify policy type as 'Ingress' to control incoming traffic rules 40 | policyTypes: 41 | - Ingress 42 | ingress: 43 | - from: 44 | # Allow ingress from any pod within the same namespace 45 | - podSelector: {} 46 | --- 47 | # NetworkPolicy to allow openshift-user-workload-monitoring pods to access all pods within the workflow's namespace 48 | apiVersion: networking.k8s.io/v1 49 | kind: NetworkPolicy 50 | metadata: 51 | name: allow-monitoring-to-sonataflow-and-workflows # hardcoded 52 | spec: 53 | # Apply this policy to all pods in the namespace 54 | podSelector: {} 55 | # Specify policy type as 'Ingress' to control incoming traffic rules 56 | policyTypes: 57 | - Ingress 58 | ingress: 59 | - from: 60 | - namespaceSelector: 61 | matchLabels: 62 | # Allow openshift-user-workload-monitoring pods to access the workflow. 63 | kubernetes.io/metadata.name: openshift-user-workload-monitoring 64 | --- 65 | apiVersion: operator.knative.dev/v1beta1 66 | kind: KnativeEventing 67 | metadata: 68 | name: knative-eventing 69 | namespace: knative-eventing 70 | spec: 71 | Registry: {} 72 | --- 73 | apiVersion: operator.knative.dev/v1beta1 74 | kind: KnativeServing 75 | metadata: 76 | name: knative-serving 77 | namespace: knative-serving 78 | spec: 79 | controller-custom-certs: 80 | name: "" 81 | type: "" 82 | registry: {} 83 | --- 84 | apiVersion: sonataflow.org/v1alpha08 85 | kind: SonataFlowPlatform 86 | metadata: 87 | name: sonataflow-platform 88 | spec: 89 | monitoring: 90 | enabled: true 91 | services: 92 | dataIndex: 93 | enabled: true 94 | persistence: 95 | postgresql: 96 | secretRef: 97 | name: backstage-psql-secret-{{backstage-name}} 98 | userKey: POSTGRES_USER 99 | passwordKey: POSTGRES_PASSWORD 100 | serviceRef: 101 | name: backstage-psql-{{backstage-name}} 102 | namespace: {{backstage-ns}} 103 | databaseName: backstage_plugin_orchestrator 104 | jobService: 105 | enabled: true 106 | persistence: 107 | postgresql: 108 | secretRef: 109 | name: backstage-psql-secret-{{backstage-name}} 110 | userKey: POSTGRES_USER 111 | passwordKey: POSTGRES_PASSWORD 112 | serviceRef: 113 | name: backstage-psql-{{backstage-name}} 114 | namespace: {{backstage-ns}} 115 | databaseName: backstage_plugin_orchestrator 116 | -------------------------------------------------------------------------------- /config/profile/rhdh/plugin-infra/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - orchestrator/infra-sonataflow.yaml 6 | - orchestrator/infra-serverless.yaml 7 | 8 | -------------------------------------------------------------------------------- /config/profile/rhdh/plugin-infra/orchestrator/infra-serverless.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: openshift-serverless 5 | --- 6 | apiVersion: operators.coreos.com/v1 7 | kind: OperatorGroup 8 | metadata: 9 | name: serverless-operator-group 10 | namespace: openshift-serverless 11 | spec: 12 | --- 13 | apiVersion: operators.coreos.com/v1alpha1 14 | kind: Subscription 15 | metadata: 16 | name: serverless-operator 17 | namespace: openshift-serverless 18 | spec: 19 | channel: stable # channel of an operator package to subscribe to 20 | installPlanApproval: Automatic # whether the update should be installed automatically 21 | name: serverless-operator # name of the operator package 22 | source: redhat-operators # name of the catalog source 23 | sourceNamespace: openshift-marketplace 24 | --- 25 | apiVersion: v1 26 | kind: Namespace 27 | metadata: 28 | name: knative-serving 29 | --- 30 | apiVersion: v1 31 | kind: Namespace 32 | metadata: 33 | name: knative-eventing 34 | 35 | -------------------------------------------------------------------------------- /config/profile/rhdh/plugin-infra/orchestrator/infra-sonataflow.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: openshift-serverless-logic 5 | --- 6 | apiVersion: operators.coreos.com/v1 7 | kind: OperatorGroup 8 | metadata: 9 | name: openshift-serverless-logic 10 | namespace: openshift-serverless-logic 11 | spec: 12 | --- 13 | apiVersion: operators.coreos.com/v1alpha1 14 | kind: Subscription 15 | metadata: 16 | name: logic-operator-rhel8 17 | namespace: openshift-serverless-logic 18 | spec: 19 | channel: alpha # channel of an operator package to subscribe to 20 | installPlanApproval: Automatic # whether the update should be installed automatically 21 | name: logic-operator-rhel8 # name of the operator package 22 | source: redhat-operators # name of the catalog source 23 | sourceNamespace: openshift-marketplace 24 | startingCSV: logic-operator-rhel8.v1.35.0 # The initial version of the operator -------------------------------------------------------------------------------- /config/profile/rhdh/plugin-rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - rbac-sonataflow.yaml 6 | 7 | 8 | -------------------------------------------------------------------------------- /config/profile/rhdh/plugin-rbac/rbac-sonataflow.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: manager-sonataflow-role 5 | rules: 6 | - apiGroups: 7 | - "sonataflow.org" 8 | resources: 9 | - sonataflowplatforms 10 | - sonataflows 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - "networking.k8s.io" 21 | resources: 22 | - networkpolicies 23 | verbs: 24 | - create 25 | - delete 26 | - get 27 | - list 28 | - patch 29 | - update 30 | - watch 31 | - apiGroups: 32 | - "operator.knative.dev" 33 | resources: 34 | - knativeeventings 35 | - knativeservings 36 | verbs: 37 | - create 38 | - delete 39 | - get 40 | - list 41 | - patch 42 | - update 43 | - watch 44 | --- 45 | apiVersion: rbac.authorization.k8s.io/v1 46 | kind: ClusterRoleBinding 47 | metadata: 48 | labels: 49 | app.kubernetes.io/component: rbac 50 | app.kubernetes.io/instance: manager-rolebinding 51 | app.kubernetes.io/managed-by: kustomize 52 | app.kubernetes.io/name: clusterrolebinding 53 | app.kubernetes.io/part-of: backstage-operator 54 | name: manager-sonataflow-rolebinding 55 | roleRef: 56 | apiGroup: rbac.authorization.k8s.io 57 | kind: ClusterRole 58 | name: manager-sonataflow-role 59 | subjects: 60 | - kind: ServiceAccount 61 | name: rhdh-controller-manager 62 | namespace: rhdh-operator 63 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | # Prometheus Monitor Service (Metrics) 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | labels: 6 | control-plane: controller-manager 7 | app.kubernetes.io/name: backstage-operator 8 | app.kubernetes.io/managed-by: kustomize 9 | name: controller-manager-metrics-monitor 10 | namespace: system 11 | spec: 12 | endpoints: 13 | - path: /metrics 14 | port: https 15 | scheme: https 16 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 17 | tlsConfig: 18 | insecureSkipVerify: true 19 | selector: 20 | matchLabels: 21 | control-plane: controller-manager 22 | -------------------------------------------------------------------------------- /config/rbac/backstage_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit backstages. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: backstage-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: backstage-operator 10 | app.kubernetes.io/part-of: backstage-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: backstage-editor-role 13 | rules: 14 | - apiGroups: 15 | - rhdh.redhat.com 16 | resources: 17 | - backstages 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - rhdh.redhat.com 28 | resources: 29 | - backstages/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/rbac/backstage_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit backstages. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: backstage-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: backstage-operator 10 | app.kubernetes.io/part-of: backstage-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: backstage-viewer-role 13 | rules: 14 | - apiGroups: 15 | - rhdh.redhat.com 16 | resources: 17 | - backstages 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - rhdh.redhat.com 24 | resources: 25 | - backstages/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Metrics 13 | - metrics_service.yaml 14 | # The following RBAC configurations are used to protect 15 | # the metrics endpoint with authn/authz. These configurations 16 | # ensure that only authorized users and service accounts 17 | # can access the metrics endpoint. Comment the following 18 | # permissions if you want to disable this protection. 19 | # More info: https://book.kubebuilder.io/reference/metrics.html 20 | - metrics_auth_role.yaml 21 | - metrics_auth_role_binding.yaml 22 | # For each CRD, "Editor" and "Viewer" roles are scaffolded by 23 | # default, aiding admins in cluster management. Those roles are 24 | # not used by the Project itself. You can comment the following lines 25 | # if you do not want those helpers be installed with your Project. 26 | - metrics_reader.yaml 27 | - backstage_editor_role.yaml 28 | - backstage_viewer_role.yaml 29 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: role 7 | app.kubernetes.io/instance: leader-election-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: backstage-operator 10 | app.kubernetes.io/part-of: backstage-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: leader-election-role 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - configmaps 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - coordination.k8s.io 28 | resources: 29 | - leases 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | - create 35 | - update 36 | - patch 37 | - delete 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - events 42 | verbs: 43 | - create 44 | - patch 45 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: rolebinding 6 | app.kubernetes.io/instance: leader-election-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: backstage-operator 9 | app.kubernetes.io/part-of: backstage-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: leader-election-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: leader-election-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/metrics_auth_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: metrics-auth-role 6 | app.kubernetes.io/instance: metrics-auth-role 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: backstage-operator 9 | app.kubernetes.io/part-of: backstage-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: metrics-auth-role 12 | rules: 13 | - apiGroups: 14 | - authentication.k8s.io 15 | resources: 16 | - tokenreviews 17 | verbs: 18 | - create 19 | - apiGroups: 20 | - authorization.k8s.io 21 | resources: 22 | - subjectaccessreviews 23 | verbs: 24 | - create 25 | -------------------------------------------------------------------------------- /config/rbac/metrics_auth_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: metrics-auth-rolebinding 6 | app.kubernetes.io/instance: metrics-auth-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: backstage-operator 9 | app.kubernetes.io/part-of: backstage-operator 10 | name: metrics-auth-rolebinding 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: ClusterRole 14 | name: metrics-auth-role 15 | subjects: 16 | - kind: ServiceAccount 17 | name: controller-manager 18 | namespace: system 19 | -------------------------------------------------------------------------------- /config/rbac/metrics_reader.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: metrics-reader 7 | app.kubernetes.io/component: metrics-rbac 8 | app.kubernetes.io/created-by: backstage-operator 9 | app.kubernetes.io/part-of: backstage-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: metrics-reader 12 | rules: 13 | - nonResourceURLs: 14 | - "/metrics" 15 | verbs: 16 | - get 17 | -------------------------------------------------------------------------------- /config/rbac/metrics_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: service 7 | app.kubernetes.io/instance: controller-manager-metrics-service 8 | app.kubernetes.io/component: metrics 9 | app.kubernetes.io/created-by: backstage-operator 10 | app.kubernetes.io/part-of: backstage-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: controller-manager-metrics-service 13 | namespace: system 14 | spec: 15 | ports: 16 | - name: metrics 17 | port: 8443 18 | protocol: TCP 19 | targetPort: metrics 20 | selector: 21 | control-plane: controller-manager 22 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | - persistentvolumeclaims 12 | - secrets 13 | - services 14 | verbs: 15 | - create 16 | - delete 17 | - get 18 | - list 19 | - patch 20 | - update 21 | - watch 22 | - apiGroups: 23 | - "" 24 | resources: 25 | - persistentvolumes 26 | verbs: 27 | - get 28 | - list 29 | - watch 30 | - apiGroups: 31 | - apps 32 | resources: 33 | - deployments 34 | - statefulsets 35 | verbs: 36 | - create 37 | - delete 38 | - get 39 | - list 40 | - patch 41 | - update 42 | - watch 43 | - apiGroups: 44 | - config.openshift.io 45 | resources: 46 | - ingresses 47 | verbs: 48 | - get 49 | - apiGroups: 50 | - rhdh.redhat.com 51 | resources: 52 | - backstages 53 | verbs: 54 | - create 55 | - delete 56 | - get 57 | - list 58 | - patch 59 | - update 60 | - watch 61 | - apiGroups: 62 | - rhdh.redhat.com 63 | resources: 64 | - backstages/finalizers 65 | verbs: 66 | - update 67 | - apiGroups: 68 | - rhdh.redhat.com 69 | resources: 70 | - backstages/status 71 | verbs: 72 | - get 73 | - patch 74 | - update 75 | - apiGroups: 76 | - route.openshift.io 77 | resources: 78 | - routes 79 | - routes/custom-host 80 | verbs: 81 | - create 82 | - delete 83 | - get 84 | - list 85 | - patch 86 | - update 87 | - watch 88 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: manager-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: backstage-operator 9 | app.kubernetes.io/part-of: backstage-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: manager-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: manager-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: serviceaccount 6 | app.kubernetes.io/instance: controller-manager 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: backstage-operator 9 | app.kubernetes.io/part-of: backstage-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/samples/_v1alpha1_backstage.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rhdh.redhat.com/v1alpha1 2 | kind: Backstage 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: backstage 6 | # app.kubernetes.io/instance: backstage-sample 7 | # app.kubernetes.io/part-of: backstage-operator 8 | # app.kubernetes.io/managed-by: kustomize 9 | # app.kubernetes.io/created-by: backstage-operator 10 | name: backstage-sample 11 | #spec: 12 | # TODO(user): Add fields here 13 | -------------------------------------------------------------------------------- /config/samples/_v1alpha2_backstage.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rhdh.redhat.com/v1alpha2 2 | kind: Backstage 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: backstage 6 | # app.kubernetes.io/instance: backstage-sample 7 | # app.kubernetes.io/part-of: backstage-operator 8 | # app.kubernetes.io/managed-by: kustomize 9 | # app.kubernetes.io/created-by: backstage-operator 10 | name: backstage-sample 11 | #spec: 12 | # TODO(user): Add fields here 13 | -------------------------------------------------------------------------------- /config/samples/_v1alpha3_backstage.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rhdh.redhat.com/v1alpha3 2 | kind: Backstage 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: backstage 6 | # app.kubernetes.io/instance: backstage-sample 7 | # app.kubernetes.io/part-of: backstage-operator 8 | # app.kubernetes.io/managed-by: kustomize 9 | # app.kubernetes.io/created-by: backstage-operator 10 | name: backstage-sample 11 | #spec: 12 | # TODO(user): Add fields here 13 | -------------------------------------------------------------------------------- /config/samples/catalog-operator-group.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1 2 | kind: OperatorGroup 3 | metadata: 4 | name: {{PROFILE_SHORT}}-operator-group -------------------------------------------------------------------------------- /config/samples/catalog-source-template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: CatalogSource 3 | metadata: 4 | name: {{PROFILE_SHORT}}-operator 5 | spec: 6 | sourceType: grpc 7 | image: {{CATALOG_IMG}} 8 | displayName: "Upstream Backstage Operator (profile: {{PROFILE}})" -------------------------------------------------------------------------------- /config/samples/catalog-subscription-template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: Subscription 3 | metadata: 4 | name: {{PROFILE_SHORT}}-operator 5 | spec: 6 | channel: {{DEFAULT_CHANNEL}} 7 | installPlanApproval: Automatic 8 | name: {{BUNDLE_METADATA_PACKAGE_NAME}} 9 | source: {{PROFILE_SHORT}}-operator 10 | sourceNamespace: {{OLM_NAMESPACE}} 11 | startingCSV: {{PROFILE_SHORT}}-operator.v{{VERSION}} -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - _v1alpha3_backstage.yaml 4 | - _v1alpha2_backstage.yaml 5 | - _v1alpha1_backstage.yaml 6 | #+kubebuilder:scaffold:manifestskustomizesamples 7 | -------------------------------------------------------------------------------- /config/scorecard/bases/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: [] 8 | -------------------------------------------------------------------------------- /config/scorecard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/config.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | patches: 6 | - path: patches/basic.config.yaml 7 | target: 8 | group: scorecard.operatorframework.io 9 | kind: Configuration 10 | name: config 11 | version: v1alpha3 12 | - path: patches/olm.config.yaml 13 | target: 14 | group: scorecard.operatorframework.io 15 | kind: Configuration 16 | name: config 17 | version: v1alpha3 18 | -------------------------------------------------------------------------------- /config/scorecard/patches/basic.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - basic-check-spec 7 | image: quay.io/operator-framework/scorecard-test:v1.37.0 8 | labels: 9 | suite: basic 10 | test: basic-check-spec-test 11 | -------------------------------------------------------------------------------- /config/scorecard/patches/olm.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - olm-bundle-validation 7 | image: quay.io/operator-framework/scorecard-test:v1.37.0 8 | labels: 9 | suite: olm 10 | test: olm-bundle-validation-test 11 | - op: add 12 | path: /stages/0/tests/- 13 | value: 14 | entrypoint: 15 | - scorecard-test 16 | - olm-crds-have-validation 17 | image: quay.io/operator-framework/scorecard-test:v1.37.0 18 | labels: 19 | suite: olm 20 | test: olm-crds-have-validation-test 21 | - op: add 22 | path: /stages/0/tests/- 23 | value: 24 | entrypoint: 25 | - scorecard-test 26 | - olm-crds-have-resources 27 | image: quay.io/operator-framework/scorecard-test:v1.37.0 28 | labels: 29 | suite: olm 30 | test: olm-crds-have-resources-test 31 | - op: add 32 | path: /stages/0/tests/- 33 | value: 34 | entrypoint: 35 | - scorecard-test 36 | - olm-spec-descriptors 37 | image: quay.io/operator-framework/scorecard-test:v1.37.0 38 | labels: 39 | suite: olm 40 | test: olm-spec-descriptors-test 41 | - op: add 42 | path: /stages/0/tests/- 43 | value: 44 | entrypoint: 45 | - scorecard-test 46 | - olm-status-descriptors 47 | image: quay.io/operator-framework/scorecard-test:v1.37.0 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /docs/images/backstage_admin_configmap_and_cr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer/rhdh-operator/9bf2e1de70d39f91103ba724e5da3058eb0abcdb/docs/images/backstage_admin_configmap_and_cr.jpg -------------------------------------------------------------------------------- /docs/images/backstage_application_advanced_config.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer/rhdh-operator/9bf2e1de70d39f91103ba724e5da3058eb0abcdb/docs/images/backstage_application_advanced_config.jpg -------------------------------------------------------------------------------- /docs/images/backstage_kubernetes_runtime.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer/rhdh-operator/9bf2e1de70d39f91103ba724e5da3058eb0abcdb/docs/images/backstage_kubernetes_runtime.jpg -------------------------------------------------------------------------------- /docs/images/backstage_operator_configuration_layers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer/rhdh-operator/9bf2e1de70d39f91103ba724e5da3058eb0abcdb/docs/images/backstage_operator_configuration_layers.jpg -------------------------------------------------------------------------------- /examples/bs-existing-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: existing-postgres-secret 5 | type: Opaque 6 | stringData: 7 | POSTGRES_PASSWORD: "admin123" 8 | POSTGRES_PORT: "5432" 9 | POSTGRES_USER: "postgres" 10 | POSTGRESQL_ADMIN_PASSWORD: "admin123" 11 | POSTGRES_HOST: "backstage-psql-bs-existing-secret" 12 | 13 | --- 14 | apiVersion: rhdh.redhat.com/v1alpha3 15 | kind: Backstage 16 | metadata: 17 | name: bs-existing-secret 18 | spec: 19 | database: 20 | enableLocalDb: true 21 | authSecretName: existing-postgres-secret 22 | -------------------------------------------------------------------------------- /examples/bs-route-disabled.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rhdh.redhat.com/v1alpha3 2 | kind: Backstage 3 | metadata: 4 | name: bs-route-disabled 5 | spec: 6 | application: 7 | route: 8 | enabled: false 9 | subdomain: test-bs-route-disabled 10 | -------------------------------------------------------------------------------- /examples/bs-route.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rhdh.redhat.com/v1alpha3 2 | kind: Backstage 3 | metadata: 4 | name: bs-route 5 | spec: 6 | application: 7 | route: 8 | subdomain: test-bs-route 9 | -------------------------------------------------------------------------------- /examples/bs1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rhdh.redhat.com/v1alpha3 2 | kind: Backstage 3 | metadata: 4 | name: bs1 5 | -------------------------------------------------------------------------------- /examples/filemounts.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: cm1 5 | data: 6 | file11.txt: | 7 | My file11 content 8 | file12.txt: | 9 | My file12 content 10 | 11 | --- 12 | apiVersion: v1 13 | kind: ConfigMap 14 | metadata: 15 | name: cm2 16 | data: 17 | file21.txt: | 18 | My file21 content 19 | file22.txt: | 20 | My file22 content 21 | 22 | --- 23 | apiVersion: v1 24 | kind: ConfigMap 25 | metadata: 26 | name: cm3 27 | data: 28 | file31.txt: | 29 | My file31 content 30 | file32.txt: | 31 | My file32 content 32 | 33 | --- 34 | apiVersion: v1 35 | kind: ConfigMap 36 | metadata: 37 | name: cm4 38 | data: 39 | file41.txt: | 40 | My file41 content 41 | file42.txt: | 42 | My file42 content 43 | 44 | --- 45 | apiVersion: v1 46 | kind: Secret 47 | metadata: 48 | name: secret1 49 | stringData: 50 | secret11.txt: | 51 | content 52 | 53 | --- 54 | apiVersion: v1 55 | kind: Secret 56 | metadata: 57 | name: secret2 58 | stringData: 59 | secret21.txt: | 60 | base64-encoded-content 61 | secret22.txt: | 62 | base64-encoded-content 63 | 64 | --- 65 | apiVersion: v1 66 | kind: Secret 67 | metadata: 68 | name: secret3 69 | stringData: 70 | secret31.txt: | 71 | base64-encoded-content 72 | secret32.txt: | 73 | base64-encoded-content 74 | 75 | --- 76 | apiVersion: rhdh.redhat.com/v1alpha3 77 | kind: Backstage 78 | metadata: 79 | name: my-rhdh-file-mounts 80 | spec: 81 | application: 82 | extraFiles: 83 | mountPath: /my/path 84 | configMaps: 85 | - name: cm1 # /my/path/file11.txt and /my/path/file12.txt expected, watched 86 | - name: cm2 # /my/path/file21.txt expected, watched 87 | key: file21.txt 88 | - name: cm3 # /my/cm3/path/file31.txt and /my/cm3/path/file312.txt expected, not watched 89 | mountPath: /my/cm3/path 90 | - name: cm4 # /my/cm4/path/file41.txt expected, watched 91 | mountPath: /my/cm4/path 92 | key: file41.txt 93 | secrets: 94 | - name: secret1 # /my/path/secret11.txt expected, watched 95 | key: secret11.txt 96 | - name: secret2 # /my/secret2/path/secret21.txt and /my/secret2/path/secret22.txt expected, not watched 97 | mountPath: /my/secret2/path 98 | - name: secret3 # /my/secret3/path/secret31.txt expected, watched 99 | mountPath: /my/secret3/path 100 | key: secret31.txt 101 | # - name: secret4 <-- this is forbidden 102 | -------------------------------------------------------------------------------- /examples/orchestrator.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: orchestrator-plugin 5 | data: 6 | dynamic-plugins.yaml: | 7 | includes: 8 | - dynamic-plugins.default.yaml 9 | plugins: 10 | - disabled: false 11 | package: "https://github.com/rhdhorchestrator/orchestrator-plugins-internal-release/releases/download/v1.5.1/backstage-plugin-orchestrator-1.5.1.tgz" 12 | integrity: sha512-7VOe+XGTUzrdO/av0DNHbydOjB3Lo+XdCs6fj3JVODLP7Ypd3GXHf/nssYxG5ZYC9F1t9MNeguE2bZOB6ckqTA== 13 | pluginConfig: 14 | dynamicPlugins: 15 | frontend: 16 | red-hat-developer-hub.backstage-plugin-orchestrator: 17 | appIcons: 18 | - importName: OrchestratorIcon 19 | module: OrchestratorPlugin 20 | name: orchestratorIcon 21 | dynamicRoutes: 22 | - importName: OrchestratorPage 23 | menuItem: 24 | icon: orchestratorIcon 25 | text: Orchestrator 26 | module: OrchestratorPlugin 27 | path: /orchestrator 28 | - disabled: false 29 | package: "https://github.com/rhdhorchestrator/orchestrator-plugins-internal-release/releases/download/v1.5.1/backstage-plugin-orchestrator-backend-dynamic-1.5.1.tgz" 30 | integrity: sha512-VIenFStdq9QvvmgmEMG8O7b2wqIebvEcqNeJ9SWZ8jen9t+efTK6D3Rde74LQ1no1QaHLx8RoxNCOuTUEF8O/g== 31 | pluginConfig: 32 | orchestrator: 33 | dataIndexService: 34 | url: http://sonataflow-platform-data-index-service 35 | dependencies: 36 | - ref: sonataflow 37 | - disabled: false 38 | package: "https://github.com/rhdhorchestrator/orchestrator-plugins-internal-release/releases/download/v1.5.1/backstage-plugin-scaffolder-backend-module-orchestrator-dynamic-1.5.1.tgz" 39 | integrity: sha512-bnVQjVsUZ470Vgm2kd5Lo/bVa2fF0q4GufBDc/8oTQsnP3zZJQqKFvFElBTCjY76RqkECydlvZ1UFybSzvockQ== 40 | pluginConfig: 41 | orchestrator: 42 | dataIndexService: 43 | url: http://sonataflow-platform-data-index-service 44 | --- 45 | apiVersion: v1 46 | kind: ConfigMap 47 | metadata: 48 | name: app-config-rhdh 49 | data: 50 | app-config-rhdh.yaml: |- 51 | auth: 52 | environment: development 53 | providers: 54 | guest: 55 | # using the guest user to query the '/api/dynamic-plugins-info/loaded-plugins' endpoint. 56 | dangerouslyAllowOutsideDevelopment: true 57 | --- 58 | apiVersion: rhdh.redhat.com/v1alpha3 59 | kind: Backstage 60 | metadata: 61 | name: orchestrator 62 | spec: 63 | application: 64 | appConfig: 65 | configMaps: 66 | - name: app-config-rhdh 67 | dynamicPluginsConfigMapName: orchestrator-plugin 68 | -------------------------------------------------------------------------------- /examples/raw-runtime-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: raw-runtime-config 5 | data: 6 | app-config.yaml: |- 7 | apiVersion: v1 8 | kind: ConfigMap 9 | metadata: 10 | name: my-backstage-config-cm1 # placeholder for -default-appconfig 11 | data: 12 | my.app-config.yaml: | 13 | backend: 14 | database: 15 | client: better-sqlite3 16 | connection: ':memory:' 17 | auth: 18 | externalAccess: 19 | - type: legacy 20 | options: 21 | subject: legacy-default-config 22 | # This is a default value, which you should change by providing your own app-config 23 | secret: "pl4s3Ch4ng3M3" 24 | auth: 25 | providers: {} 26 | service.yaml: |- 27 | apiVersion: v1 28 | kind: Service 29 | metadata: 30 | name: backstage # placeholder for 'backstage-' 31 | annotations: 32 | my.custom-ann1: val1 33 | spec: 34 | type: ClusterIP 35 | selector: 36 | rhdh.redhat.com/app: # placeholder for 'backstage-' 37 | ports: 38 | - name: http-backend 39 | port: 80 40 | targetPort: backend 41 | #- name: http-metrics 42 | # protocol: TCP 43 | # port: 9464 44 | # targetPort: 9464 45 | 46 | --- 47 | apiVersion: v1 48 | kind: Secret 49 | metadata: 50 | name: some-secrets 51 | type: Opaque 52 | stringData: 53 | MY_SUPER_SECRET_1: "some value" # notsecret 54 | 55 | --- 56 | apiVersion: rhdh.redhat.com/v1alpha3 57 | kind: Backstage 58 | metadata: 59 | name: bs-raw-runtime-config 60 | spec: 61 | rawRuntimeConfig: 62 | backstageConfig: raw-runtime-config 63 | database: 64 | enableLocalDb: false 65 | -------------------------------------------------------------------------------- /examples/rhdh-cr.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: app-config-rhdh 5 | data: 6 | "app-config-rhdh.yaml": | 7 | app: 8 | title: My Awesome RHDH 9 | # As of 0.6 (RHDH 1.6), this is not needed on OCP by default, but needed on other platforms 10 | # baseUrl: https://my-rhdh-example.com 11 | backend: 12 | # As of 0.6 (RHDH 1.6), this is not needed on OCP by default, but needed on other platforms 13 | # baseUrl: https://my-rhdh-example.com 14 | auth: 15 | externalAccess: 16 | - type: legacy 17 | options: 18 | subject: legacy-default-config 19 | secret: "${BACKEND_SECRET}" 20 | #cors: 21 | # # As of 0.6 (RHDH 1.6), this is not needed on OCP by default, but needed on other platforms 22 | # origin: https://my-rhdh-example.com 23 | auth: 24 | # see https://backstage.io/docs/auth/ to learn about auth providers 25 | environment: development 26 | providers: 27 | github: 28 | development: 29 | clientId: '${GH_CLIENT_ID}' 30 | clientSecret: '${GH_CLIENT_SECRET}' 31 | 32 | --- 33 | apiVersion: v1 34 | kind: Secret 35 | metadata: 36 | name: secrets-rhdh 37 | stringData: 38 | # generated with the command below (from https://backstage.io/docs/auth/service-to-service-auth/#setup): 39 | # node -p 'require("crypto").randomBytes(24).toString("base64")' 40 | BACKEND_SECRET: "R2FxRVNrcmwzYzhhN3l0V1VRcnQ3L1pLT09WaVhDNUEK" # notsecret 41 | GH_ORG: "my-gh-org" 42 | GH_CLIENT_ID: "my GH client ID" 43 | GH_CLIENT_SECRET: "my GH client secret" 44 | 45 | --- 46 | apiVersion: v1 47 | kind: ConfigMap 48 | metadata: 49 | name: dynamic-plugins-rhdh 50 | data: 51 | dynamic-plugins.yaml: | 52 | includes: 53 | - dynamic-plugins.default.yaml 54 | plugins: 55 | - package: './dynamic-plugins/dist/backstage-plugin-catalog-backend-module-github-dynamic' 56 | disabled: false 57 | pluginConfig: 58 | catalog: 59 | providers: 60 | github: 61 | myorg: 62 | organization: '${GH_ORG}' 63 | schedule: 64 | # supports cron, ISO duration, "human duration" (used below) 65 | frequency: { minutes: 30} 66 | # supports ISO duration, "human duration (used below) 67 | timeout: { minutes: 3} 68 | initialDelay: { seconds: 15} 69 | 70 | --- 71 | apiVersion: rhdh.redhat.com/v1alpha3 72 | kind: Backstage 73 | metadata: 74 | name: my-rhdh 75 | spec: 76 | deployment: 77 | patch: 78 | spec: 79 | template: 80 | spec: 81 | containers: 82 | - name: backstage-backend 83 | image: quay.io/rhdh/rhdh-hub-rhel9:latest 84 | application: 85 | appConfig: 86 | configMaps: 87 | - name: app-config-rhdh 88 | dynamicPluginsConfigMapName: dynamic-plugins-rhdh 89 | extraEnvs: 90 | secrets: 91 | - name: secrets-rhdh 92 | -------------------------------------------------------------------------------- /hack/db_copy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | to_host= 4 | to_port=5432 5 | to_user=postgres 6 | 7 | from_host=127.0.0.1 8 | from_port=15432 9 | from_user=postgres 10 | 11 | allDB=("backstage_plugin_app" "backstage_plugin_auth" "backstage_plugin_catalog" "backstage_plugin_permission" "backstage_plugin_scaffolder" "backstage_plugin_search") 12 | 13 | for db in ${!allDB[@]}; 14 | do 15 | db=${allDB[$db]} 16 | echo Copying database: $db 17 | PGPASSWORD=$TO_PSW psql -h $to_host -p $to_port -U $to_user -c "create database $db;" 18 | pg_dump -h $from_host -p $from_port -U $from_user -d $db | PGPASSWORD=$TO_PSW psql -h $to_host -p $to_port -U $to_user -d $db 19 | done -------------------------------------------------------------------------------- /integration_tests/README.md: -------------------------------------------------------------------------------- 1 | 2 | **How to run Integration Tests** 3 | 4 | - For development (controller will reconsile internally) 5 | - As a part of the whole testing suite just: 6 | 7 | `make test` 8 | - Standalone, just integration tests: 9 | 10 | `make integration-test` 11 | 12 | - For QE (integration/e2e testing). No OLM 13 | There are 2 environment variables to use with `make` command 14 | - `USE_EXISTING_CLUSTER=true` tells test suite to use externally running cluster (from the current .kube/config context) instead of envtest. 15 | - `USE_EXISTING_CONTROLLER=true` tells test suite to use operator controller manager either deployed to the cluster OR (prevails if both) running locally with `make [install] run` command. Works only with `USE_EXISTING_CLUSTER=true` 16 | 17 | So, in most of the cases 18 | - Make sure you test desirable version of Operator image, that's what 19 | `make image-build image-push` does. See Makefile what version `` has. 20 | - Prepare your cluster with: 21 | - `make install deploy` this will install CR and deploy Controller to `backstage-system` 22 | - `make integration-test USE_EXISTING_CLUSTER=true USE_EXISTING_CONTROLLER=true` 23 | 24 | To run GINKGO with command line arguments (see https://onsi.github.io/ginkgo/#running-specs) 25 | use 'ARGS' environment variable. 26 | For example to run specific test(s) you can use something like: 27 | 28 | `make integration-test ARGS='--focus "my favorite test"'` 29 | 30 | **NOTE:** 31 | 32 | Some tests are Openshift specific only and skipped in a local envtest and bare k8s cluster. 33 | 34 | ` 35 | if !isOpenshiftCluster() { 36 | Skip("Skipped for non-Openshift cluster") 37 | } 38 | ` 39 | 40 | Some tests are workable only in real (EXISTING) cluster and skipped in envtest. 41 | 42 | ` 43 | if !*testEnv.UseExistingCluster { 44 | Skip("Skipped for not real cluster") 45 | } 46 | ` -------------------------------------------------------------------------------- /integration_tests/route_test.go: -------------------------------------------------------------------------------- 1 | package integration_tests 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | corev1 "k8s.io/api/core/v1" 8 | "k8s.io/utils/ptr" 9 | 10 | openshift "github.com/openshift/api/route/v1" 11 | 12 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 13 | 14 | "github.com/redhat-developer/rhdh-operator/pkg/model" 15 | 16 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 17 | 18 | "k8s.io/apimachinery/pkg/types" 19 | 20 | . "github.com/onsi/ginkgo/v2" 21 | . "github.com/onsi/gomega" 22 | ) 23 | 24 | var _ = When("create default backstage", func() { 25 | 26 | var ( 27 | ctx context.Context 28 | ns string 29 | ) 30 | 31 | BeforeEach(func() { 32 | ctx = context.Background() 33 | ns = createNamespace(ctx) 34 | }) 35 | 36 | AfterEach(func() { 37 | deleteNamespace(ctx, ns) 38 | }) 39 | 40 | for _, tt := range []struct { 41 | name string 42 | desiredRoute bsv1.Route 43 | expectedBaseUrlMatcher func() any 44 | }{ 45 | { 46 | name: "route disabled", 47 | desiredRoute: bsv1.Route{ 48 | Enabled: ptr.To(false), 49 | }, 50 | expectedBaseUrlMatcher: func() any { 51 | return BeEmpty() 52 | }, 53 | }, 54 | { 55 | name: "route with subdomain", 56 | desiredRoute: bsv1.Route{ 57 | //Host: "localhost", 58 | //Enabled: ptr.To(true), 59 | Subdomain: "test", 60 | }, 61 | expectedBaseUrlMatcher: func() any { 62 | return MatchRegexp("^https://test.+") 63 | }, 64 | }, 65 | { 66 | name: "route with host", 67 | desiredRoute: bsv1.Route{ 68 | Host: "host.example.com", 69 | Subdomain: "test", 70 | }, 71 | expectedBaseUrlMatcher: func() any { 72 | return "https://host.example.com" 73 | }, 74 | }, 75 | } { 76 | tt := tt 77 | It("creates Backstage object (on Openshift) - "+tt.name, func() { 78 | 79 | if !currentPlatform.IsOpenshift() { 80 | Skip("Skipped for non-Openshift cluster") 81 | } 82 | 83 | backstageName := createAndReconcileBackstage(ctx, ns, bsv1.BackstageSpec{ 84 | Application: &bsv1.Application{ 85 | Route: &tt.desiredRoute, 86 | }, 87 | }, "") 88 | 89 | Eventually(func() error { 90 | found := &bsv1.Backstage{} 91 | return k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found) 92 | }, time.Minute, time.Second).Should(Succeed()) 93 | 94 | _, err := NewTestBackstageReconciler(ns).ReconcileAny(ctx, reconcile.Request{ 95 | NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns}, 96 | }) 97 | Expect(err).To(Not(HaveOccurred())) 98 | 99 | Eventually(func(g Gomega) { 100 | if ptr.Deref(tt.desiredRoute.Enabled, true) { 101 | By("creating Route") 102 | route := &openshift.Route{} 103 | err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.RouteName(backstageName)}, route) 104 | g.Expect(err).To(Not(HaveOccurred()), controllerMessage()) 105 | 106 | g.Expect(route.Status.Ingress).To(HaveLen(1)) 107 | g.Expect(route.Status.Ingress[0].Host).To(Not(BeEmpty())) 108 | } 109 | 110 | By("updating the baseUrls in the default app-config CM, per the desired route settings (RHIDP-6192)") 111 | var appConfigCm corev1.ConfigMap 112 | err = k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: model.AppConfigDefaultName(backstageName)}, &appConfigCm) 113 | g.Expect(err).ShouldNot(HaveOccurred()) 114 | g.Expect(appConfigCm).To(HaveAppConfigBaseUrl(tt.expectedBaseUrlMatcher())) 115 | }, 5*time.Minute, time.Second).Should(Succeed()) 116 | 117 | }) 118 | } 119 | }) 120 | -------------------------------------------------------------------------------- /integration_tests/testdata/raw-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: bs1-deployment 5 | labels: 6 | app: bs1 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: bs1 12 | template: 13 | metadata: 14 | labels: 15 | app: bs1 16 | spec: 17 | containers: 18 | - name: backstage-backend 19 | image: busybox 20 | initContainers: 21 | - name: install-dynamic-plugins 22 | image: busybox 23 | -------------------------------------------------------------------------------- /integration_tests/testdata/raw-pvcs.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: myclaim1 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | volumeMode: Filesystem 9 | resources: 10 | requests: 11 | storage: 8Gi 12 | --- 13 | apiVersion: v1 14 | kind: PersistentVolumeClaim 15 | metadata: 16 | name: myclaim2 17 | spec: 18 | accessModes: 19 | - ReadWriteOnce 20 | volumeMode: Filesystem 21 | resources: 22 | requests: 23 | storage: 8Gi -------------------------------------------------------------------------------- /integration_tests/testdata/raw-pvcs2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: myclaim1 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | volumeMode: Filesystem 9 | resources: 10 | requests: 11 | storage: 1Gi 12 | storageClassName: "my-pvctest-storage" 13 | 14 | -------------------------------------------------------------------------------- /integration_tests/testdata/raw-statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: db-statefulset 5 | spec: 6 | serviceName: "" 7 | podManagementPolicy: Parallel 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: db 12 | template: 13 | metadata: 14 | labels: 15 | app: db 16 | spec: 17 | containers: 18 | - name: db 19 | image: busybox -------------------------------------------------------------------------------- /integration_tests/testdata/rhdh-replace-dynaplugin-root.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rhdh.redhat.com/v1alpha2 2 | kind: Backstage 3 | spec: 4 | deployment: 5 | patch: 6 | spec: 7 | template: 8 | spec: 9 | volumes: 10 | - $patch: replace 11 | name: dynamic-plugins-root 12 | persistentVolumeClaim: 13 | claimName: dynamic-plugins-root-pvc -------------------------------------------------------------------------------- /integration_tests/testdata/spec-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rhdh.redhat.com/v1alpha2 2 | kind: Backstage 3 | spec: 4 | deployment: 5 | patch: 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: backstage-backend 11 | image: busybox 12 | volumes: 13 | - ephemeral: 14 | volumeClaimTemplate: 15 | spec: 16 | storageClassName: "special" 17 | accessModes: 18 | - ReadWriteOnce 19 | resources: 20 | requests: 21 | storage: 2Gi 22 | name: my-volume -------------------------------------------------------------------------------- /internal/controller/mock_client.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | "strings" 9 | 10 | "k8s.io/apimachinery/pkg/api/errors" 11 | "k8s.io/apimachinery/pkg/api/meta" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | "sigs.k8s.io/controller-runtime/pkg/client" 15 | ) 16 | 17 | const implementMe = "implement me if needed" 18 | 19 | // Mock K8s go-client with very basic implementation of (some) methods 20 | // to be able to simply test controller logic 21 | type MockClient struct { 22 | objects map[NameKind][]byte 23 | } 24 | 25 | func NewMockClient() MockClient { 26 | return MockClient{ 27 | objects: map[NameKind][]byte{}, 28 | } 29 | } 30 | 31 | type NameKind struct { 32 | Name string 33 | Kind string 34 | } 35 | 36 | func kind(obj runtime.Object) string { 37 | str := reflect.TypeOf(obj).String() 38 | return str[strings.LastIndex(str, ".")+1:] 39 | //return reflect.TypeOf(obj).String() 40 | } 41 | 42 | func (m MockClient) Get(_ context.Context, key client.ObjectKey, obj client.Object, _ ...client.GetOption) error { 43 | 44 | if key.Name == "" { 45 | return fmt.Errorf("get: name should not be empty") 46 | } 47 | uobj := m.objects[NameKind{Name: key.Name, Kind: kind(obj)}] 48 | if uobj == nil { 49 | return errors.NewNotFound(schema.GroupResource{Group: "", Resource: kind(obj)}, key.Name) 50 | } 51 | err := json.Unmarshal(uobj, obj) 52 | if err != nil { 53 | return err 54 | } 55 | return nil 56 | } 57 | 58 | func (m MockClient) List(_ context.Context, _ client.ObjectList, _ ...client.ListOption) error { 59 | panic(implementMe) 60 | } 61 | 62 | func (m MockClient) Create(_ context.Context, obj client.Object, _ ...client.CreateOption) error { 63 | if obj.GetName() == "" { 64 | return fmt.Errorf("update: object Name should not be empty") 65 | } 66 | uobj := m.objects[NameKind{Name: obj.GetName(), Kind: kind(obj)}] 67 | if uobj != nil { 68 | return errors.NewAlreadyExists(schema.GroupResource{Group: "", Resource: kind(obj)}, obj.GetName()) 69 | } 70 | dat, err := json.Marshal(obj) 71 | if err != nil { 72 | return err 73 | } 74 | m.objects[NameKind{Name: obj.GetName(), Kind: kind(obj)}] = dat 75 | return nil 76 | } 77 | 78 | func (m MockClient) Delete(_ context.Context, _ client.Object, _ ...client.DeleteOption) error { 79 | panic(implementMe) 80 | } 81 | 82 | func (m MockClient) Update(_ context.Context, obj client.Object, _ ...client.UpdateOption) error { 83 | 84 | if obj.GetName() == "" { 85 | return fmt.Errorf("update: object Name should not be empty") 86 | } 87 | uobj := m.objects[NameKind{Name: obj.GetName(), Kind: kind(obj)}] 88 | if uobj == nil { 89 | return errors.NewNotFound(schema.GroupResource{Group: "", Resource: kind(obj)}, obj.GetName()) 90 | } 91 | dat, err := json.Marshal(obj) 92 | if err != nil { 93 | return err 94 | } 95 | m.objects[NameKind{Name: obj.GetName(), Kind: kind(obj)}] = dat 96 | return nil 97 | } 98 | 99 | func (m MockClient) Patch(_ context.Context, _ client.Object, _ client.Patch, _ ...client.PatchOption) error { 100 | panic(implementMe) 101 | } 102 | 103 | func (m MockClient) DeleteAllOf(_ context.Context, _ client.Object, _ ...client.DeleteAllOfOption) error { 104 | panic(implementMe) 105 | } 106 | 107 | func (m MockClient) Status() client.SubResourceWriter { 108 | panic(implementMe) 109 | } 110 | 111 | func (m MockClient) SubResource(_ string) client.SubResourceClient { 112 | panic(implementMe) 113 | } 114 | 115 | func (m MockClient) Scheme() *runtime.Scheme { 116 | panic(implementMe) 117 | } 118 | 119 | func (m MockClient) RESTMapper() meta.RESTMapper { 120 | panic(implementMe) 121 | } 122 | 123 | func (m MockClient) GroupVersionKindFor(_ runtime.Object) (schema.GroupVersionKind, error) { 124 | panic(implementMe) 125 | } 126 | 127 | func (m MockClient) IsObjectNamespaced(_ runtime.Object) (bool, error) { 128 | panic(implementMe) 129 | } 130 | -------------------------------------------------------------------------------- /internal/controller/platform_detector.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | ctrl "sigs.k8s.io/controller-runtime" 7 | 8 | "github.com/redhat-developer/rhdh-operator/pkg/platform" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/client-go/discovery" 11 | "k8s.io/client-go/kubernetes" 12 | ) 13 | 14 | func DetectPlatform() (platform.Platform, error) { 15 | 16 | config := ctrl.GetConfigOrDie() 17 | clientset, err := kubernetes.NewForConfig(config) 18 | if err != nil { 19 | return platform.Default, err 20 | } 21 | 22 | discoveryClient := discovery.NewDiscoveryClient(clientset.RESTClient()) 23 | 24 | // Check for OpenShift 25 | apiGroups, err := discoveryClient.ServerGroups() 26 | if err != nil { 27 | return platform.Default, err 28 | } 29 | 30 | for _, group := range apiGroups.Groups { 31 | if group.Name == "route.openshift.io" { 32 | return platform.OpenShift, nil 33 | } 34 | } 35 | 36 | // Check for EKS 37 | for _, group := range apiGroups.Groups { 38 | if group.Name == "eks.amazonaws.com" { 39 | return platform.EKS, nil 40 | } 41 | } 42 | 43 | // Check for AKS 44 | namespace, err := clientset.CoreV1().Namespaces().Get(context.TODO(), "kube-system", metav1.GetOptions{}) 45 | if err == nil { 46 | if _, exists := namespace.Labels["kubernetes.azure.com/managed"]; exists { 47 | return platform.AKS, nil 48 | } 49 | } 50 | 51 | // Check for GKE 52 | if err == nil { 53 | if _, exists := namespace.Labels["container.googleapis.com/cluster-name"]; exists { 54 | return platform.GKE, nil 55 | } 56 | } 57 | 58 | // Default to Kubernetes 59 | return platform.Kubernetes, nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/controller/plugin-deps.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/redhat-developer/rhdh-operator/pkg/model" 10 | "k8s.io/apimachinery/pkg/types" 11 | 12 | "sigs.k8s.io/controller-runtime/pkg/log" 13 | 14 | "k8s.io/utils/ptr" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | ) 17 | 18 | func (r *BackstageReconciler) applyPluginDeps(ctx context.Context, nsName types.NamespacedName, plugins model.DynamicPlugins) error { 19 | 20 | lg := log.FromContext(ctx) 21 | 22 | objects, err := model.GetPluginDeps(nsName.Name, nsName.Namespace, plugins) 23 | if err != nil { 24 | return fmt.Errorf("failed to get plugin dependencies: %w", err) 25 | } 26 | 27 | // Process the objects as needed 28 | var errs []error 29 | for _, obj := range objects { 30 | // Apply the unstructured object 31 | lg.V(1).Info("apply plugin dependency: ", "name", obj.GetName(), "kind", obj.GetKind(), "namespace", obj.GetNamespace()) 32 | 33 | // Set the namespace if not set 34 | if obj.GetNamespace() == "" { 35 | obj.SetNamespace(nsName.Namespace) 36 | } 37 | 38 | if err := r.Patch(ctx, obj, client.Apply, &client.PatchOptions{FieldManager: BackstageFieldManager, Force: ptr.To(true)}); err != nil { 39 | errs = append(errs, err) 40 | } 41 | } 42 | 43 | if len(errs) > 0 { 44 | return combineErrors(errs) 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func combineErrors(errs []error) error { 51 | var sb strings.Builder 52 | for _, err := range errs { 53 | sb.WriteString(err.Error() + "\n") 54 | } 55 | return errors.New(sb.String()) 56 | } 57 | -------------------------------------------------------------------------------- /internal/controller/preprocessor_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "testing" 8 | 9 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 10 | "github.com/redhat-developer/rhdh-operator/pkg/model" 11 | 12 | "github.com/stretchr/testify/assert" 13 | corev1 "k8s.io/api/core/v1" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/types" 16 | ) 17 | 18 | func updateConfigMap(t *testing.T) BackstageReconciler { 19 | ctx := context.TODO() 20 | 21 | bs := bsv1.Backstage{ 22 | ObjectMeta: metav1.ObjectMeta{ 23 | Name: "bs1", 24 | Namespace: "ns1", 25 | }, 26 | Spec: bsv1.BackstageSpec{ 27 | Application: &bsv1.Application{ 28 | AppConfig: &bsv1.AppConfig{ 29 | ConfigMaps: []bsv1.FileObjectRef{{Name: "cm1"}}, 30 | }, 31 | }, 32 | }, 33 | } 34 | 35 | cm := corev1.ConfigMap{} 36 | cm.Name = "cm1" 37 | 38 | rc := BackstageReconciler{ 39 | Client: NewMockClient(), 40 | } 41 | 42 | assert.NoError(t, rc.Create(ctx, &cm)) 43 | 44 | // reconcile 45 | extConf, err := rc.preprocessSpec(ctx, bs) 46 | assert.NoError(t, err) 47 | 48 | oldHash := extConf.WatchingHash 49 | 50 | // Update ConfigMap with new data 51 | err = rc.Get(ctx, types.NamespacedName{Namespace: "ns1", Name: "cm1"}, &cm) 52 | assert.NoError(t, err) 53 | cm.Data = map[string]string{"key": "value"} 54 | err = rc.Update(ctx, &cm) 55 | assert.NoError(t, err) 56 | 57 | // reconcile again 58 | extConf, err = rc.preprocessSpec(ctx, bs) 59 | assert.NoError(t, err) 60 | 61 | assert.NotEqual(t, oldHash, extConf.WatchingHash) 62 | 63 | return rc 64 | } 65 | 66 | func TestExtConfigChanged(t *testing.T) { 67 | 68 | ctx := context.TODO() 69 | cm := corev1.ConfigMap{} 70 | 71 | rc := updateConfigMap(t) 72 | err := rc.Get(ctx, types.NamespacedName{Namespace: "ns1", Name: "cm1"}, &cm) 73 | assert.NoError(t, err) 74 | // true : Backstage will be reconciled 75 | assert.Equal(t, "true", cm.Labels[model.ExtConfigSyncLabel]) 76 | 77 | err = os.Setenv(AutoSyncEnvVar, "false") 78 | assert.NoError(t, err) 79 | 80 | rc = updateConfigMap(t) 81 | err = rc.Get(ctx, types.NamespacedName{Namespace: "ns1", Name: "cm1"}, &cm) 82 | assert.NoError(t, err) 83 | // false : Backstage will not be reconciled 84 | assert.Equal(t, "false", cm.Labels[model.ExtConfigSyncLabel]) 85 | 86 | } 87 | 88 | // TestExtConfigChanged tests if concatData returns the same data when the order of the keys is different 89 | func TestExtConcatData(t *testing.T) { 90 | cm := corev1.ConfigMap{} 91 | 92 | cm.Data = map[string]string{"key1": "value1", "key2": "value2", "key3": "value3", "key4": "value4"} 93 | original := []byte("original") 94 | data1 := concatData(original, &cm) 95 | 96 | cm.Data = map[string]string{"key4": "value4", "key2": "value2", "key3": "value3", "key1": "value1"} 97 | assert.Equal(t, data1, concatData(original, &cm)) 98 | 99 | } 100 | -------------------------------------------------------------------------------- /pkg/model/appconfig.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "golang.org/x/exp/maps" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | 10 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 11 | "github.com/redhat-developer/rhdh-operator/pkg/utils" 12 | 13 | corev1 "k8s.io/api/core/v1" 14 | ) 15 | 16 | type AppConfigFactory struct{} 17 | 18 | // factory method to create App Config object 19 | func (f AppConfigFactory) newBackstageObject() RuntimeObject { 20 | return &AppConfig{} 21 | } 22 | 23 | // structure containing ConfigMap where keys are Backstage ConfigApp file names and vaues are contents of the files 24 | // Mount path is a patch to the follder to place the files to 25 | type AppConfig struct { 26 | ConfigMap *corev1.ConfigMap 27 | } 28 | 29 | func init() { 30 | registerConfig("app-config.yaml", AppConfigFactory{}, false) 31 | } 32 | 33 | func AppConfigDefaultName(backstageName string) string { 34 | return utils.GenerateRuntimeObjectName(backstageName, "backstage-appconfig") 35 | } 36 | 37 | func addAppConfigsFromSpec(spec bsv1.BackstageSpec, model *BackstageModel) error { 38 | 39 | if spec.Application == nil || spec.Application.AppConfig == nil || spec.Application.AppConfig.ConfigMaps == nil { 40 | return nil 41 | } 42 | 43 | for _, specCm := range spec.Application.AppConfig.ConfigMaps { 44 | mp, wSubpath := model.backstageDeployment.mountPath(specCm.MountPath, specCm.Key, spec.Application.AppConfig.MountPath) 45 | updatePodWithAppConfig(model.backstageDeployment, model.backstageDeployment.container(), specCm.Name, 46 | mp, specCm.Key, wSubpath, model.ExternalConfig.AppConfigKeys[specCm.Name]) 47 | } 48 | return nil 49 | } 50 | 51 | // implementation of RuntimeObject interface 52 | func (b *AppConfig) Object() runtime.Object { 53 | return b.ConfigMap 54 | } 55 | 56 | // implementation of RuntimeObject interface 57 | func (b *AppConfig) setObject(obj runtime.Object) { 58 | b.ConfigMap = nil 59 | if obj != nil { 60 | b.ConfigMap = obj.(*corev1.ConfigMap) 61 | } 62 | } 63 | 64 | // implementation of RuntimeObject interface 65 | func (b *AppConfig) EmptyObject() client.Object { 66 | return &corev1.ConfigMap{} 67 | } 68 | 69 | // implementation of RuntimeObject interface 70 | func (b *AppConfig) addToModel(model *BackstageModel, _ bsv1.Backstage) (bool, error) { 71 | if b.ConfigMap != nil { 72 | model.appConfig = b 73 | model.setRuntimeObject(b) 74 | return true, nil 75 | } 76 | return false, nil 77 | } 78 | 79 | // implementation of RuntimeObject interface 80 | func (b *AppConfig) updateAndValidate(m *BackstageModel, backstage bsv1.Backstage) error { 81 | updatePodWithAppConfig(m.backstageDeployment, m.backstageDeployment.container(), b.ConfigMap.Name, m.backstageDeployment.defaultMountPath(), "", true, maps.Keys(b.ConfigMap.Data)) 82 | return nil 83 | } 84 | 85 | func (b *AppConfig) setMetaInfo(backstage bsv1.Backstage, scheme *runtime.Scheme) { 86 | b.ConfigMap.SetName(AppConfigDefaultName(backstage.Name)) 87 | setMetaInfo(b.ConfigMap, backstage, scheme) 88 | } 89 | 90 | // updatePodWithAppConfig contributes to Volumes, container.VolumeMounts and container.Args 91 | func updatePodWithAppConfig(bsd *BackstageDeployment, container *corev1.Container, cmName, mountPath, key string, withSubPath bool, cmData []string) { 92 | bsd.mountFilesFrom([]string{container.Name}, ConfigMapObjectKind, 93 | cmName, mountPath, key, withSubPath, cmData) 94 | 95 | for _, file := range cmData { 96 | if key == "" || key == file { 97 | container.Args = append(container.Args, []string{"--config", filepath.Join(mountPath, file)}...) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /pkg/model/configmapenvs.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 5 | "github.com/redhat-developer/rhdh-operator/pkg/utils" 6 | 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | 9 | "k8s.io/apimachinery/pkg/runtime" 10 | 11 | corev1 "k8s.io/api/core/v1" 12 | ) 13 | 14 | type ConfigMapEnvsFactory struct{} 15 | 16 | func (f ConfigMapEnvsFactory) newBackstageObject() RuntimeObject { 17 | return &ConfigMapEnvs{} 18 | } 19 | 20 | type ConfigMapEnvs struct { 21 | ConfigMap *corev1.ConfigMap 22 | } 23 | 24 | func init() { 25 | registerConfig("configmap-envs.yaml", ConfigMapEnvsFactory{}, false) 26 | } 27 | 28 | func addConfigMapEnvsFromSpec(spec bsv1.BackstageSpec, model *BackstageModel) { 29 | if spec.Application == nil || spec.Application.ExtraEnvs == nil || spec.Application.ExtraEnvs.ConfigMaps == nil { 30 | return 31 | } 32 | 33 | for _, specCm := range spec.Application.ExtraEnvs.ConfigMaps { 34 | model.backstageDeployment.addEnvVarsFrom([]string{BackstageContainerName()}, ConfigMapObjectKind, specCm.Name, specCm.Key) 35 | } 36 | } 37 | 38 | // Object implements RuntimeObject interface 39 | func (p *ConfigMapEnvs) Object() runtime.Object { 40 | return p.ConfigMap 41 | } 42 | 43 | func (p *ConfigMapEnvs) setObject(obj runtime.Object) { 44 | p.ConfigMap = nil 45 | if obj != nil { 46 | p.ConfigMap = obj.(*corev1.ConfigMap) 47 | } 48 | } 49 | 50 | // EmptyObject implements RuntimeObject interface 51 | func (p *ConfigMapEnvs) EmptyObject() client.Object { 52 | return &corev1.ConfigMap{} 53 | } 54 | 55 | // implementation of RuntimeObject interface 56 | func (p *ConfigMapEnvs) addToModel(model *BackstageModel, _ bsv1.Backstage) (bool, error) { 57 | if p.ConfigMap != nil { 58 | model.setRuntimeObject(p) 59 | return true, nil 60 | } 61 | return false, nil 62 | } 63 | 64 | // implementation of RuntimeObject interface 65 | func (p *ConfigMapEnvs) updateAndValidate(m *BackstageModel, _ bsv1.Backstage) error { 66 | m.backstageDeployment.addEnvVarsFrom([]string{BackstageContainerName()}, ConfigMapObjectKind, 67 | p.ConfigMap.Name, "") 68 | return nil 69 | } 70 | 71 | // implementation of RuntimeObject interface 72 | func (p *ConfigMapEnvs) setMetaInfo(backstage bsv1.Backstage, scheme *runtime.Scheme) { 73 | p.ConfigMap.SetName(utils.GenerateRuntimeObjectName(backstage.Name, "backstage-envs")) 74 | setMetaInfo(p.ConfigMap, backstage, scheme) 75 | } 76 | -------------------------------------------------------------------------------- /pkg/model/configmapenvs_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/redhat-developer/rhdh-operator/pkg/platform" 8 | 9 | "k8s.io/utils/ptr" 10 | 11 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 12 | 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestDefaultConfigMapEnvFrom(t *testing.T) { 19 | 20 | bs := bsv1.Backstage{ 21 | ObjectMeta: metav1.ObjectMeta{ 22 | Name: "bs", 23 | Namespace: "ns123", 24 | }, 25 | Spec: bsv1.BackstageSpec{ 26 | Database: &bsv1.Database{ 27 | EnableLocalDb: ptr.To(false), 28 | }, 29 | }, 30 | } 31 | 32 | testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("configmap-envs.yaml", "raw-cm-envs.yaml") 33 | 34 | model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) 35 | 36 | assert.NoError(t, err) 37 | assert.NotNil(t, model) 38 | 39 | bscontainer := model.backstageDeployment.container() 40 | assert.NotNil(t, bscontainer) 41 | 42 | assert.Equal(t, 1, len(bscontainer.EnvFrom)) 43 | assert.Equal(t, 0, len(bscontainer.Env)) 44 | 45 | } 46 | 47 | func TestSpecifiedConfigMapEnvs(t *testing.T) { 48 | 49 | bs := bsv1.Backstage{ 50 | ObjectMeta: metav1.ObjectMeta{ 51 | Name: "bs", 52 | Namespace: "ns123", 53 | }, 54 | Spec: bsv1.BackstageSpec{ 55 | Application: &bsv1.Application{ 56 | ExtraEnvs: &bsv1.ExtraEnvs{ 57 | ConfigMaps: []bsv1.EnvObjectRef{}, 58 | }, 59 | }, 60 | }, 61 | } 62 | 63 | bs.Spec.Application.ExtraEnvs.ConfigMaps = append(bs.Spec.Application.ExtraEnvs.ConfigMaps, 64 | bsv1.EnvObjectRef{Name: "mapName", Key: "ENV1"}) 65 | 66 | testObj := createBackstageTest(bs).withDefaultConfig(true) 67 | 68 | testObj.externalConfig.ExtraEnvConfigMapKeys = map[string]DataObjectKeys{} 69 | testObj.externalConfig.ExtraEnvConfigMapKeys["mapName"] = NewDataObjectKeys(map[string]string{"mapName": "ENV1"}, nil) 70 | 71 | model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) 72 | 73 | assert.NoError(t, err) 74 | assert.NotNil(t, model) 75 | 76 | bscontainer := model.backstageDeployment.container() 77 | assert.NotNil(t, bscontainer) 78 | assert.Equal(t, 1, len(bscontainer.Env)) 79 | 80 | assert.NotNil(t, bscontainer.Env[0]) 81 | assert.Equal(t, "ENV1", bscontainer.Env[0].ValueFrom.ConfigMapKeyRef.Key) 82 | 83 | } 84 | -------------------------------------------------------------------------------- /pkg/model/configmapfiles.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "golang.org/x/exp/maps" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | 8 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 9 | "github.com/redhat-developer/rhdh-operator/pkg/utils" 10 | 11 | corev1 "k8s.io/api/core/v1" 12 | ) 13 | 14 | type ConfigMapFilesFactory struct{} 15 | 16 | func (f ConfigMapFilesFactory) newBackstageObject() RuntimeObject { 17 | return &ConfigMapFiles{} 18 | } 19 | 20 | type ConfigMapFiles struct { 21 | ConfigMap *corev1.ConfigMap 22 | } 23 | 24 | func init() { 25 | registerConfig("configmap-files.yaml", ConfigMapFilesFactory{}, false) 26 | } 27 | 28 | func addConfigMapFilesFromSpec(spec bsv1.BackstageSpec, model *BackstageModel) error { 29 | if spec.Application == nil || spec.Application.ExtraFiles == nil || spec.Application.ExtraFiles.ConfigMaps == nil { 30 | return nil 31 | } 32 | 33 | for _, specCm := range spec.Application.ExtraFiles.ConfigMaps { 34 | 35 | mp, wSubpath := model.backstageDeployment.mountPath(specCm.MountPath, specCm.Key, spec.Application.ExtraFiles.MountPath) 36 | keys := model.ExternalConfig.ExtraFileConfigMapKeys[specCm.Name].All() 37 | model.backstageDeployment.mountFilesFrom([]string{BackstageContainerName()}, ConfigMapObjectKind, 38 | specCm.Name, mp, specCm.Key, wSubpath, keys) 39 | } 40 | return nil 41 | } 42 | 43 | // implementation of RuntimeObject interface 44 | func (p *ConfigMapFiles) Object() runtime.Object { 45 | return p.ConfigMap 46 | } 47 | 48 | func (p *ConfigMapFiles) setObject(obj runtime.Object) { 49 | p.ConfigMap = nil 50 | if obj != nil { 51 | p.ConfigMap = obj.(*corev1.ConfigMap) 52 | } 53 | } 54 | 55 | // implementation of RuntimeObject interface 56 | func (p *ConfigMapFiles) EmptyObject() client.Object { 57 | return &corev1.ConfigMap{} 58 | } 59 | 60 | // implementation of RuntimeObject interface 61 | func (p *ConfigMapFiles) addToModel(model *BackstageModel, _ bsv1.Backstage) (bool, error) { 62 | if p.ConfigMap != nil { 63 | model.setRuntimeObject(p) 64 | return true, nil 65 | } 66 | return false, nil 67 | } 68 | 69 | // implementation of RuntimeObject interface 70 | func (p *ConfigMapFiles) updateAndValidate(m *BackstageModel, _ bsv1.Backstage) error { 71 | 72 | keys := append(maps.Keys(p.ConfigMap.Data), maps.Keys(p.ConfigMap.BinaryData)...) 73 | m.backstageDeployment.mountFilesFrom([]string{BackstageContainerName()}, ConfigMapObjectKind, 74 | p.ConfigMap.Name, m.backstageDeployment.defaultMountPath(), "", true, keys) 75 | 76 | return nil 77 | } 78 | 79 | func (p *ConfigMapFiles) setMetaInfo(backstage bsv1.Backstage, scheme *runtime.Scheme) { 80 | p.ConfigMap.SetName(utils.GenerateRuntimeObjectName(backstage.Name, "backstage-files")) 81 | setMetaInfo(p.ConfigMap, backstage, scheme) 82 | } 83 | -------------------------------------------------------------------------------- /pkg/model/db-secret.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strconv" 5 | 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | 8 | "k8s.io/apimachinery/pkg/runtime" 9 | 10 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 11 | "github.com/redhat-developer/rhdh-operator/pkg/utils" 12 | 13 | corev1 "k8s.io/api/core/v1" 14 | ) 15 | 16 | type DbSecretFactory struct{} 17 | 18 | func (f DbSecretFactory) newBackstageObject() RuntimeObject { 19 | return &DbSecret{} 20 | } 21 | 22 | type DbSecret struct { 23 | secret *corev1.Secret 24 | } 25 | 26 | func init() { 27 | registerConfig("db-secret.yaml", DbSecretFactory{}, false) 28 | } 29 | 30 | func DbSecretDefaultName(backstageName string) string { 31 | return utils.GenerateRuntimeObjectName(backstageName, "backstage-psql-secret") 32 | } 33 | 34 | // implementation of RuntimeObject interface 35 | func (b *DbSecret) Object() runtime.Object { 36 | return b.secret 37 | } 38 | 39 | // implementation of RuntimeObject interface 40 | func (b *DbSecret) setObject(obj runtime.Object) { 41 | b.secret = nil 42 | if obj != nil { 43 | b.secret = obj.(*corev1.Secret) 44 | } 45 | } 46 | 47 | // implementation of RuntimeObject interface 48 | func (b *DbSecret) addToModel(model *BackstageModel, backstage bsv1.Backstage) (bool, error) { 49 | 50 | // do not add if specified 51 | if backstage.Spec.IsAuthSecretSpecified() { 52 | return false, nil 53 | } 54 | 55 | if b.secret != nil && model.localDbEnabled { 56 | model.setRuntimeObject(b) 57 | model.LocalDbSecret = b 58 | return true, nil 59 | } 60 | 61 | return false, nil 62 | } 63 | 64 | // implementation of RuntimeObject interface 65 | func (b *DbSecret) EmptyObject() client.Object { 66 | return &corev1.Secret{} 67 | } 68 | 69 | // implementation of RuntimeObject interface 70 | func (b *DbSecret) updateAndValidate(model *BackstageModel, backstage bsv1.Backstage) error { 71 | 72 | pswd, _ := utils.GeneratePassword(24) 73 | 74 | service := model.LocalDbService 75 | 76 | b.secret.StringData = map[string]string{ 77 | "POSTGRES_PASSWORD": pswd, 78 | "POSTGRESQL_ADMIN_PASSWORD": pswd, 79 | "POSTGRES_USER": "postgres", 80 | "POSTGRES_HOST": service.service.GetName(), 81 | "POSTGRES_PORT": strconv.FormatInt(int64(service.service.Spec.Ports[0].Port), 10), 82 | } 83 | 84 | return nil 85 | } 86 | 87 | func (b *DbSecret) setMetaInfo(backstage bsv1.Backstage, scheme *runtime.Scheme) { 88 | b.secret.SetName(DbSecretDefaultName(backstage.Name)) 89 | setMetaInfo(b.secret, backstage, scheme) 90 | } 91 | -------------------------------------------------------------------------------- /pkg/model/db-secret_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/redhat-developer/rhdh-operator/pkg/platform" 9 | 10 | "k8s.io/utils/ptr" 11 | 12 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 13 | 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | var dbSecretBackstage = &bsv1.Backstage{ 20 | ObjectMeta: metav1.ObjectMeta{ 21 | Name: "bs", 22 | Namespace: "ns123", 23 | }, 24 | Spec: bsv1.BackstageSpec{ 25 | Database: &bsv1.Database{ 26 | EnableLocalDb: ptr.To(false), 27 | }, 28 | }, 29 | } 30 | 31 | func TestEmptyDbSecret(t *testing.T) { 32 | 33 | bs := *dbSecretBackstage.DeepCopy() 34 | 35 | // expected generatePassword = false (default db-secret defined) will come from preprocess 36 | testObj := createBackstageTest(bs).withDefaultConfig(true).withLocalDb().addToDefaultConfig("db-secret.yaml", "db-empty-secret.yaml") 37 | 38 | model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) 39 | 40 | assert.NoError(t, err) 41 | assert.NotNil(t, model.LocalDbSecret) 42 | assert.Equal(t, fmt.Sprintf("backstage-psql-secret-%s", bs.Name), model.LocalDbSecret.secret.Name) 43 | 44 | dbss := model.localDbStatefulSet 45 | assert.NotNil(t, dbss) 46 | assert.Equal(t, 1, len(dbss.container().EnvFrom)) 47 | 48 | assert.Equal(t, model.LocalDbSecret.secret.Name, dbss.container().EnvFrom[0].SecretRef.Name) 49 | } 50 | 51 | func TestDefaultWithGeneratedSecrets(t *testing.T) { 52 | bs := *dbSecretBackstage.DeepCopy() 53 | 54 | // expected generatePassword = true (no db-secret defined) will come from preprocess 55 | testObj := createBackstageTest(bs).withDefaultConfig(true).withLocalDb().addToDefaultConfig("db-secret.yaml", "db-generated-secret.yaml") 56 | 57 | model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) 58 | 59 | assert.NoError(t, err) 60 | assert.Equal(t, fmt.Sprintf("backstage-psql-secret-%s", bs.Name), model.LocalDbSecret.secret.Name) 61 | //should be generated 62 | // assert.NotEmpty(t, model.LocalDbSecret.secret.StringData["POSTGRES_USER"]) 63 | // assert.NotEmpty(t, model.LocalDbSecret.secret.StringData["POSTGRES_PASSWORD"]) 64 | 65 | dbss := model.localDbStatefulSet 66 | assert.NotNil(t, dbss) 67 | assert.Equal(t, 1, len(dbss.container().EnvFrom)) 68 | assert.Equal(t, model.LocalDbSecret.secret.Name, dbss.container().EnvFrom[0].SecretRef.Name) 69 | } 70 | 71 | func TestSpecifiedSecret(t *testing.T) { 72 | bs := *dbSecretBackstage.DeepCopy() 73 | bs.Spec.Database.AuthSecretName = "custom-db-secret" 74 | 75 | // expected generatePassword = false (db-secret defined in the spec) will come from preprocess 76 | testObj := createBackstageTest(bs).withDefaultConfig(true).withLocalDb().addToDefaultConfig("db-secret.yaml", "db-generated-secret.yaml") 77 | 78 | model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) 79 | 80 | assert.NoError(t, err) 81 | assert.Nil(t, model.LocalDbSecret) 82 | 83 | assert.Equal(t, bs.Spec.Database.AuthSecretName, model.localDbStatefulSet.container().EnvFrom[0].SecretRef.Name) 84 | assert.Equal(t, bs.Spec.Database.AuthSecretName, model.backstageDeployment.container().EnvFrom[0].SecretRef.Name) 85 | } 86 | -------------------------------------------------------------------------------- /pkg/model/db-service.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | 8 | "k8s.io/apimachinery/pkg/runtime" 9 | 10 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 11 | "github.com/redhat-developer/rhdh-operator/pkg/utils" 12 | 13 | corev1 "k8s.io/api/core/v1" 14 | ) 15 | 16 | type DbServiceFactory struct{} 17 | 18 | func (f DbServiceFactory) newBackstageObject() RuntimeObject { 19 | return &DbService{} 20 | } 21 | 22 | type DbService struct { 23 | service *corev1.Service 24 | } 25 | 26 | func init() { 27 | registerConfig("db-service.yaml", DbServiceFactory{}, false) 28 | } 29 | 30 | func DbServiceName(backstageName string) string { 31 | return utils.GenerateRuntimeObjectName(backstageName, "backstage-psql") 32 | } 33 | 34 | // implementation of RuntimeObject interface 35 | func (b *DbService) Object() runtime.Object { 36 | return b.service 37 | } 38 | 39 | func (b *DbService) setObject(obj runtime.Object) { 40 | b.service = nil 41 | if obj != nil { 42 | b.service = obj.(*corev1.Service) 43 | } 44 | } 45 | 46 | // implementation of RuntimeObject interface 47 | func (b *DbService) addToModel(model *BackstageModel, _ bsv1.Backstage) (bool, error) { 48 | if b.service == nil { 49 | if model.localDbEnabled { 50 | return false, fmt.Errorf("LocalDb Service not initialized, make sure there is db-service.yaml in default or raw configuration") 51 | } 52 | return false, nil 53 | } else { 54 | if !model.localDbEnabled { 55 | return false, nil 56 | } 57 | } 58 | 59 | // force this service to be headless even if it is not set in the original config 60 | b.service.Spec.ClusterIP = corev1.ClusterIPNone 61 | 62 | model.LocalDbService = b 63 | model.setRuntimeObject(b) 64 | 65 | return true, nil 66 | } 67 | 68 | // implementation of RuntimeObject interface 69 | func (b *DbService) EmptyObject() client.Object { 70 | return &corev1.Service{} 71 | } 72 | 73 | // implementation of RuntimeObject interface 74 | func (b *DbService) updateAndValidate(_ *BackstageModel, _ bsv1.Backstage) error { 75 | return nil 76 | } 77 | 78 | func (b *DbService) setMetaInfo(backstage bsv1.Backstage, scheme *runtime.Scheme) { 79 | b.service.SetName(DbServiceName(backstage.Name)) 80 | utils.GenerateLabel(&b.service.Spec.Selector, BackstageAppLabel, utils.BackstageDbAppLabelValue(backstage.Name)) 81 | setMetaInfo(b.service, backstage, scheme) 82 | } 83 | -------------------------------------------------------------------------------- /pkg/model/db-statefulset_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/redhat-developer/rhdh-operator/pkg/platform" 9 | 10 | corev1 "k8s.io/api/core/v1" 11 | 12 | "k8s.io/utils/ptr" 13 | 14 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 15 | 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | 18 | "github.com/stretchr/testify/assert" 19 | ) 20 | 21 | var dbStatefulSetBackstage = &bsv1.Backstage{ 22 | ObjectMeta: metav1.ObjectMeta{ 23 | Name: "bs", 24 | Namespace: "ns123", 25 | }, 26 | Spec: bsv1.BackstageSpec{ 27 | Database: &bsv1.Database{}, 28 | Application: &bsv1.Application{}, 29 | }, 30 | } 31 | 32 | // test default StatefulSet 33 | func TestDefault(t *testing.T) { 34 | bs := *dbStatefulSetBackstage.DeepCopy() 35 | testObj := createBackstageTest(bs).withDefaultConfig(true) 36 | 37 | model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) 38 | assert.NoError(t, err) 39 | 40 | assert.Equal(t, model.LocalDbService.service.Name, model.localDbStatefulSet.statefulSet.Spec.ServiceName) 41 | assert.Equal(t, corev1.ClusterIPNone, model.LocalDbService.service.Spec.ClusterIP) 42 | } 43 | 44 | // It tests the overriding image feature 45 | func TestOverrideDbImage(t *testing.T) { 46 | bs := *dbStatefulSetBackstage.DeepCopy() 47 | 48 | bs.Spec.Database.EnableLocalDb = ptr.To(false) 49 | 50 | testObj := createBackstageTest(bs).withDefaultConfig(true). 51 | addToDefaultConfig("db-statefulset.yaml", "janus-db-statefulset.yaml").withLocalDb() 52 | 53 | _ = os.Setenv(LocalDbImageEnvVar, "dummy") 54 | 55 | model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) 56 | assert.NoError(t, err) 57 | 58 | assert.Equal(t, "dummy", model.localDbStatefulSet.statefulSet.Spec.Template.Spec.Containers[0].Image) 59 | } 60 | 61 | // test bs.Spec.Application.ImagePullSecrets shared with StatefulSet 62 | func TestImagePullSecretSpec(t *testing.T) { 63 | bs := *dbStatefulSetBackstage.DeepCopy() 64 | bs.Spec.Application.ImagePullSecrets = []string{"my-secret1", "my-secret2"} 65 | 66 | testObj := createBackstageTest(bs).withDefaultConfig(true) 67 | model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) 68 | assert.NoError(t, err) 69 | 70 | assert.Equal(t, 2, len(model.localDbStatefulSet.statefulSet.Spec.Template.Spec.ImagePullSecrets)) 71 | assert.Equal(t, "my-secret1", model.localDbStatefulSet.statefulSet.Spec.Template.Spec.ImagePullSecrets[0].Name) 72 | assert.Equal(t, "my-secret2", model.localDbStatefulSet.statefulSet.Spec.Template.Spec.ImagePullSecrets[1].Name) 73 | 74 | // no image pull secrets specified 75 | bs = *dbStatefulSetBackstage.DeepCopy() 76 | testObj = createBackstageTest(bs).withDefaultConfig(true). 77 | addToDefaultConfig("db-statefulset.yaml", "ips-db-statefulset.yaml") 78 | 79 | model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) 80 | if assert.NoError(t, err) { 81 | // if imagepullsecrets not defined - default used 82 | assert.Equal(t, 2, len(model.localDbStatefulSet.statefulSet.Spec.Template.Spec.ImagePullSecrets)) 83 | assert.Equal(t, "ips-db1", model.localDbStatefulSet.statefulSet.Spec.Template.Spec.ImagePullSecrets[0].Name) 84 | assert.Equal(t, "ips-db2", model.localDbStatefulSet.statefulSet.Spec.Template.Spec.ImagePullSecrets[1].Name) 85 | } 86 | 87 | // empty list of image pull secrets 88 | bs = *dbStatefulSetBackstage.DeepCopy() 89 | bs.Spec.Application.ImagePullSecrets = []string{} 90 | 91 | testObj = createBackstageTest(bs).withDefaultConfig(true). 92 | addToDefaultConfig("db-statefulset.yaml", "ips-db-statefulset.yaml") 93 | 94 | model, err = InitObjects(context.TODO(), bs, testObj.externalConfig, platform.OpenShift, testObj.scheme) 95 | if assert.NoError(t, err) { 96 | assert.Equal(t, 0, len(model.localDbStatefulSet.statefulSet.Spec.Template.Spec.ImagePullSecrets)) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /pkg/model/externalconfig.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "golang.org/x/exp/maps" 5 | corev1 "k8s.io/api/core/v1" 6 | ) 7 | 8 | const ExtConfigSyncLabel = "rhdh.redhat.com/ext-config-sync" 9 | const BackstageNameAnnotation = "rhdh.redhat.com/backstage-name" 10 | 11 | type ExternalConfig struct { 12 | RawConfig map[string]string 13 | DynamicPlugins corev1.ConfigMap 14 | AppConfigKeys map[string][]string 15 | ExtraFileConfigMapKeys map[string]DataObjectKeys 16 | ExtraFileSecretKeys map[string]DataObjectKeys 17 | ExtraEnvConfigMapKeys map[string]DataObjectKeys 18 | ExtraEnvSecretKeys map[string]DataObjectKeys 19 | ExtraPvcKeys []string 20 | 21 | OpenShiftIngressDomain string 22 | 23 | WatchingHash string 24 | } 25 | 26 | func NewExternalConfig() ExternalConfig { 27 | 28 | return ExternalConfig{ 29 | RawConfig: map[string]string{}, 30 | DynamicPlugins: corev1.ConfigMap{}, 31 | AppConfigKeys: map[string][]string{}, 32 | ExtraFileConfigMapKeys: map[string]DataObjectKeys{}, 33 | ExtraFileSecretKeys: map[string]DataObjectKeys{}, 34 | ExtraEnvConfigMapKeys: map[string]DataObjectKeys{}, 35 | ExtraEnvSecretKeys: map[string]DataObjectKeys{}, 36 | ExtraPvcKeys: []string{}, 37 | 38 | WatchingHash: "", 39 | } 40 | } 41 | 42 | type DataObjectKeys struct { 43 | StringDataKey []string 44 | BinaryDataKey []string 45 | } 46 | 47 | func NewDataObjectKeys(stringData map[string]string, binaryData map[string][]byte) DataObjectKeys { 48 | return DataObjectKeys{ 49 | StringDataKey: maps.Keys(stringData), 50 | BinaryDataKey: maps.Keys(binaryData), 51 | } 52 | } 53 | 54 | func (k DataObjectKeys) All() []string { 55 | return append(k.StringDataKey, k.BinaryDataKey...) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/model/interfaces.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 5 | 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | 8 | "k8s.io/apimachinery/pkg/runtime" 9 | ) 10 | 11 | // Registered Object configuring Backstage runtime model 12 | type ObjectConfig struct { 13 | // Factory to create the object 14 | ObjectFactory ObjectFactory 15 | // Unique key identifying the "kind" of Object which also is the name of config file. 16 | // For example: "deployment.yaml" containing configuration of Backstage Deployment 17 | Key string 18 | // Single or multiple object 19 | Multiple bool 20 | } 21 | 22 | // Interface for Runtime Objects factory method 23 | type ObjectFactory interface { 24 | newBackstageObject() RuntimeObject 25 | } 26 | 27 | // Abstraction for the model Backstage object taking part in deployment 28 | type RuntimeObject interface { 29 | // Object underlying Kubernetes object 30 | Object() runtime.Object 31 | // setObject sets object 32 | setObject(object runtime.Object) 33 | // EmptyObject an empty object: the same type as Object if Object is client.Object or Item type of Multiobject 34 | EmptyObject() client.Object 35 | // adds runtime object to the model 36 | // returns false if the object was not added to the model (not configured) 37 | addToModel(model *BackstageModel, backstage bsv1.Backstage) (bool, error) 38 | // at this stage all the information is added to the model 39 | // this step is for updating the final references and validate the object 40 | updateAndValidate(model *BackstageModel, backstage bsv1.Backstage) error 41 | // sets object name, labels and other necessary meta information 42 | setMetaInfo(backstage bsv1.Backstage, scheme *runtime.Scheme) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/model/model_tests.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | openshift "github.com/openshift/api/route/v1" 9 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 10 | 11 | "k8s.io/utils/ptr" 12 | 13 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 14 | 15 | "k8s.io/apimachinery/pkg/runtime" 16 | 17 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 18 | ) 19 | 20 | // testBackstageObject it is a helper object to simplify testing model component allowing to customize and isolate testing configuration 21 | // usual sequence of creating testBackstageObject contains such a steps: 22 | // createBackstageTest(bsv1.Backstage). 23 | // withDefaultConfig(useDef bool) 24 | // addToDefaultConfig(key, fileName) 25 | type testBackstageObject struct { 26 | backstage bsv1.Backstage 27 | externalConfig ExternalConfig 28 | scheme *runtime.Scheme 29 | } 30 | 31 | // initialises testBackstageObject object 32 | func createBackstageTest(bs bsv1.Backstage) *testBackstageObject { 33 | ec := ExternalConfig{ 34 | RawConfig: map[string]string{}, 35 | //AppConfigs: map[string]corev1.ConfigMap{}, 36 | //ExtraFileConfigMaps: map[string]corev1.ConfigMap{}, 37 | //ExtraEnvConfigMaps: map[string]corev1.ConfigMap{}, 38 | } 39 | b := &testBackstageObject{backstage: bs, externalConfig: ec, scheme: runtime.NewScheme()} 40 | utilruntime.Must(bsv1.AddToScheme(b.scheme)) 41 | utilruntime.Must(clientgoscheme.AddToScheme(b.scheme)) 42 | utilruntime.Must(openshift.Install(b.scheme)) 43 | return b 44 | } 45 | 46 | // enables LocalDB 47 | func (b *testBackstageObject) withLocalDb() *testBackstageObject { 48 | b.backstage.Spec.Database.EnableLocalDb = ptr.To(true) 49 | return b 50 | } 51 | 52 | // tells if object should use default Backstage Deployment/Service configuration from ./testdata/default-config or not 53 | func (b *testBackstageObject) withDefaultConfig(useDef bool) *testBackstageObject { 54 | if useDef { 55 | // here we have default-config folder 56 | _ = os.Setenv("LOCALBIN", "./testdata") 57 | } else { 58 | _ = os.Setenv("LOCALBIN", ".") 59 | } 60 | return b 61 | } 62 | 63 | // adds particular part of configuration pointing to configuration key 64 | // where key is configuration key (such as "deployment.yaml" and fileName is a name of additional conf file in ./testdata 65 | func (b *testBackstageObject) addToDefaultConfig(key string, fileName string) *testBackstageObject { 66 | 67 | yaml, err := readTestYamlFile(fileName) 68 | if err != nil { 69 | panic(err) 70 | } 71 | 72 | b.externalConfig.RawConfig[key] = string(yaml) 73 | 74 | return b 75 | } 76 | 77 | // reads file from ./testdata 78 | func readTestYamlFile(name string) ([]byte, error) { 79 | 80 | b, err := os.ReadFile(filepath.Join("testdata", name)) // #nosec G304, path is constructed internally 81 | if err != nil { 82 | return nil, fmt.Errorf("failed to read YAML file: %w", err) 83 | } 84 | return b, nil 85 | } 86 | -------------------------------------------------------------------------------- /pkg/model/multiobject/multiobject.go: -------------------------------------------------------------------------------- 1 | package multiobject 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/runtime" 5 | "k8s.io/apimachinery/pkg/runtime/schema" 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | // MultiObject implements runtime.Object interface to make it used in the model along with client.Object 10 | type MultiObject struct { 11 | ObjectKind schema.ObjectKind 12 | Items []client.Object 13 | } 14 | 15 | func (m *MultiObject) GetObjectKind() schema.ObjectKind { 16 | return m.ObjectKind 17 | } 18 | 19 | func (m *MultiObject) DeepCopyObject() runtime.Object { 20 | panic("DeepCopyObject for MultiObject is not implemented") 21 | } 22 | -------------------------------------------------------------------------------- /pkg/model/plugin_deps.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/redhat-developer/rhdh-operator/pkg/utils" 10 | 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | ) 13 | 14 | func GetPluginDeps(bsName, bsNamespace string, plugins DynamicPlugins) ([]*unstructured.Unstructured, error) { 15 | dir := filepath.Join(os.Getenv("LOCALBIN"), "plugin-deps") 16 | pdeps, err := plugins.Dependencies() 17 | if err != nil { 18 | return nil, fmt.Errorf("failed to get plugin dependencies: %w", err) 19 | } 20 | 21 | //get refs from enabled 22 | var refs []string 23 | for _, dep := range pdeps { 24 | if dep.Ref != "" { 25 | refs = append(refs, dep.Ref) 26 | } 27 | } 28 | 29 | return ReadPluginDeps(dir, bsName, bsNamespace, refs) 30 | 31 | } 32 | 33 | // ReadPluginDeps reads the plugin dependencies from the specified directory 34 | // and returns a slice of unstructured.Unstructured objects. 35 | func ReadPluginDeps(rootDir, bsName, bsNamespace string, enabled []string) ([]*unstructured.Unstructured, error) { 36 | 37 | if !utils.DirectoryExists(rootDir) { 38 | return []*unstructured.Unstructured{}, nil 39 | } 40 | 41 | var objects []*unstructured.Unstructured 42 | 43 | // Read the directory tree 44 | files, err := getDepsFiles(rootDir, enabled) 45 | 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | for _, file := range files { 51 | if !utils.IsYamlFile(file) { 52 | continue 53 | } 54 | 55 | // Read file content 56 | content, err := os.ReadFile(filepath.Clean(file)) 57 | if err != nil { 58 | return nil, fmt.Errorf("failed to read file %s: %w", file, err) 59 | } 60 | 61 | // Perform substitutions 62 | modifiedContent := strings.ReplaceAll(string(content), "{{backstage-name}}", bsName) 63 | modifiedContent = strings.ReplaceAll(modifiedContent, "{{backstage-ns}}", bsNamespace) 64 | 65 | // Parse the modified content 66 | objs, err := utils.ReadYamlContent(modifiedContent) 67 | 68 | if err != nil { 69 | return nil, fmt.Errorf("failed to read YAML file %s: %w", file, err) 70 | } 71 | objects = append(objects, objs...) 72 | } 73 | 74 | return objects, nil 75 | } 76 | 77 | func getDepsFiles(root string, enabledPrefixes []string) ([]string, error) { 78 | var files []string 79 | 80 | // Read the directory contents 81 | entries, err := os.ReadDir(root) 82 | if err != nil { 83 | return nil, fmt.Errorf("failed to read directory %s: %w", root, err) 84 | } 85 | 86 | // Iterate over the entries and filter by prefixes 87 | for _, entry := range entries { 88 | if entry.IsDir() { 89 | continue // Skip directories 90 | } 91 | 92 | // Check if the file name starts with any of the enabled prefixes 93 | for _, prefix := range enabledPrefixes { 94 | if strings.HasPrefix(entry.Name(), prefix) { 95 | files = append(files, filepath.Join(root, entry.Name())) 96 | break 97 | } 98 | } 99 | 100 | // TODO add some warning if file is not in the enabled list 101 | } 102 | 103 | return files, nil 104 | } 105 | -------------------------------------------------------------------------------- /pkg/model/plugin_deps_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | corev1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestReadPluginDeps(t *testing.T) { 15 | dir := t.TempDir() 16 | 17 | // Create files in the root directory 18 | file1 := filepath.Join(dir, "sonata.yaml") 19 | file2 := filepath.Join(dir, "otherplugin.yaml") 20 | file3 := filepath.Join(dir, "sonata-config.yaml") 21 | file4 := filepath.Join(dir, "unrelated.txt") 22 | 23 | err := os.WriteFile(file1, []byte("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: sonata"), 0644) 24 | assert.NoError(t, err) 25 | err = os.WriteFile(file2, []byte("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: test2"), 0644) 26 | assert.NoError(t, err) 27 | err = os.WriteFile(file3, []byte("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: sonata"), 0644) 28 | assert.NoError(t, err) 29 | err = os.WriteFile(file4, []byte("some unrelated content"), 0644) 30 | assert.NoError(t, err) 31 | 32 | objects, err := ReadPluginDeps(dir, "", "", []string{"sonata"}) 33 | assert.NoError(t, err) 34 | assert.Len(t, objects, 2) 35 | 36 | // Verify the names of the objects 37 | assert.Equal(t, "sonata", objects[0].GetName()) 38 | assert.Equal(t, "sonata", objects[1].GetName()) 39 | } 40 | 41 | func TestReadPluginDepsSubstitutions(t *testing.T) { 42 | 43 | dir := t.TempDir() 44 | 45 | file1 := filepath.Join(dir, "file1.yaml") 46 | yamlContent := ` 47 | apiVersion: v1 48 | kind: ConfigMap 49 | metadata: 50 | name: {{backstage-name}} 51 | namespace: {{backstage-ns}} 52 | ` 53 | err := os.WriteFile(file1, []byte(yamlContent), 0644) 54 | assert.NoError(t, err) 55 | 56 | bsName := "test-name" 57 | bsNamespace := "test-namespace" 58 | objects, err := ReadPluginDeps(dir, bsName, bsNamespace, []string{"file1"}) 59 | assert.NoError(t, err) 60 | assert.Len(t, objects, 1) 61 | 62 | assert.Equal(t, bsName, objects[0].GetName()) 63 | assert.Equal(t, bsNamespace, objects[0].GetNamespace()) 64 | } 65 | 66 | func TestGetPluginDeps(t *testing.T) { 67 | // Setup temporary directory 68 | tempDir := t.TempDir() 69 | t.Setenv("LOCALBIN", tempDir) 70 | 71 | pluginDepsDir := filepath.Join(tempDir, "plugin-deps") 72 | err := os.Mkdir(pluginDepsDir, 0755) 73 | assert.NoError(t, err) 74 | 75 | // Create mock plugin dependency files 76 | file1 := filepath.Join(pluginDepsDir, "dep1.yaml") 77 | file2 := filepath.Join(pluginDepsDir, "dep2.yaml") 78 | 79 | err = os.WriteFile(file1, []byte(` 80 | apiVersion: v1 81 | kind: ConfigMap 82 | metadata: 83 | name: dep1 84 | `), 0644) 85 | assert.NoError(t, err) 86 | 87 | err = os.WriteFile(file2, []byte(` 88 | apiVersion: v1 89 | kind: ConfigMap 90 | metadata: 91 | name: dep2 92 | `), 0644) 93 | assert.NoError(t, err) 94 | 95 | dynaPlugins := DynamicPlugins{ 96 | ConfigMap: &corev1.ConfigMap{ 97 | ObjectMeta: metav1.ObjectMeta{ 98 | Name: "test-configmap", 99 | }, 100 | Data: map[string]string{ 101 | "dynamic-plugins.yaml": ` 102 | includes: 103 | - dynamic-plugins.default.yaml 104 | plugins: 105 | - package: './dynamic-plugins/dist/test' 106 | disabled: false 107 | dependencies: 108 | - ref: dep1 109 | - ref: dep2 110 | `, 111 | }, 112 | }, 113 | } 114 | 115 | // Call GetPluginDeps 116 | bsName := "test-name" 117 | bsNamespace := "test-namespace" 118 | objects, err := GetPluginDeps(bsName, bsNamespace, dynaPlugins) 119 | assert.NoError(t, err) 120 | assert.Len(t, objects, 2) 121 | 122 | // Verify the returned objects 123 | actualNames := []string{objects[0].GetName(), objects[1].GetName()} 124 | expectedNames := []string{"dep1", "dep2"} 125 | assert.ElementsMatch(t, expectedNames, actualNames) 126 | } 127 | 128 | func TestReadPluginDepsNoFiles(t *testing.T) { 129 | dir := t.TempDir() 130 | 131 | // Call ReadPluginDeps with an empty directory 132 | objects, err := ReadPluginDeps(dir, "", "", []string{"sonata"}) 133 | assert.NoError(t, err) 134 | assert.Len(t, objects, 0) 135 | } 136 | -------------------------------------------------------------------------------- /pkg/model/pvcs.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 8 | "github.com/redhat-developer/rhdh-operator/pkg/model/multiobject" 9 | "github.com/redhat-developer/rhdh-operator/pkg/utils" 10 | 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | 13 | corev1 "k8s.io/api/core/v1" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | ) 16 | 17 | type BackstagePvcsFactory struct{} 18 | 19 | func (f BackstagePvcsFactory) newBackstageObject() RuntimeObject { 20 | return &BackstagePvcs{} 21 | } 22 | 23 | func init() { 24 | registerConfig("pvcs.yaml", BackstagePvcsFactory{}, true) 25 | } 26 | 27 | func addPvcsFromSpec(spec bsv1.BackstageSpec, model *BackstageModel) { 28 | if spec.Application == nil || spec.Application.ExtraFiles == nil || spec.Application.ExtraFiles.Pvcs == nil || len(spec.Application.ExtraFiles.Pvcs) == 0 { 29 | return 30 | } 31 | 32 | for _, pvcSpec := range spec.Application.ExtraFiles.Pvcs { 33 | 34 | subPath := "" 35 | mountPath, wSubpath := model.backstageDeployment.mountPath(pvcSpec.MountPath, "", spec.Application.ExtraFiles.MountPath) 36 | 37 | if wSubpath { 38 | mountPath = filepath.Join(mountPath, pvcSpec.Name) 39 | subPath = utils.ToRFC1123Label(pvcSpec.Name) 40 | } 41 | 42 | addPvc(model.backstageDeployment, pvcSpec.Name, mountPath, subPath, []string{BackstageContainerName()}) 43 | } 44 | } 45 | 46 | type BackstagePvcs struct { 47 | pvcs *multiobject.MultiObject 48 | } 49 | 50 | func PvcsName(backstageName, originalName string) string { 51 | return fmt.Sprintf("%s-%s", utils.GenerateRuntimeObjectName(backstageName, "backstage"), originalName) 52 | } 53 | 54 | func (b *BackstagePvcs) Object() runtime.Object { 55 | return b.pvcs 56 | } 57 | 58 | func (b *BackstagePvcs) setObject(object runtime.Object) { 59 | b.pvcs = object.(*multiobject.MultiObject) 60 | } 61 | 62 | func (b *BackstagePvcs) EmptyObject() client.Object { 63 | return &corev1.PersistentVolumeClaim{} 64 | } 65 | 66 | func (b *BackstagePvcs) addToModel(model *BackstageModel, _ bsv1.Backstage) (bool, error) { 67 | if b.pvcs != nil { 68 | model.setRuntimeObject(b) 69 | return true, nil 70 | } 71 | return false, nil 72 | } 73 | 74 | func (b *BackstagePvcs) updateAndValidate(m *BackstageModel, _ bsv1.Backstage) error { 75 | for _, o := range b.pvcs.Items { 76 | pvc, ok := o.(*corev1.PersistentVolumeClaim) 77 | if !ok { 78 | return fmt.Errorf("payload is not corev1.PersistentVolumeClaim: %T", o) 79 | } 80 | mountPath, subPath, containers := m.backstageDeployment.getDefConfigMountInfo(o) 81 | addPvc(m.backstageDeployment, pvc.Name, mountPath, subPath, containers) 82 | } 83 | return nil 84 | } 85 | 86 | func (b *BackstagePvcs) setMetaInfo(backstage bsv1.Backstage, scheme *runtime.Scheme) { 87 | for _, item := range b.pvcs.Items { 88 | pvc := item.(*corev1.PersistentVolumeClaim) 89 | utils.AddAnnotation(pvc, ConfiguredNameAnnotation, item.GetName()) 90 | pvc.Name = PvcsName(backstage.Name, pvc.Name) 91 | setMetaInfo(pvc, backstage, scheme) 92 | } 93 | } 94 | 95 | func addPvc(bsd *BackstageDeployment, pvcName, mountPath, subPath string, affectedContainers []string) { 96 | 97 | volName := utils.ToRFC1123Label(pvcName) 98 | volSrc := corev1.VolumeSource{ 99 | PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ 100 | ClaimName: pvcName, 101 | }, 102 | } 103 | bsd.deployment.Spec.Template.Spec.Volumes = 104 | append(bsd.deployment.Spec.Template.Spec.Volumes, corev1.Volume{Name: volName, VolumeSource: volSrc}) 105 | for _, c := range affectedContainers { 106 | update := bsd.containerByName(c) 107 | update.VolumeMounts = append(update.VolumeMounts, 108 | corev1.VolumeMount{Name: volName, MountPath: mountPath, SubPath: subPath}) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /pkg/model/secretenvs.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | 6 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 7 | "github.com/redhat-developer/rhdh-operator/pkg/model/multiobject" 8 | "github.com/redhat-developer/rhdh-operator/pkg/utils" 9 | 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | 12 | "k8s.io/apimachinery/pkg/runtime" 13 | 14 | corev1 "k8s.io/api/core/v1" 15 | ) 16 | 17 | const SecretEnvsObjectKey = "secret-envs.yaml" 18 | 19 | type SecretEnvsFactory struct{} 20 | 21 | func (f SecretEnvsFactory) newBackstageObject() RuntimeObject { 22 | return &SecretEnvs{} 23 | } 24 | 25 | type SecretEnvs struct { 26 | secrets *multiobject.MultiObject 27 | } 28 | 29 | func init() { 30 | registerConfig(SecretEnvsObjectKey, SecretEnvsFactory{}, true) 31 | } 32 | 33 | func addSecretEnvsFromSpec(spec bsv1.BackstageSpec, model *BackstageModel) error { 34 | if spec.Application == nil || spec.Application.ExtraEnvs == nil || spec.Application.ExtraEnvs.Secrets == nil { 35 | return nil 36 | } 37 | 38 | for _, specSec := range spec.Application.ExtraEnvs.Secrets { 39 | model.backstageDeployment.addEnvVarsFrom([]string{BackstageContainerName()}, SecretObjectKind, specSec.Name, specSec.Key) 40 | } 41 | return nil 42 | } 43 | 44 | // implementation of RuntimeObject interface 45 | func (p *SecretEnvs) Object() runtime.Object { 46 | return p.secrets 47 | } 48 | 49 | // implementation of RuntimeObject interface 50 | func (p *SecretEnvs) setObject(obj runtime.Object) { 51 | p.secrets = nil 52 | if obj != nil { 53 | // p.Secret = obj.(*corev1.Secret) 54 | p.secrets = obj.(*multiobject.MultiObject) 55 | } 56 | } 57 | 58 | // implementation of RuntimeObject interface 59 | func (p *SecretEnvs) EmptyObject() client.Object { 60 | return &corev1.Secret{} 61 | } 62 | 63 | // implementation of RuntimeObject interface 64 | func (p *SecretEnvs) addToModel(model *BackstageModel, _ bsv1.Backstage) (bool, error) { 65 | if p.secrets != nil { 66 | model.setRuntimeObject(p) 67 | return true, nil 68 | } 69 | return false, nil 70 | } 71 | 72 | // implementation of RuntimeObject interface 73 | func (p *SecretEnvs) updateAndValidate(m *BackstageModel, _ bsv1.Backstage) error { 74 | 75 | for _, item := range p.secrets.Items { 76 | _, ok := item.(*corev1.Secret) 77 | if !ok { 78 | return fmt.Errorf("payload is not corev1.Secret: %T", item) 79 | } 80 | toContainers := utils.FilterContainers(m.backstageDeployment.allContainers(), item.GetAnnotations()[ContainersAnnotation]) 81 | if toContainers == nil { 82 | toContainers = []string{BackstageContainerName()} 83 | } 84 | m.backstageDeployment.addEnvVarsFrom(toContainers, SecretObjectKind, item.GetName(), "") 85 | } 86 | 87 | return nil 88 | } 89 | 90 | // implementation of RuntimeObject interface 91 | func (p *SecretEnvs) setMetaInfo(backstage bsv1.Backstage, scheme *runtime.Scheme) { 92 | for _, item := range p.secrets.Items { 93 | secret := item.(*corev1.Secret) 94 | utils.AddAnnotation(secret, ConfiguredNameAnnotation, item.GetName()) 95 | if len(p.secrets.Items) == 1 { 96 | // keep for backward compatibility 97 | secret.Name = utils.GenerateRuntimeObjectName(backstage.Name, "backstage-envs") 98 | } else { 99 | secret.Name = fmt.Sprintf("%s-%s", utils.GenerateRuntimeObjectName(backstage.Name, "backstage-envs"), secret.Name) 100 | } 101 | setMetaInfo(secret, backstage, scheme) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /pkg/model/secretenvs_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/redhat-developer/rhdh-operator/pkg/platform" 8 | 9 | "github.com/redhat-developer/rhdh-operator/pkg/model/multiobject" 10 | 11 | "k8s.io/utils/ptr" 12 | 13 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 14 | 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | func TestDefaultSecretEnvFrom(t *testing.T) { 21 | 22 | bs := bsv1.Backstage{ 23 | ObjectMeta: metav1.ObjectMeta{ 24 | Name: "bs", 25 | }, 26 | Spec: bsv1.BackstageSpec{ 27 | Database: &bsv1.Database{ 28 | EnableLocalDb: ptr.To(false), 29 | }, 30 | }, 31 | } 32 | 33 | testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig(SecretEnvsObjectKey, "raw-sec-envs.yaml") 34 | 35 | model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) 36 | 37 | assert.NoError(t, err) 38 | assert.NotNil(t, model) 39 | 40 | bscontainer := model.backstageDeployment.container() 41 | assert.NotNil(t, bscontainer) 42 | 43 | assert.Equal(t, 1, len(bscontainer.EnvFrom)) 44 | assert.Equal(t, 0, len(bscontainer.Env)) 45 | 46 | } 47 | 48 | func TestDefaultMultiSecretEnv(t *testing.T) { 49 | 50 | bs := bsv1.Backstage{ 51 | ObjectMeta: metav1.ObjectMeta{ 52 | Name: "bs", 53 | }, 54 | Spec: bsv1.BackstageSpec{ 55 | Database: &bsv1.Database{ 56 | EnableLocalDb: ptr.To(false), 57 | }, 58 | }, 59 | } 60 | 61 | testObj := createBackstageTest(bs).withDefaultConfig(true).addToDefaultConfig("deployment.yaml", "multicontainer-deployment.yaml"). 62 | addToDefaultConfig(SecretEnvsObjectKey, "raw-multi-secret.yaml") 63 | 64 | model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) 65 | 66 | assert.NoError(t, err) 67 | assert.NotNil(t, model) 68 | 69 | assert.Equal(t, 4, len(model.backstageDeployment.allContainers())) 70 | assert.Equal(t, 3, len(model.backstageDeployment.container().EnvFrom)) 71 | assert.Equal(t, 2, len(model.backstageDeployment.containerByName("install-dynamic-plugins").EnvFrom)) 72 | assert.Equal(t, 1, len(model.backstageDeployment.containerByName("another-container").EnvFrom)) 73 | mo := model.getRuntimeObjectByType(&SecretEnvs{}).Object().(*multiobject.MultiObject) 74 | assert.Equal(t, 3, len(mo.Items)) 75 | } 76 | 77 | func TestSpecifiedSecretEnvs(t *testing.T) { 78 | 79 | bs := bsv1.Backstage{ 80 | ObjectMeta: metav1.ObjectMeta{ 81 | Name: "bs", 82 | Namespace: "ns123", 83 | }, 84 | Spec: bsv1.BackstageSpec{ 85 | Application: &bsv1.Application{ 86 | ExtraEnvs: &bsv1.ExtraEnvs{ 87 | Secrets: []bsv1.EnvObjectRef{}, 88 | }, 89 | }, 90 | }, 91 | } 92 | 93 | bs.Spec.Application.ExtraEnvs.Secrets = append(bs.Spec.Application.ExtraEnvs.Secrets, 94 | bsv1.EnvObjectRef{Name: "secName", Key: "ENV1"}) 95 | 96 | testObj := createBackstageTest(bs).withDefaultConfig(true) 97 | testObj.externalConfig.ExtraEnvConfigMapKeys = map[string]DataObjectKeys{} 98 | testObj.externalConfig.ExtraEnvConfigMapKeys["secName"] = NewDataObjectKeys(map[string]string{"secName": "ENV1"}, nil) 99 | 100 | model, err := InitObjects(context.TODO(), bs, testObj.externalConfig, platform.Default, testObj.scheme) 101 | 102 | assert.NoError(t, err) 103 | assert.NotNil(t, model) 104 | 105 | bscontainer := model.backstageDeployment.container() 106 | assert.NotNil(t, bscontainer) 107 | assert.Equal(t, 1, len(bscontainer.Env)) 108 | 109 | assert.NotNil(t, bscontainer.Env[0]) 110 | assert.Equal(t, "ENV1", bscontainer.Env[0].ValueFrom.SecretKeyRef.Key) 111 | 112 | } 113 | -------------------------------------------------------------------------------- /pkg/model/secretfiles.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/redhat-developer/rhdh-operator/pkg/model/multiobject" 7 | 8 | "golang.org/x/exp/maps" 9 | 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | 12 | "k8s.io/apimachinery/pkg/runtime" 13 | 14 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 15 | "github.com/redhat-developer/rhdh-operator/pkg/utils" 16 | 17 | corev1 "k8s.io/api/core/v1" 18 | ) 19 | 20 | const SecretFilesObjectKey = "secret-files.yaml" 21 | 22 | type SecretFilesFactory struct{} 23 | 24 | func (f SecretFilesFactory) newBackstageObject() RuntimeObject { 25 | return &SecretFiles{} 26 | } 27 | 28 | type SecretFiles struct { 29 | secrets *multiobject.MultiObject 30 | } 31 | 32 | func init() { 33 | registerConfig(SecretFilesObjectKey, SecretFilesFactory{}, true) 34 | } 35 | 36 | func addSecretFilesFromSpec(spec bsv1.BackstageSpec, model *BackstageModel) error { 37 | 38 | if spec.Application == nil || spec.Application.ExtraFiles == nil || spec.Application.ExtraFiles.Secrets == nil { 39 | return nil 40 | } 41 | 42 | for _, specSec := range spec.Application.ExtraFiles.Secrets { 43 | 44 | if specSec.MountPath == "" && specSec.Key == "" { 45 | return fmt.Errorf("key is required if defaultMountPath is not specified for secret %s", specSec.Name) 46 | } 47 | mp, wSubpath := model.backstageDeployment.mountPath(specSec.MountPath, specSec.Key, spec.Application.ExtraFiles.MountPath) 48 | keys := model.ExternalConfig.ExtraFileSecretKeys[specSec.Name].All() 49 | model.backstageDeployment.mountFilesFrom([]string{BackstageContainerName()}, SecretObjectKind, 50 | specSec.Name, mp, specSec.Key, wSubpath, keys) 51 | } 52 | return nil 53 | } 54 | 55 | // implementation of RuntimeObject interface 56 | func (p *SecretFiles) Object() runtime.Object { 57 | return p.secrets 58 | } 59 | 60 | // implementation of RuntimeObject interface 61 | func (p *SecretFiles) setObject(obj runtime.Object) { 62 | p.secrets = nil 63 | if obj != nil { 64 | p.secrets = obj.(*multiobject.MultiObject) 65 | } 66 | } 67 | 68 | // implementation of RuntimeObject interface 69 | func (p *SecretFiles) EmptyObject() client.Object { 70 | return &corev1.Secret{} 71 | } 72 | 73 | // implementation of RuntimeObject interface 74 | func (p *SecretFiles) addToModel(model *BackstageModel, _ bsv1.Backstage) (bool, error) { 75 | if p.secrets != nil { 76 | model.setRuntimeObject(p) 77 | return true, nil 78 | } 79 | return false, nil 80 | } 81 | 82 | // implementation of RuntimeObject interface 83 | func (p *SecretFiles) updateAndValidate(m *BackstageModel, _ bsv1.Backstage) error { 84 | 85 | for _, item := range p.secrets.Items { 86 | secret, ok := item.(*corev1.Secret) 87 | if !ok { 88 | return fmt.Errorf("payload is not corev1.Secret: %T", item) 89 | } 90 | 91 | keys := append(maps.Keys(secret.Data), maps.Keys(secret.StringData)...) 92 | mountPath, subPath, containers := m.backstageDeployment.getDefConfigMountInfo(item) 93 | m.backstageDeployment.mountFilesFrom(containers, SecretObjectKind, 94 | item.GetName(), mountPath, "", subPath != "", keys) 95 | } 96 | return nil 97 | } 98 | 99 | // implementation of RuntimeObject interface 100 | func (p *SecretFiles) setMetaInfo(backstage bsv1.Backstage, scheme *runtime.Scheme) { 101 | 102 | for _, item := range p.secrets.Items { 103 | secret := item.(*corev1.Secret) 104 | if len(p.secrets.Items) == 1 { 105 | // keep for backward compatibility 106 | secret.Name = utils.GenerateRuntimeObjectName(backstage.Name, "backstage-files") 107 | } else { 108 | utils.AddAnnotation(secret, ConfiguredNameAnnotation, item.GetName()) 109 | secret.Name = fmt.Sprintf("%s-%s", utils.GenerateRuntimeObjectName(backstage.Name, "backstage-files"), secret.Name) 110 | } 111 | setMetaInfo(secret, backstage, scheme) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /pkg/model/service.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | 8 | "k8s.io/apimachinery/pkg/runtime" 9 | 10 | bsv1 "github.com/redhat-developer/rhdh-operator/api/v1alpha3" 11 | "github.com/redhat-developer/rhdh-operator/pkg/utils" 12 | 13 | corev1 "k8s.io/api/core/v1" 14 | ) 15 | 16 | type BackstageServiceFactory struct{} 17 | 18 | func (f BackstageServiceFactory) newBackstageObject() RuntimeObject { 19 | return &BackstageService{} 20 | } 21 | 22 | type BackstageService struct { 23 | service *corev1.Service 24 | } 25 | 26 | func init() { 27 | registerConfig("service.yaml", BackstageServiceFactory{}, false) 28 | } 29 | 30 | func ServiceName(backstageName string) string { 31 | return utils.GenerateRuntimeObjectName(backstageName, "backstage") 32 | } 33 | 34 | // implementation of RuntimeObject interface 35 | func (b *BackstageService) Object() runtime.Object { 36 | return b.service 37 | } 38 | 39 | func (b *BackstageService) setObject(obj runtime.Object) { 40 | b.service = nil 41 | if obj != nil { 42 | b.service = obj.(*corev1.Service) 43 | } 44 | } 45 | 46 | // implementation of RuntimeObject interface 47 | func (b *BackstageService) addToModel(model *BackstageModel, _ bsv1.Backstage) (bool, error) { 48 | if b.service == nil { 49 | return false, fmt.Errorf("Backstage Service is not initialized, make sure there is service.yaml in default or raw configuration") 50 | } 51 | model.backstageService = b 52 | model.setRuntimeObject(b) 53 | 54 | return true, nil 55 | 56 | } 57 | 58 | // implementation of RuntimeObject interface 59 | func (b *BackstageService) EmptyObject() client.Object { 60 | return &corev1.Service{} 61 | } 62 | 63 | // implementation of RuntimeObject interface 64 | func (b *BackstageService) updateAndValidate(_ *BackstageModel, _ bsv1.Backstage) error { 65 | return nil 66 | } 67 | 68 | func (b *BackstageService) setMetaInfo(backstage bsv1.Backstage, scheme *runtime.Scheme) { 69 | b.service.SetName(ServiceName(backstage.Name)) 70 | utils.GenerateLabel(&b.service.Spec.Selector, BackstageAppLabel, utils.BackstageAppLabelValue(backstage.Name)) 71 | setMetaInfo(b.service, backstage, scheme) 72 | } 73 | -------------------------------------------------------------------------------- /pkg/model/testdata/db-defined-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: postgres-secrets # will be replaced 5 | namespace: backstage 6 | type: Opaque 7 | stringData: 8 | POSTGRES_PASSWORD: admin123 9 | POSTGRES_PORT: "5432" 10 | POSTGRES_USER: postgres 11 | POSTGRESQL_ADMIN_PASSWORD: admin123 12 | POSTGRES_HOST: bs1-db-service #placeholder -db-service -------------------------------------------------------------------------------- /pkg/model/testdata/db-empty-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | -------------------------------------------------------------------------------- /pkg/model/testdata/db-generated-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: postgres-secrets # will be replaced 5 | namespace: backstage 6 | type: Opaque 7 | stringData: 8 | POSTGRES_PASSWORD: "postgres" 9 | POSTGRES_PORT: "5432" 10 | POSTGRES_USER: "postgres" 11 | POSTGRES_HOST: bs1-db-service #placeholder -db-service -------------------------------------------------------------------------------- /pkg/model/testdata/default-config/db-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: # will be replaced 5 | namespace: backstage 6 | type: Opaque 7 | #stringData: 8 | # POSTGRES_PASSWORD: #wrgd5688 #admin123 # leave it empty to make it autogenerated 9 | # POSTGRES_PORT: "5432" 10 | # POSTGRES_USER: postgres 11 | # POSTGRESQL_ADMIN_PASSWORD: admin123 12 | # POSTGRES_HOST: bs1-db-service #placeholder -db-service -------------------------------------------------------------------------------- /pkg/model/testdata/default-config/db-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: backstage-psql # placeholder for 'backstage-psql-' .NOTE: For the time it is static and linked to Secret-> postgres-secrets -> OSTGRES_HOST 5 | spec: 6 | selector: 7 | rhdh.redhat.com/app: backstage-psql-cr1 # placeholder for 'backstage-psql-' 8 | ports: 9 | - port: 5432 10 | -------------------------------------------------------------------------------- /pkg/model/testdata/default-config/db-statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: backstage-psql-cr1 # placeholder for 'backstage-psql-' 5 | spec: 6 | podManagementPolicy: OrderedReady 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | rhdh.redhat.com/app: backstage-psql-cr1 # placeholder for 'backstage-psql-' 11 | serviceName: backstage-psql-cr1-hl # placeholder for 'backstage-psql--hl' 12 | template: 13 | metadata: 14 | labels: 15 | rhdh.redhat.com/app: backstage-psql-cr1 # placeholder for 'backstage-psql-' 16 | name: backstage-db-cr1 # placeholder for 'backstage-psql-' 17 | spec: 18 | automountServiceAccountToken: false 19 | containers: 20 | - env: 21 | - name: POSTGRESQL_PORT_NUMBER 22 | value: "5432" 23 | - name: POSTGRESQL_VOLUME_DIR 24 | value: /var/lib/pgsql/data 25 | - name: PGDATA 26 | value: /var/lib/pgsql/data/userdata 27 | image: quay.io/fedora/postgresql-15:latest 28 | imagePullPolicy: IfNotPresent 29 | securityContext: 30 | runAsNonRoot: true 31 | allowPrivilegeEscalation: false 32 | seccompProfile: 33 | type: RuntimeDefault 34 | capabilities: 35 | drop: 36 | - ALL 37 | livenessProbe: 38 | exec: 39 | command: 40 | - /bin/sh 41 | - -c 42 | - exec pg_isready -U ${POSTGRES_USER} -h 127.0.0.1 -p 5432 43 | failureThreshold: 6 44 | initialDelaySeconds: 30 45 | periodSeconds: 10 46 | successThreshold: 1 47 | timeoutSeconds: 5 48 | name: postgresql 49 | ports: 50 | - containerPort: 5432 51 | name: tcp-postgresql 52 | protocol: TCP 53 | readinessProbe: 54 | exec: 55 | command: 56 | - /bin/sh 57 | - -c 58 | - -e 59 | - | 60 | exec pg_isready -U ${POSTGRES_USER} -h 127.0.0.1 -p 5432 61 | failureThreshold: 6 62 | initialDelaySeconds: 5 63 | periodSeconds: 10 64 | successThreshold: 1 65 | timeoutSeconds: 5 66 | resources: 67 | requests: 68 | cpu: 250m 69 | memory: 256Mi 70 | limits: 71 | cpu: 250m 72 | memory: 1024Mi 73 | ephemeral-storage: 20Mi 74 | volumeMounts: 75 | - mountPath: /dev/shm 76 | name: dshm 77 | - mountPath: /var/lib/pgsql/data 78 | name: data 79 | restartPolicy: Always 80 | securityContext: {} 81 | serviceAccount: default 82 | serviceAccountName: default 83 | volumes: 84 | - emptyDir: 85 | medium: Memory 86 | name: dshm 87 | updateStrategy: 88 | rollingUpdate: 89 | partition: 0 90 | type: RollingUpdate 91 | volumeClaimTemplates: 92 | - apiVersion: v1 93 | kind: PersistentVolumeClaim 94 | metadata: 95 | name: data 96 | spec: 97 | accessModes: 98 | - ReadWriteOnce 99 | resources: 100 | requests: 101 | storage: 1Gi -------------------------------------------------------------------------------- /pkg/model/testdata/default-config/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: backstage 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | backstage.io/app: # placeholder for 'backstage-' 10 | template: 11 | metadata: 12 | labels: 13 | backstage.io/app: # placeholder for 'backstage-' 14 | spec: 15 | containers: 16 | - name: backstage-backend # placeholder for 'backstage-backend' 17 | image: ghcr.io/backstage/backstage 18 | imagePullPolicy: IfNotPresent 19 | ports: 20 | - name: http 21 | containerPort: 7007 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /pkg/model/testdata/default-config/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: backstage 5 | labels: 6 | default: true 7 | spec: 8 | type: NodePort 9 | selector: 10 | backstage.io/app: # placeholder for 'backstage-' 11 | ports: 12 | - name: http 13 | port: 80 14 | targetPort: http -------------------------------------------------------------------------------- /pkg/model/testdata/dynamic-plugins-deps.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: default-dynamic-plugins # must be the same as (deployment.yaml).spec.template.spec.volumes.name.dynamic-plugins-conf.configMap.name 5 | data: 6 | "enabled-plugins-deps.txt": | 7 | orchestrator/* 8 | "dynamic-plugins.yaml": | 9 | includes: 10 | - dynamic-plugins.default.yaml 11 | plugins: [] -------------------------------------------------------------------------------- /pkg/model/testdata/invalid-service-type.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Deployment 3 | -------------------------------------------------------------------------------- /pkg/model/testdata/ips-db-statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: backstage-psql-cr1 # placeholder for 'backstage-psql-' 5 | spec: 6 | selector: 7 | matchLabels: 8 | rhdh.redhat.com/app: backstage-psql-cr1 # placeholder for 'backstage-psql-' 9 | serviceName: backstage-psql-cr1-hl # placeholder for 'backstage-psql--hl' 10 | template: 11 | metadata: 12 | labels: 13 | rhdh.redhat.com/app: backstage-psql-cr1 # placeholder for 'backstage-psql-' 14 | name: backstage-db-cr1 # placeholder for 'backstage-psql-' 15 | spec: 16 | imagePullSecrets: 17 | - name: ips-db1 18 | - name: ips-db2 19 | automountServiceAccountToken: false 20 | containers: 21 | - image: quay.io/fedora/postgresql-15:latest 22 | name: postgres 23 | 24 | -------------------------------------------------------------------------------- /pkg/model/testdata/ips-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: backstage 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | backstage.io/app: # placeholder for 'backstage-' 10 | template: 11 | metadata: 12 | labels: 13 | backstage.io/app: # placeholder for 'backstage-' 14 | spec: 15 | imagePullSecrets: 16 | - name: ips1 17 | - name: ips2 18 | containers: 19 | - name: backstage-backend # placeholder for 'backstage-backend' 20 | image: ghcr.io/backstage/backstage 21 | imagePullPolicy: IfNotPresent 22 | ports: 23 | - name: http 24 | containerPort: 7007 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /pkg/model/testdata/janus-db-statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: backstage-psql-cr1 # placeholder for 'backstage-psql-' 5 | spec: 6 | podManagementPolicy: OrderedReady 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | rhdh.redhat.com/app: backstage-psql-cr1 # placeholder for 'backstage-psql-' 11 | serviceName: backstage-psql-cr1-hl # placeholder for 'backstage-psql--hl' 12 | template: 13 | metadata: 14 | labels: 15 | rhdh.redhat.com/app: backstage-psql-cr1 # placeholder for 'backstage-psql-' 16 | name: backstage-db-cr1 # placeholder for 'backstage-psql-' 17 | spec: 18 | containers: 19 | - env: 20 | - name: POSTGRESQL_PORT_NUMBER 21 | value: "5432" 22 | - name: POSTGRESQL_VOLUME_DIR 23 | value: /var/lib/pgsql/data 24 | - name: PGDATA 25 | value: /var/lib/pgsql/data/userdata 26 | image: quay.io/fedora/postgresql-15:latest # will be replaced with the actual image 27 | imagePullPolicy: IfNotPresent 28 | securityContext: 29 | runAsNonRoot: true 30 | allowPrivilegeEscalation: false 31 | seccompProfile: 32 | type: RuntimeDefault 33 | capabilities: 34 | drop: 35 | - ALL 36 | livenessProbe: 37 | exec: 38 | command: 39 | - /bin/sh 40 | - -c 41 | - exec pg_isready -U ${POSTGRES_USER} -h 127.0.0.1 -p 5432 42 | failureThreshold: 6 43 | initialDelaySeconds: 30 44 | periodSeconds: 10 45 | successThreshold: 1 46 | timeoutSeconds: 5 47 | name: postgresql 48 | ports: 49 | - containerPort: 5432 50 | name: tcp-postgresql 51 | protocol: TCP 52 | readinessProbe: 53 | exec: 54 | command: 55 | - /bin/sh 56 | - -c 57 | - -e 58 | - | 59 | exec pg_isready -U ${POSTGRES_USER} -h 127.0.0.1 -p 5432 60 | failureThreshold: 6 61 | initialDelaySeconds: 5 62 | periodSeconds: 10 63 | successThreshold: 1 64 | timeoutSeconds: 5 65 | resources: 66 | requests: 67 | cpu: 250m 68 | memory: 256Mi 69 | limits: 70 | memory: 1024Mi 71 | volumeMounts: 72 | - mountPath: /dev/shm 73 | name: dshm 74 | - mountPath: /var/lib/pgsql/data 75 | name: data 76 | restartPolicy: Always 77 | securityContext: {} 78 | serviceAccount: default 79 | serviceAccountName: default 80 | volumes: 81 | - emptyDir: 82 | medium: Memory 83 | name: dshm 84 | updateStrategy: 85 | rollingUpdate: 86 | partition: 0 87 | type: RollingUpdate 88 | volumeClaimTemplates: 89 | - apiVersion: v1 90 | kind: PersistentVolumeClaim 91 | metadata: 92 | name: data 93 | spec: 94 | accessModes: 95 | - ReadWriteOnce 96 | resources: 97 | requests: 98 | storage: 1Gi -------------------------------------------------------------------------------- /pkg/model/testdata/janus-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: # placeholder for 'backstage-' 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | rhdh.redhat.com/app: # placeholder for 'backstage-' 10 | template: 11 | metadata: 12 | labels: 13 | rhdh.redhat.com/app: # placeholder for 'backstage-' 14 | spec: 15 | # serviceAccountName: default 16 | volumes: 17 | - ephemeral: 18 | volumeClaimTemplate: 19 | spec: 20 | accessModes: 21 | - ReadWriteOnce 22 | resources: 23 | requests: 24 | storage: 1Gi 25 | name: dynamic-plugins-root 26 | - name: dynamic-plugins-npmrc 27 | secret: 28 | defaultMode: 420 29 | optional: true 30 | secretName: dynamic-plugins-npmrc 31 | - name: dynamic-plugins-registry-auth 32 | secret: 33 | defaultMode: 416 34 | optional: true 35 | secretName: dynamic-plugins-registry-auth 36 | initContainers: 37 | - command: 38 | - ./install-dynamic-plugins.sh 39 | - /dynamic-plugins-root 40 | env: 41 | - name: NPM_CONFIG_USERCONFIG 42 | value: /opt/app-root/src/.npmrc.dynamic-plugins 43 | image: 'quay.io/rhdh/rhdh-hub-rhel9:next' 44 | imagePullPolicy: IfNotPresent 45 | name: install-dynamic-plugins 46 | volumeMounts: 47 | - mountPath: /dynamic-plugins-root 48 | name: dynamic-plugins-root 49 | - mountPath: /opt/app-root/src/.npmrc.dynamic-plugins 50 | name: dynamic-plugins-npmrc 51 | readOnly: true 52 | subPath: .npmrc 53 | - mountPath: /opt/app-root/src/.config/containers 54 | name: dynamic-plugins-registry-auth 55 | readOnly: true 56 | workingDir: /opt/app-root/src 57 | 58 | containers: 59 | - name: backstage-backend # placeholder for 'backstage-backend' 60 | image: quay.io/rhdh/rhdh-hub-rhel9:next 61 | imagePullPolicy: IfNotPresent 62 | args: 63 | - "--config" 64 | - "dynamic-plugins-root/app-config.dynamic-plugins.yaml" 65 | readinessProbe: 66 | failureThreshold: 3 67 | httpGet: 68 | path: /healthcheck 69 | port: 7007 70 | scheme: HTTP 71 | initialDelaySeconds: 30 72 | periodSeconds: 10 73 | successThreshold: 2 74 | timeoutSeconds: 2 75 | livenessProbe: 76 | failureThreshold: 3 77 | httpGet: 78 | path: /healthcheck 79 | port: 7007 80 | scheme: HTTP 81 | initialDelaySeconds: 60 82 | periodSeconds: 10 83 | successThreshold: 1 84 | timeoutSeconds: 2 85 | ports: 86 | - name: backend 87 | containerPort: 7007 88 | env: 89 | - name: APP_CONFIG_backend_listen_port 90 | value: "7007" 91 | volumeMounts: 92 | - mountPath: /opt/app-root/src/dynamic-plugins-root 93 | name: dynamic-plugins-root 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /pkg/model/testdata/multi-pvc-containers.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: myclaim1 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | volumeMode: Filesystem 9 | resources: 10 | requests: 11 | storage: 8Gi 12 | storageClassName: slow 13 | --- 14 | apiVersion: v1 15 | kind: PersistentVolumeClaim 16 | metadata: 17 | name: myclaim2 18 | annotations: 19 | rhdh.redhat.com/mount-path: /mount/path/from/annotation 20 | rhdh.redhat.com/containers: "backstage-backend,install-dynamic-plugins" 21 | spec: 22 | accessModes: 23 | - ReadWriteOnce 24 | volumeMode: Filesystem 25 | resources: 26 | requests: 27 | storage: 8Gi 28 | storageClassName: slow 29 | --- 30 | apiVersion: v1 31 | kind: PersistentVolumeClaim 32 | metadata: 33 | name: myclaim3 34 | annotations: 35 | rhdh.redhat.com/mount-path: /mount/path/from/annotation2 36 | rhdh.redhat.com/containers: "*" 37 | spec: 38 | accessModes: 39 | - ReadWriteOnce 40 | volumeMode: Filesystem 41 | resources: 42 | requests: 43 | storage: 8Gi 44 | storageClassName: slow -------------------------------------------------------------------------------- /pkg/model/testdata/multi-pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: myclaim1 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | volumeMode: Filesystem 9 | resources: 10 | requests: 11 | storage: 8Gi 12 | storageClassName: slow 13 | --- 14 | apiVersion: v1 15 | kind: PersistentVolumeClaim 16 | metadata: 17 | name: myclaim2 18 | annotations: 19 | rhdh.redhat.com/mount-path: /mount/path/from/annotation 20 | spec: 21 | accessModes: 22 | - ReadWriteOnce 23 | volumeMode: Filesystem 24 | resources: 25 | requests: 26 | storage: 8Gi 27 | storageClassName: slow -------------------------------------------------------------------------------- /pkg/model/testdata/multi-service-err.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | spec: 4 | ports: 5 | - port: 8080 6 | --- 7 | apiVersion: v1 8 | kind: Service 9 | spec: 10 | ports: 11 | - port: 8080 -------------------------------------------------------------------------------- /pkg/model/testdata/multicontainer-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: # placeholder for 'backstage-' 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | rhdh.redhat.com/app: # placeholder for 'backstage-' 10 | template: 11 | metadata: 12 | labels: 13 | rhdh.redhat.com/app: # placeholder for 'backstage-' 14 | spec: 15 | initContainers: 16 | - image: 'quay.io/rhdh/rhdh-hub-rhel9:next' 17 | name: install-dynamic-plugins 18 | - image: 'quay.io/rhdh/rhdh-hub-rhel9:next' 19 | name: another-init-container 20 | containers: 21 | - name: backstage-backend 22 | image: quay.io/rhdh/rhdh-hub-rhel9:next 23 | - name: another-container 24 | image: quay.io/rhdh/rhdh-hub-rhel9:next 25 | -------------------------------------------------------------------------------- /pkg/model/testdata/raw-app-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: my-backstage-config-cm1 5 | data: 6 | default.app-config.yaml: | 7 | backend: 8 | database: 9 | connection: 10 | password: ${POSTGRES_PASSWORD} 11 | user: ${POSTGRES_USER} 12 | auth: 13 | externalAccess: 14 | - type: legacy 15 | options: 16 | subject: legacy-default-config 17 | # This is a default value, which you should change by providing your own app-config 18 | secret: "pl4s3Ch4ng3M3" 19 | -------------------------------------------------------------------------------- /pkg/model/testdata/raw-cm-envs.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: 5 | data: 6 | ENV1: "env var 1" 7 | ENV2: "env var 2" -------------------------------------------------------------------------------- /pkg/model/testdata/raw-cm-files.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: # placeholder for '-dynamic-plugins' 5 | data: 6 | "dynamic-plugins123.yaml": | 7 | includes: 8 | - dynamic-plugins.default.yaml 9 | plugins: [] -------------------------------------------------------------------------------- /pkg/model/testdata/raw-dynamic-plugins.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: default-dynamic-plugins # must be the same as (deployment.yaml).spec.template.spec.volumes.name.dynamic-plugins-conf.configMap.name 5 | data: 6 | "dynamic-plugins.yaml": | 7 | includes: 8 | - dynamic-plugins.default.yaml 9 | plugins: [] -------------------------------------------------------------------------------- /pkg/model/testdata/raw-multi-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: secret1 5 | annotations: 6 | rhdh.redhat.com/containers: "backstage-backend,install-dynamic-plugins" 7 | stringData: 8 | data1: "data1" 9 | data2: "data2" 10 | --- 11 | apiVersion: v1 12 | kind: Secret 13 | metadata: 14 | annotations: 15 | rhdh.redhat.com/mount-path: /mount/path/from/annotation 16 | name: secret2 17 | stringData: 18 | data3: "data3" 19 | data4: "data4" 20 | --- 21 | apiVersion: v1 22 | kind: Secret 23 | metadata: 24 | name: secret3 25 | annotations: 26 | rhdh.redhat.com/containers: "*" 27 | rhdh.redhat.com/mount-path: /mount/path/from/annotation2 28 | stringData: 29 | data5: "data5" 30 | -------------------------------------------------------------------------------- /pkg/model/testdata/raw-route.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: route.openshift.io/v1 2 | kind: Route 3 | metadata: 4 | name: # placeholder for 'backstage-' 5 | spec: 6 | port: 7 | targetPort: default 8 | path: /default 9 | tls: 10 | insecureEdgeTerminationPolicy: Redirect 11 | termination: edge 12 | to: 13 | kind: Service 14 | name: # placeholder for 'backstage-' -------------------------------------------------------------------------------- /pkg/model/testdata/raw-sec-envs.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: 5 | stringData: 6 | ENV1: "env var 1" 7 | ENV2: "env var 2" -------------------------------------------------------------------------------- /pkg/model/testdata/raw-secret-files.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: # to be generaated 5 | stringData: 6 | "dynamic-plugins321.yaml": | 7 | includes: 8 | - dynamic-plugins.default.yaml 9 | plugins: [] -------------------------------------------------------------------------------- /pkg/model/testdata/sidecar-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: # placeholder for 'backstage-' 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | rhdh.redhat.com/app: # placeholder for 'backstage-' 10 | template: 11 | metadata: 12 | labels: 13 | rhdh.redhat.com/app: # placeholder for 'backstage-' 14 | spec: 15 | initContainers: 16 | - image: 'quay.io/rhdh/rhdh-hub-rhel9:next' 17 | name: install-dynamic-plugins 18 | containers: 19 | - name: backstage-backend # placeholder for 'backstage-backend' 20 | image: quay.io/rhdh/rhdh-hub-rhel9:next 21 | - name: sidecar 22 | image: busybox 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /pkg/model/testdata/single-pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: myclaim1 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | volumeMode: Filesystem 9 | resources: 10 | requests: 11 | storage: 8Gi 12 | storageClassName: slow 13 | -------------------------------------------------------------------------------- /pkg/model/testdata/working-dir-mount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: "" 5 | spec: 6 | # selector: 7 | # matchLabels: 8 | # backstage.io/app: # 9 | template: 10 | # metadata: 11 | # labels: 12 | # backstage.io/app: # 13 | spec: 14 | containers: 15 | - name: backstage-backend 16 | image: ghcr.io/backstage/backstage 17 | workingDir: "/my/home" 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /pkg/platform/platform.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | // Platform represents the Kubernetes platform type 4 | type Platform struct { 5 | Name string 6 | Extension string 7 | } 8 | 9 | var ( 10 | OpenShift = Platform{Name: "OpenShift", Extension: "ocp"} 11 | EKS = Platform{Name: "EKS", Extension: "k8s"} 12 | AKS = Platform{Name: "AKS", Extension: "k8s"} 13 | GKE = Platform{Name: "GKE", Extension: "k8s"} 14 | Kubernetes = Platform{Name: "Kubernetes", Extension: "k8s"} 15 | Default = Kubernetes 16 | ) 17 | 18 | func (p Platform) IsOpenshift() bool { 19 | return p == OpenShift 20 | } 21 | -------------------------------------------------------------------------------- /pkg/utils/pod-mutator.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | ) 6 | 7 | // sets pullSecret for Pod 8 | func SetImagePullSecrets(podSpec *corev1.PodSpec, pullSecrets []string) { 9 | if pullSecrets == nil { 10 | return 11 | } 12 | podSpec.ImagePullSecrets = []corev1.LocalObjectReference{} 13 | for _, ps := range pullSecrets { 14 | podSpec.ImagePullSecrets = append(podSpec.ImagePullSecrets, corev1.LocalObjectReference{Name: ps}) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pkg/utils/testdata/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: backstage 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | backstage.io/app: # placeholder for 'backstage-' 10 | template: 11 | metadata: 12 | labels: 13 | backstage.io/app: # placeholder for 'backstage-' 14 | spec: 15 | containers: 16 | - name: backstage-backend # placeholder for 'backstage-backend' 17 | image: ghcr.io/backstage/backstage 18 | imagePullPolicy: IfNotPresent 19 | ports: 20 | - name: http 21 | containerPort: 7007 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /pkg/utils/testdata/deployment.yaml.k8s: -------------------------------------------------------------------------------- 1 | #apiVersion: apps/v1 2 | #kind: Deployment 3 | #metadata: 4 | # name: backstage 5 | spec: 6 | replicas: 2 7 | # if securityContext not present in AKS/EKS, the error is like this: 8 | #Error: EACCES: permission denied, open '/dynamic-plugins-root/backstage-plugin-scaffolder-backend-module-github-dynamic-0.2.2.tgz' 9 | # fsGroup doesn not work for Openshift 10 | template: 11 | spec: 12 | securityContext: 13 | fsGroup: 1001 -------------------------------------------------------------------------------- /pkg/utils/yaml.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/fs" 8 | "os" 9 | "path/filepath" 10 | 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | "k8s.io/apimachinery/pkg/util/yaml" 13 | ) 14 | 15 | func ReadYamlFilesFromDir(dir string) ([]*unstructured.Unstructured, error) { 16 | 17 | if !DirectoryExists(dir) { 18 | return []*unstructured.Unstructured{}, nil 19 | } 20 | 21 | var objects []*unstructured.Unstructured 22 | err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { 23 | if err != nil { 24 | return err 25 | } 26 | if d.IsDir() || !IsYamlFile(d.Name()) { 27 | return nil 28 | } 29 | 30 | objs, err := ReadYamlFile(path) 31 | if err != nil { 32 | return fmt.Errorf("failed to read YAML file %s: %w", path, err) 33 | } 34 | objects = append(objects, objs...) 35 | return nil 36 | }) 37 | 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return objects, nil 43 | } 44 | 45 | func ReadYamlFile(path string) ([]*unstructured.Unstructured, error) { 46 | fpath := filepath.Clean(path) 47 | if _, err := os.Stat(fpath); err != nil { 48 | return nil, err 49 | } 50 | conf, err := os.ReadFile(fpath) 51 | if err != nil { 52 | return nil, fmt.Errorf("failed to read YAML file: %w", err) 53 | } 54 | 55 | dec := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(conf), 1000) 56 | var objects []*unstructured.Unstructured 57 | for { 58 | obj := &unstructured.Unstructured{} 59 | err := dec.Decode(obj) 60 | if err != nil { 61 | if err == io.EOF { 62 | break 63 | } 64 | return nil, fmt.Errorf("failed to decode YAML: %w", err) 65 | } 66 | objects = append(objects, obj) 67 | } 68 | 69 | return objects, nil 70 | } 71 | 72 | func ReadYamlContent(content string) ([]*unstructured.Unstructured, error) { 73 | // Create a YAML decoder from the content 74 | dec := yaml.NewYAMLOrJSONDecoder(bytes.NewReader([]byte(content)), 1000) 75 | var objects []*unstructured.Unstructured 76 | 77 | // Decode the content into unstructured objects 78 | for { 79 | obj := &unstructured.Unstructured{} 80 | err := dec.Decode(obj) 81 | if err != nil { 82 | if err == io.EOF { 83 | break 84 | } 85 | return nil, fmt.Errorf("failed to decode YAML content: %w", err) 86 | } 87 | objects = append(objects, obj) 88 | } 89 | 90 | return objects, nil 91 | } 92 | 93 | func IsYamlFile(filename string) bool { 94 | ext := filepath.Ext(filename) 95 | return ext == ".yaml" || ext == ".yml" 96 | } 97 | 98 | func DirectoryExists(path string) bool { 99 | info, err := os.Stat(path) 100 | if os.IsNotExist(err) { 101 | return false 102 | } 103 | return info.IsDir() 104 | } 105 | -------------------------------------------------------------------------------- /pkg/utils/yaml_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestReadYamlFile(t *testing.T) { 12 | // Create a temporary directory 13 | dir, err := os.MkdirTemp("", "test-yaml") 14 | assert.NoError(t, err) 15 | defer os.RemoveAll(dir) 16 | 17 | // Create a sample YAML file 18 | yamlContent := ` 19 | apiVersion: v1 20 | kind: ConfigMap 21 | metadata: 22 | name: test-configmap 23 | data: 24 | key: value 25 | ` 26 | filePath := filepath.Join(dir, "test.yaml") 27 | err = os.WriteFile(filePath, []byte(yamlContent), 0644) 28 | assert.NoError(t, err) 29 | 30 | // Test ReadYamlFile 31 | objects, err := ReadYamlFile(filePath) 32 | assert.NoError(t, err) 33 | assert.Len(t, objects, 1) 34 | 35 | obj := objects[0] 36 | assert.Equal(t, "ConfigMap", obj.GetKind()) 37 | assert.Equal(t, "test-configmap", obj.GetName()) 38 | } 39 | 40 | func TestReadYamlFilesFromDir(t *testing.T) { 41 | // Create a temporary directory 42 | dir, err := os.MkdirTemp("", "test-yaml-dir") 43 | assert.NoError(t, err) 44 | defer os.RemoveAll(dir) 45 | 46 | // Create sample YAML files 47 | yamlContent1 := ` 48 | apiVersion: v1 49 | kind: ConfigMap 50 | metadata: 51 | name: test-configmap1 52 | data: 53 | key: value1 54 | ` 55 | yamlContent2 := ` 56 | apiVersion: v1 57 | kind: ConfigMap 58 | metadata: 59 | name: test-configmap2 60 | data: 61 | key: value2 62 | ` 63 | err = os.WriteFile(filepath.Join(dir, "test1.yaml"), []byte(yamlContent1), 0644) 64 | assert.NoError(t, err) 65 | err = os.WriteFile(filepath.Join(dir, "test2.yaml"), []byte(yamlContent2), 0644) 66 | assert.NoError(t, err) 67 | 68 | // Test ReadYamlFilesFromDir 69 | objects, err := ReadYamlFilesFromDir(dir) 70 | assert.NoError(t, err) 71 | assert.Len(t, objects, 2) 72 | 73 | obj1 := objects[0] 74 | assert.Equal(t, "ConfigMap", obj1.GetKind()) 75 | assert.Equal(t, "test-configmap1", obj1.GetName()) 76 | 77 | obj2 := objects[1] 78 | assert.Equal(t, "ConfigMap", obj2.GetKind()) 79 | assert.Equal(t, "test-configmap2", obj2.GetName()) 80 | } 81 | --------------------------------------------------------------------------------