├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── 04_SUPPORT_QUESTION.md │ ├── 03_CODEBASE_IMPROVEMENT.md │ ├── 01_BUG_REPORT.md │ └── 02_FEATURE_REQUEST.md ├── pr-title-checker-config.json ├── dependabot.yml ├── workflows │ ├── pr-title-check.yml │ ├── build-and-release-install.yml │ ├── draft-release-on-push.yml │ ├── ci-check-licenses.yml │ ├── update-trivy-cache.yml │ ├── build-and-release-sbom.yml │ └── build-and-release-images.yml ├── PULL_REQUEST_TEMPLATE.md └── release-drafter.yml ├── docs ├── about │ ├── contributing.md │ ├── code-of-conduct.md │ ├── branding │ │ ├── paas-logo-v1-16x16px.png │ │ ├── paas-logo-v1-32x32px.png │ │ ├── paas-logo-v1-120x80px.png │ │ ├── paas-logo-v1-149x100px.png │ │ ├── paas-logo-v1-180x180px.png │ │ ├── paas-logo-v1-400x400px.png │ │ ├── paas-logo-v1-500x335px.png │ │ └── index.md │ └── license.md ├── user-guide │ ├── use-case1.png │ ├── 02_capabilities.md │ ├── 01_basic-usage.md │ ├── 02_groups-and-users.md │ └── 02_application-namespaces.md ├── overview │ ├── core_concepts │ │ ├── paasns.png │ │ ├── bootstrap.png │ │ ├── index.md │ │ ├── paas.md │ │ └── paasns.md │ └── index.md ├── administrators-guide │ ├── cluster-wide-quotas │ │ ├── clusterwide_quota.png │ │ └── basic-usage.md │ ├── install.md │ ├── security.md │ ├── feature-flags.md │ ├── validations.md │ ├── index.md │ └── v1alpha1-conversion.md └── development-guide │ ├── 00_api.md │ ├── 21_security-issues.md │ ├── 50_notes.md │ ├── 20_issues.md │ ├── 40_e2e-tests.md │ ├── 25_support-policy.md │ ├── 30_submitting-a-pr.md │ └── maintaining.md ├── test ├── README.md └── e2e │ ├── manifests │ ├── paas-context │ │ ├── capability-applicationsets │ │ │ ├── ns-argocd.yaml │ │ │ ├── appset-sso.yaml │ │ │ ├── appset-argocd.yaml │ │ │ ├── appset-cap5as.yaml │ │ │ ├── appset-tekton.yaml │ │ │ ├── appset-grafana.yaml │ │ │ └── kustomization.yaml │ │ ├── kustomization.yaml │ │ └── roles │ │ │ ├── monitoring-edit.yaml │ │ │ ├── alert-auditing-edit.yaml │ │ │ └── kustomization.yaml │ ├── gitops-operator │ │ ├── kustomization.yaml │ │ └── README.md │ ├── openshift │ │ ├── kustomization.yaml │ │ ├── Group.yaml │ │ └── README.md │ └── paas │ │ ├── kustomization.yaml │ │ ├── service_patch.yaml │ │ └── deployment_patch.yaml │ ├── fixtures │ └── crypt │ │ └── pub │ │ ├── publicKey0 │ │ └── publicKey1 │ ├── paas.go │ ├── steps.go │ ├── capability-external_test.go │ └── clusterresourcequota_test.go ├── manifests ├── certmanager │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── certificate.yaml ├── webhook │ ├── kustomization.yaml │ ├── service.yaml │ └── kustomizeconfig.yaml ├── config │ └── kustomization.yaml ├── webservice │ ├── kustomization.yaml │ ├── service.yaml │ ├── route.yaml │ ├── configmap.yaml │ └── deployment.yaml ├── rbac │ ├── metrics_reader_role.yaml │ ├── metrics_auth_role_binding.yaml │ ├── metrics_auth_role.yaml │ ├── service_account.yaml │ ├── kustomization.yaml │ ├── role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── paas_viewer_role.yaml │ ├── paasns_viewer_role.yaml │ ├── paasconfig_viewer_role.yaml │ ├── leader_election_role.yaml │ ├── paas_editor_role.yaml │ ├── paasns_editor_role.yaml │ ├── paasconfig_editor_role.yaml │ └── role.yaml ├── manager │ ├── kustomization.yaml │ ├── namespace.yaml │ └── opr-paas-deployment.yaml ├── crd │ ├── patches │ │ ├── webhook_in_paas.yaml │ │ ├── webhook_in_paasns.yaml │ │ └── webhook_in_paasconfig.yaml │ ├── kustomizeconfig.yaml │ └── kustomization.yaml └── default │ └── manager_webhook_patch.yaml ├── pkg ├── fields │ ├── element.go │ ├── elementarray.go │ ├── elementlist.go │ ├── elementlist_test.go │ ├── entries.go │ └── elementmap.go ├── templating │ ├── yaml_to.go │ ├── yaml_to_test.go │ └── main.go └── quota │ ├── quota.go │ └── quota_test.go ├── .dockerignore ├── hack ├── boilerplate.go.txt └── update-manifests.sh ├── CODE_OF_CONDUCT.md ├── mkdocs_overrides └── main.html ├── api ├── v1alpha2 │ ├── paas_conversion.go │ ├── paasns_conversion.go │ ├── paasconfig_conversion.go │ ├── namespaced_name.go │ ├── paas_types_test.go │ ├── groupversion_info.go │ ├── validations.go │ ├── suite_test.go │ └── paasns_types.go ├── paasconfig.go ├── paasresource.go ├── v1alpha1 │ ├── groupversion_info.go │ ├── paasns_conversion_test.go │ ├── paasns_conversion.go │ ├── validations.go │ ├── paas_types_ginkgo_test.go │ └── suite_test.go └── plugin │ └── main.go ├── NOTICE.md ├── internal ├── version │ └── main.go ├── argocd-plugin-generator │ ├── metrics.go │ ├── middleware.go │ ├── middleware_test.go │ └── suite_test.go ├── controller │ ├── main.go │ ├── main_test.go │ └── rsa_controller.go ├── config │ ├── informer.go │ └── config_test.go ├── utils │ ├── utils.go │ ├── utils_test.go │ └── notifier.go ├── logging │ └── components_test.go └── webhook │ └── v1alpha1 │ ├── utils.go │ └── validated_secrets.go ├── requirements.txt ├── examples └── resources │ ├── _v1alpha1_paasns.yaml │ ├── _v1alpha2_paas.yaml │ └── _v1alpha1_paas.yaml ├── bundle └── metadata │ └── annotations.yaml ├── crd-ref-docs-config.yml ├── bundle.Dockerfile ├── .gitignore ├── CITATION.cff ├── Dockerfile ├── CONTRIBUTING.md ├── PROJECT ├── publiccode.yml ├── mkdocs.yml ├── README.md ├── SECURITY.md └── .golangci.yaml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @belastingdienst/paas 2 | -------------------------------------------------------------------------------- /docs/about/contributing.md: -------------------------------------------------------------------------------- 1 | 2 | --8<-- "CONTRIBUTING.md" -------------------------------------------------------------------------------- /docs/about/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | 2 | --8<-- "CODE_OF_CONDUCT.md" -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | Everything in this folder is used during e2e testing 4 | -------------------------------------------------------------------------------- /docs/user-guide/use-case1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belastingdienst/opr-paas/HEAD/docs/user-guide/use-case1.png -------------------------------------------------------------------------------- /manifests/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /docs/overview/core_concepts/paasns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belastingdienst/opr-paas/HEAD/docs/overview/core_concepts/paasns.png -------------------------------------------------------------------------------- /test/e2e/manifests/paas-context/capability-applicationsets/ns-argocd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: asns -------------------------------------------------------------------------------- /docs/overview/core_concepts/bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belastingdienst/opr-paas/HEAD/docs/overview/core_concepts/bootstrap.png -------------------------------------------------------------------------------- /manifests/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /docs/about/branding/paas-logo-v1-16x16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belastingdienst/opr-paas/HEAD/docs/about/branding/paas-logo-v1-16x16px.png -------------------------------------------------------------------------------- /docs/about/branding/paas-logo-v1-32x32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belastingdienst/opr-paas/HEAD/docs/about/branding/paas-logo-v1-32x32px.png -------------------------------------------------------------------------------- /pkg/fields/element.go: -------------------------------------------------------------------------------- 1 | package fields 2 | 3 | // Element represents a value for one entry in the list of the listgenerator 4 | type Element any 5 | -------------------------------------------------------------------------------- /docs/about/branding/paas-logo-v1-120x80px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belastingdienst/opr-paas/HEAD/docs/about/branding/paas-logo-v1-120x80px.png -------------------------------------------------------------------------------- /docs/about/branding/paas-logo-v1-149x100px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belastingdienst/opr-paas/HEAD/docs/about/branding/paas-logo-v1-149x100px.png -------------------------------------------------------------------------------- /docs/about/branding/paas-logo-v1-180x180px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belastingdienst/opr-paas/HEAD/docs/about/branding/paas-logo-v1-180x180px.png -------------------------------------------------------------------------------- /docs/about/branding/paas-logo-v1-400x400px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belastingdienst/opr-paas/HEAD/docs/about/branding/paas-logo-v1-400x400px.png -------------------------------------------------------------------------------- /docs/about/branding/paas-logo-v1-500x335px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belastingdienst/opr-paas/HEAD/docs/about/branding/paas-logo-v1-500x335px.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | -------------------------------------------------------------------------------- /manifests/config/kustomization.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | 5 | resources: 6 | - example-keys.yaml 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The Paas Operator project follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /test/e2e/manifests/gitops-operator/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - argoproj.io_applicationsets.yaml -------------------------------------------------------------------------------- /test/e2e/manifests/openshift/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - ClusterResourceQuota.yaml 5 | - Group.yaml 6 | -------------------------------------------------------------------------------- /test/e2e/manifests/paas-context/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - capability-applicationsets 5 | - roles 6 | -------------------------------------------------------------------------------- /test/e2e/manifests/paas-context/roles/monitoring-edit.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: ClusterRole 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: monitoring-edit 6 | rules: [] 7 | -------------------------------------------------------------------------------- /docs/administrators-guide/cluster-wide-quotas/clusterwide_quota.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belastingdienst/opr-paas/HEAD/docs/administrators-guide/cluster-wide-quotas/clusterwide_quota.png -------------------------------------------------------------------------------- /test/e2e/manifests/paas-context/roles/alert-auditing-edit.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: ClusterRole 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: alert-routing-edit 6 | rules: [] 7 | -------------------------------------------------------------------------------- /test/e2e/manifests/paas-context/roles/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - alert-auditing-edit.yaml 5 | - monitoring-edit.yaml 6 | -------------------------------------------------------------------------------- /manifests/webservice/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - configmap.yaml 5 | - deployment.yaml 6 | - route.yaml 7 | - service.yaml 8 | -------------------------------------------------------------------------------- /test/e2e/manifests/paas-context/capability-applicationsets/appset-sso.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: ApplicationSet 3 | metadata: 4 | name: ssoas 5 | namespace: asns 6 | spec: 7 | generators: [] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/04_SUPPORT_QUESTION.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support Question 3 | about: Question on how to use this project 4 | title: "support: " 5 | labels: "question" 6 | assignees: "" 7 | --- 8 | 9 | # Support Question -------------------------------------------------------------------------------- /docs/development-guide/00_api.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/e2e/manifests/paas-context/capability-applicationsets/appset-argocd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: ApplicationSet 3 | metadata: 4 | name: argoas 5 | namespace: asns 6 | spec: 7 | generators: [] -------------------------------------------------------------------------------- /test/e2e/manifests/paas-context/capability-applicationsets/appset-cap5as.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: ApplicationSet 3 | metadata: 4 | name: cap5as 5 | namespace: asns 6 | spec: 7 | generators: [] -------------------------------------------------------------------------------- /test/e2e/manifests/paas-context/capability-applicationsets/appset-tekton.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: ApplicationSet 3 | metadata: 4 | name: tektonas 5 | namespace: asns 6 | spec: 7 | generators: [] -------------------------------------------------------------------------------- /docs/development-guide/21_security-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Raising security issues 3 | summary: How to raise security related issues. 4 | authors: 5 | - hikarukin 6 | date: 2024-09-018 7 | --- 8 | 9 | --8<-- "SECURITY.md" -------------------------------------------------------------------------------- /pkg/fields/elementarray.go: -------------------------------------------------------------------------------- 1 | package fields 2 | 3 | // ElementArray is an interface which represents all values that could be turned into an ElementMap 4 | type ElementArray interface { 5 | AsElementMap() ElementMap 6 | } 7 | -------------------------------------------------------------------------------- /test/e2e/manifests/paas-context/capability-applicationsets/appset-grafana.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: ApplicationSet 3 | metadata: 4 | name: grafanaas 5 | namespace: asns 6 | spec: 7 | generators: [] -------------------------------------------------------------------------------- /manifests/rbac/metrics_reader_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /mkdocs_overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block outdated %} 4 | You're not viewing the latest version. 5 | 6 | Click here to go to latest. 7 | 8 | {% endblock %} -------------------------------------------------------------------------------- /manifests/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - namespace.yaml 3 | - opr-paas-deployment.yaml 4 | apiVersion: kustomize.config.k8s.io/v1beta1 5 | kind: Kustomization 6 | images: 7 | - name: controller 8 | newName: controller 9 | newTag: latest 10 | -------------------------------------------------------------------------------- /api/v1alpha2/paas_conversion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package v1alpha2 8 | 9 | // Hub marks this type as a conversion hub. 10 | func (*Paas) Hub() {} 11 | -------------------------------------------------------------------------------- /api/v1alpha2/paasns_conversion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package v1alpha2 8 | 9 | // Hub marks this type as a conversion hub. 10 | func (*PaasNS) Hub() {} 11 | -------------------------------------------------------------------------------- /api/v1alpha2/paasconfig_conversion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package v1alpha2 8 | 9 | // Hub marks this type as a conversion hub. 10 | func (*PaasConfig) Hub() {} 11 | -------------------------------------------------------------------------------- /test/e2e/manifests/paas/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - ./generator-secret.yaml 6 | - ../../../../manifests/default/ 7 | 8 | patches: 9 | - path: deployment_patch.yaml 10 | - path: service_patch.yaml 11 | -------------------------------------------------------------------------------- /manifests/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/03_CODEBASE_IMPROVEMENT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Codebase improvement 3 | about: Provide your feedback for the existing codebase. Suggest a better solution for algorithms, development tools, etc. 4 | title: "dev: " 5 | labels: "enhancement" 6 | assignees: "" 7 | --- 8 | 9 | # Codebase Improvement -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | Licensing 2 | ========= 3 | 4 | Copyright 2024, Tax Administration of The Netherlands. 5 | Licensed under the EUPL 1.2. 6 | 7 | For FULL details of the license under which this software has been made available, 8 | please see the [LICENSE.md](./LICENSE.md) file in the root folder of this project. 9 | -------------------------------------------------------------------------------- /test/e2e/manifests/paas-context/capability-applicationsets/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - appset-argocd.yaml 5 | - appset-cap5as.yaml 6 | - appset-grafana.yaml 7 | - appset-sso.yaml 8 | - appset-tekton.yaml 9 | - ns-argocd.yaml 10 | -------------------------------------------------------------------------------- /manifests/webservice/service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: opr-paas-webservice 5 | spec: 6 | ports: 7 | - name: http 8 | protocol: TCP 9 | port: 80 10 | targetPort: 8080 11 | selector: 12 | app.kubernetes.io/component: webservice 13 | app.kubernetes.io/part-of: opr-paas 14 | -------------------------------------------------------------------------------- /docs/about/license.md: -------------------------------------------------------------------------------- 1 | The Paas Operator is being made available under the European Union Public License 2 | (EUPL) v1.2, which is available in [multiple languages](https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12). 3 | 4 | For your convenience, you can find the full, english language version below. 5 | 6 | ---- 7 | 8 | --8<-- "LICENSE.md" -------------------------------------------------------------------------------- /internal/version/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package version 8 | 9 | // PaasVersion holds the current version of this release 10 | // It is seed'ed by golreleaser and/or Dockerfile 11 | var PaasVersion = "v0.0.0devel" 12 | -------------------------------------------------------------------------------- /manifests/rbac/metrics_auth_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: metrics-auth-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: metrics-auth-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /manifests/rbac/metrics_auth_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-auth-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /manifests/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: opr-paas 6 | app.kubernetes.io/managed-by: kustomize 7 | name: webhook-service 8 | namespace: system 9 | spec: 10 | ports: 11 | - port: 443 12 | protocol: TCP 13 | targetPort: 9443 14 | selector: 15 | control-plane: paas-controller-manager 16 | -------------------------------------------------------------------------------- /api/paasconfig.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // PaasConfig is the generic interface for a Paas config, basically mandating 4 | // that any version PaasConfig has a GetSpec method which returns a spec. 5 | type PaasConfig[S any] interface { 6 | GetSpec() S 7 | } 8 | 9 | // ConfigCapabilities is a generic interface needed to allow the generic PaasConfig 10 | // interface. 11 | type ConfigCapabilities interface{} 12 | -------------------------------------------------------------------------------- /manifests/manager/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: paas-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: opr-paas 10 | app.kubernetes.io/part-of: opr-paas 11 | app.kubernetes.io/managed-by: kustomize 12 | name: paas-system -------------------------------------------------------------------------------- /docs/development-guide/50_notes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Developer notes 3 | summary: Some notes for developers of the Paas Operator code. 4 | authors: 5 | - hikarukin 6 | date: 2024-07-04 7 | --- 8 | 9 | Developer notes 10 | =============== 11 | 12 | - Because of dependency issues we decided to use a stub instead of importing all 13 | dependencies behind the original code of ArgoCD. 14 | 15 | More info in `internal/stubs/argoproj/v1alpha1` 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jinja2~=3.1 2 | markdown~=3.10 3 | mkdocs~=1.6 4 | mkdocs-material-extensions~=1.3 5 | mike~=2.1 6 | pygments~=2.19 7 | pymdown-extensions~=10.19 8 | 9 | # Requirements for plugins 10 | babel~=2.17 11 | colorama~=0.4 12 | paginate~=0.5 13 | regex>=2022.4 14 | requests~=2.32 15 | 16 | mkdocs-kroki-plugin 17 | mkdocs-literate-nav 18 | mkdocs-material 19 | mkdocs-material[recommended] 20 | mkdocs-material[imaging] 21 | mkdocs-redirects -------------------------------------------------------------------------------- /api/v1alpha2/namespaced_name.go: -------------------------------------------------------------------------------- 1 | package v1alpha2 2 | 3 | // NamespacedName is an internal type that can be used by the PaasConfig sub resources to define namespaced resources. 4 | type NamespacedName struct { 5 | // +kubebuilder:validation:MinLength=1 6 | // +kubebuilder:validation:Required 7 | Name string `json:"name"` 8 | // +kubebuilder:validation:MinLength=1 9 | // +kubebuilder:validation:Required 10 | Namespace string `json:"namespace"` 11 | } 12 | -------------------------------------------------------------------------------- /manifests/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: paas-controller-manager 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: opr-paas 9 | app.kubernetes.io/part-of: opr-paas 10 | app.kubernetes.io/managed-by: kustomize 11 | name: paas-controller-manager 12 | namespace: paas-system 13 | -------------------------------------------------------------------------------- /pkg/fields/elementlist.go: -------------------------------------------------------------------------------- 1 | package fields 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // ElementList represents a list of any values 8 | type ElementList []Element 9 | 10 | // AsElementMap converts the list into an ElementMap with string indices as keys. 11 | func (el ElementList) AsElementMap() ElementMap { 12 | result := ElementMap{} 13 | for index, value := range el { 14 | result[fmt.Sprintf("%d", index)] = value 15 | } 16 | return result 17 | } 18 | -------------------------------------------------------------------------------- /manifests/webservice/route.yaml: -------------------------------------------------------------------------------- 1 | kind: Route 2 | apiVersion: route.openshift.io/v1 3 | metadata: 4 | name: paas-webservice 5 | labels: 6 | app.kubernetes.io/component: webservice 7 | app.kubernetes.io/part-of: opr-paas 8 | spec: 9 | to: 10 | kind: Service 11 | name: paas-webservice 12 | weight: 100 13 | port: 14 | targetPort: http 15 | tls: 16 | termination: edge 17 | insecureEdgeTerminationPolicy: Redirect 18 | wildcardPolicy: None 19 | -------------------------------------------------------------------------------- /manifests/crd/patches/webhook_in_paas.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: paas.cpet.belastingdienst.nl 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 | -------------------------------------------------------------------------------- /manifests/crd/patches/webhook_in_paasns.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: paasns.cpet.belastingdienst.nl 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 | -------------------------------------------------------------------------------- /manifests/crd/patches/webhook_in_paasconfig.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: paasconfig.cpet.belastingdienst.nl 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 | -------------------------------------------------------------------------------- /.github/pr-title-checker-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "LABEL": { 3 | "name": "title needs formatting", 4 | "color": "EEEEEE" 5 | }, 6 | "CHECKS": { 7 | "prefixes": ["[Bot] docs: "], 8 | "regexp": "^(feat|perf|fix|hotfix|bug|docs|test|revert|refactor|ci|build|chore)!?(\\(.*\\))?!?:.*" 9 | }, 10 | "MESSAGES": { 11 | "success": "PR title is valid", 12 | "failure": "PR title is invalid", 13 | "notice": "PR Title needs to pass regex '^(feat|perf|fix|hotfix|bug|docs|test|revert|refactor|ci|build|chore)!?(\\(.*\\))?!?:.*" 14 | } 15 | } -------------------------------------------------------------------------------- /examples/resources/_v1alpha1_paasns.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cpet.belastingdienst.nl/v1alpha1 2 | kind: PaasNS 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: paasns 6 | app.kubernetes.io/instance: paasns-sample 7 | app.kubernetes.io/part-of: opr-paas 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: opr-paas 10 | name: ns1 11 | namespace: aap-aap 12 | spec: 13 | paas: aap-aap 14 | groups: 15 | - group1 16 | - group2 17 | sshSecrets: 18 | 'ssh://git@vcs/proj/repo/': >- 19 | 2wkeKe== 20 | -------------------------------------------------------------------------------- /test/e2e/manifests/paas/service_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app.kubernetes.io/managed-by: kustomize 6 | app.kubernetes.io/name: opr-paas 7 | name: webhook-service 8 | namespace: paas-system 9 | spec: 10 | ports: 11 | - protocol: TCP 12 | port: 4355 13 | targetPort: 4355 14 | name: plugin-generator 15 | - protocol: TCP 16 | port: 8080 17 | targetPort: 8080 18 | name: http 19 | - port: 443 20 | protocol: TCP 21 | targetPort: 9443 22 | name: webhook 23 | -------------------------------------------------------------------------------- /pkg/templating/yaml_to.go: -------------------------------------------------------------------------------- 1 | package templating 2 | 3 | import ( 4 | "github.com/belastingdienst/opr-paas/v4/pkg/fields" 5 | "github.com/goccy/go-yaml" 6 | ) 7 | 8 | func yamlToMap(data []byte) (result fields.ElementMap, err error) { 9 | result = fields.ElementMap{} 10 | if err = yaml.Unmarshal(data, &result); err != nil { 11 | return nil, err 12 | } 13 | return result, nil 14 | } 15 | 16 | func yamlToList(data []byte) (result fields.ElementList, err error) { 17 | if err = yaml.Unmarshal(data, &result); err != nil { 18 | return nil, err 19 | } 20 | return result, nil 21 | } 22 | -------------------------------------------------------------------------------- /manifests/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 | -------------------------------------------------------------------------------- /docs/development-guide/20_issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Raising issues 3 | summary: How to raise issues or feature requests. 4 | authors: 5 | - hikarukin 6 | date: 2024-07-04 7 | --- 8 | 9 | Raising Issues 10 | ============== 11 | 12 | When raising issues, please specify the following: 13 | 14 | - Setup details as specified in the issue template 15 | - A scenario where the issue occurred (with details on how to reproduce it) 16 | - Errors and log messages that are displayed by the involved software 17 | - Any other detail that might be useful 18 | 19 | For security related issues, we have a [dedicated security policy](21_security-issues.md). 20 | -------------------------------------------------------------------------------- /manifests/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | 5 | resources: 6 | - leader_election_role.yaml 7 | - leader_election_role_binding.yaml 8 | - metrics_auth_role.yaml 9 | - metrics_auth_role_binding.yaml 10 | - metrics_reader_role.yaml 11 | - role.yaml 12 | - role_binding.yaml 13 | - service_account.yaml 14 | # Uncomment the following lines if you want to add roles for end-users 15 | # - paas_editor_role.yaml 16 | # - paas_viewer_role.yaml 17 | # - paasns_editor_role.yaml 18 | # - paasns_viewer_role.yaml 19 | # - paasconfig_editor_role.yaml 20 | # - paasconfig_viewer_role.yaml -------------------------------------------------------------------------------- /docs/overview/core_concepts/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Welcome! 3 | summary: Introduction to core concepts of the Paas operator. 4 | authors: 5 | - devotional-phoenix-97 6 | date: 2025-01-20 7 | --- 8 | 9 | # Core concepts used in the Paas operator 10 | 11 | By leveraging the Paas operator, an organization can: 12 | 13 | - bring together all resources of a microservices into a single unit called a Paas 14 | - maintain multi tenancy between Paas instances 15 | - enable developers with capabilities to be used as part of the process behind maintaining the microservices 16 | 17 | Read more about these and other Core Concepts of the Paas operator in these pages. 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "gomod" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | - package-ecosystem: "pip" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | - package-ecosystem: "docker" 19 | directory: "/" 20 | schedule: 21 | interval: "weekly" 22 | -------------------------------------------------------------------------------- /manifests/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: paas-manager-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: opr-paas 9 | app.kubernetes.io/part-of: opr-paas 10 | app.kubernetes.io/managed-by: kustomize 11 | name: paas-manager-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: paas-manager-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: paas-controller-manager 19 | namespace: paas-system 20 | -------------------------------------------------------------------------------- /manifests/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: paas-leader-election-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: opr-paas 9 | app.kubernetes.io/part-of: opr-paas 10 | app.kubernetes.io/managed-by: kustomize 11 | name: paas-leader-election-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: paas-leader-election-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: paas-controller-manager 19 | namespace: paas-system 20 | -------------------------------------------------------------------------------- /bundle/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: opr-paas 7 | operators.operatorframework.io.bundle.channels.v1: stable 8 | operators.operatorframework.io.bundle.channel.default.v1: stable 9 | operators.operatorframework.io.metrics.builder: operator-sdk-v1.40.0 10 | operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 11 | operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v4 12 | -------------------------------------------------------------------------------- /api/v1alpha2/paas_types_test.go: -------------------------------------------------------------------------------- 1 | package v1alpha2_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | 8 | "github.com/belastingdienst/opr-paas/v4/api/v1alpha2" 9 | ) 10 | 11 | var _ = Describe("PaasTypes", func() { 12 | var paas *v1alpha2.Paas 13 | const paasName = "mypaas" 14 | BeforeEach(func() { 15 | paas = &v1alpha2.Paas{ 16 | ObjectMeta: metav1.ObjectMeta{ 17 | Name: paasName, 18 | }, 19 | } 20 | }) 21 | Describe("New Paas", func() { 22 | Context("with default values", func() { 23 | It("should have name properly set", func() { 24 | Expect(paas.Name).To(Equal(paasName)) 25 | }) 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /manifests/rbac/paas_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view paas. 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: paas-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: opr-paas 10 | app.kubernetes.io/part-of: opr-paas 11 | app.kubernetes.io/managed-by: kustomize 12 | name: paas-viewer-role 13 | rules: 14 | - apiGroups: 15 | - cpet.belastingdienst.nl 16 | resources: 17 | - paas 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - cpet.belastingdienst.nl 24 | resources: 25 | - paas/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /manifests/rbac/paasns_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view paasns. 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: paasns-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: opr-paas 10 | app.kubernetes.io/part-of: opr-paas 11 | app.kubernetes.io/managed-by: kustomize 12 | name: paasns-viewer-role 13 | rules: 14 | - apiGroups: 15 | - cpet.belastingdienst.nl 16 | resources: 17 | - paasns 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - cpet.belastingdienst.nl 24 | resources: 25 | - paasns/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /pkg/fields/elementlist_test.go: -------------------------------------------------------------------------------- 1 | package fields_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/belastingdienst/opr-paas/v4/pkg/fields" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var ( 11 | element1 = "a" 12 | element2 = 6 13 | element3 = map[string]any{element1: element2} 14 | element4 = []any{element1, element2} 15 | listElements = fields.ElementList{ 16 | element1, 17 | element2, 18 | element3, 19 | element4, 20 | } 21 | ) 22 | 23 | func TestListAsElementMap(t *testing.T) { 24 | sm := listElements.AsElementMap() 25 | assert.Equal( 26 | t, 27 | fields.ElementMap{ 28 | "0": element1, 29 | "1": element2, 30 | "2": element3, 31 | "3": element4, 32 | }, 33 | sm, 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /manifests/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: paas-controller-manager 5 | namespace: system 6 | labels: 7 | app.kubernetes.io/name: opr-paas 8 | app.kubernetes.io/managed-by: kustomize 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: manager 14 | ports: 15 | - containerPort: 9443 16 | name: webhook-server 17 | protocol: TCP 18 | volumeMounts: 19 | - mountPath: /tmp/k8s-webhook-server/serving-certs 20 | name: cert 21 | readOnly: true 22 | volumes: 23 | - name: cert 24 | secret: 25 | defaultMode: 420 26 | secretName: webhook-server-cert 27 | -------------------------------------------------------------------------------- /test/e2e/manifests/openshift/Group.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: groups.user.openshift.io 5 | spec: 6 | group: user.openshift.io 7 | versions: 8 | - name: v1 9 | served: true 10 | storage: true 11 | schema: 12 | openAPIV3Schema: 13 | type: object 14 | properties: 15 | apiVersion: 16 | type: string 17 | kind: 18 | type: string 19 | metadata: 20 | type: object 21 | users: 22 | type: array 23 | items: 24 | type: string 25 | scope: Cluster 26 | names: 27 | plural: groups 28 | singular: group 29 | kind: Group -------------------------------------------------------------------------------- /manifests/rbac/paasconfig_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view paasconfig. 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: paasconfig-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: opr-paas 10 | app.kubernetes.io/part-of: opr-paas 11 | app.kubernetes.io/managed-by: kustomize 12 | name: paasconfig-viewer-role 13 | rules: 14 | - apiGroups: 15 | - cpet.belastingdienst.nl 16 | resources: 17 | - paasconfig 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - cpet.belastingdienst.nl 24 | resources: 25 | - paasconfig/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /manifests/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: paas-leader-election-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: opr-paas 10 | app.kubernetes.io/part-of: opr-paas 11 | app.kubernetes.io/managed-by: kustomize 12 | name: paas-leader-election-role 13 | rules: 14 | - apiGroups: 15 | - coordination.k8s.io 16 | resources: 17 | - leases 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - "" 28 | resources: 29 | - events 30 | verbs: 31 | - create 32 | - patch 33 | -------------------------------------------------------------------------------- /crd-ref-docs-config.yml: -------------------------------------------------------------------------------- 1 | processor: 2 | # RE2 regular expressions describing types that should be excluded from the generated documentation. 3 | ignoreTypes: 4 | - "Associa(ted|tor|tionStatus|tionConf)$" 5 | # RE2 regular expressions describing type fields that should be excluded from the generated documentation. 6 | ignoreFields: 7 | - "status$" 8 | - "TypeMeta$" 9 | 10 | render: 11 | # Version of Kubernetes to use when generating links to Kubernetes API documentation. 12 | kubernetesVersion: 1.22 13 | # Generate better link for known types 14 | knownTypes: 15 | - name: SecretObjectReference 16 | package: sigs.k8s.io/gateway-api/apis/v1beta1 17 | link: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.SecretObjectReference 18 | -------------------------------------------------------------------------------- /manifests/rbac/paas_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit paas. 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: paas-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: opr-paas 10 | app.kubernetes.io/part-of: opr-paas 11 | app.kubernetes.io/managed-by: kustomize 12 | name: paas-editor-role 13 | rules: 14 | - apiGroups: 15 | - cpet.belastingdienst.nl 16 | resources: 17 | - paas 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - cpet.belastingdienst.nl 28 | resources: 29 | - paas/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /manifests/rbac/paasns_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit paasns. 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: paasns-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: opr-paas 10 | app.kubernetes.io/part-of: opr-paas 11 | app.kubernetes.io/managed-by: kustomize 12 | name: paasns-editor-role 13 | rules: 14 | - apiGroups: 15 | - cpet.belastingdienst.nl 16 | resources: 17 | - paasns 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - cpet.belastingdienst.nl 28 | resources: 29 | - paasns/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /manifests/rbac/paasconfig_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit paasconfig. 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: paasconfig-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: opr-paas 10 | app.kubernetes.io/part-of: opr-paas 11 | app.kubernetes.io/managed-by: kustomize 12 | name: paasconfig-editor-role 13 | rules: 14 | - apiGroups: 15 | - cpet.belastingdienst.nl 16 | resources: 17 | - paasconfig 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - cpet.belastingdienst.nl 28 | resources: 29 | - paasconfig/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /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=opr-paas 8 | LABEL operators.operatorframework.io.bundle.channels.v1=stable 9 | LABEL operators.operatorframework.io.bundle.channel.default.v1=stable 10 | LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.40.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 | # Copy files to locations specified by labels. 15 | COPY bundle/manifests /manifests/ 16 | COPY bundle/metadata /metadata/ 17 | -------------------------------------------------------------------------------- /.github/workflows/pr-title-check.yml: -------------------------------------------------------------------------------- 1 | name: Lint PR title 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, reopened, synchronize] 6 | 7 | permissions: {} 8 | 9 | # PR updates can happen in quick succession leading to this 10 | # workflow being trigger a number of times. This limits it 11 | # to one run per PR. 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.head_ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | validate: 18 | permissions: 19 | contents: read 20 | pull-requests: read 21 | name: Validate PR Title 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: thehanimo/pr-title-checker@7fbfe05602bdd86f926d3fb3bccb6f3aed43bc70 # v1.4.3 25 | with: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | configuration_path: ".github/pr-title-checker-config.json" 28 | -------------------------------------------------------------------------------- /manifests/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting nameReference. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | -------------------------------------------------------------------------------- /test/e2e/fixtures/crypt/pub/publicKey0: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5rkpTO2NmIG7ybyfp2VG 3 | /QUCc0fhh1Hf4W4LYg/Lwqm6+dTGi9WYqDD0p+MjfmDooWVb1XpNdIi6qbJK+wW9 4 | AeeIHzuOIA3aVTQIxvfx0+F9TO9UOJuJ19j3jJmvLcdzPvRKHeirq8xtshOAgLqy 5 | 2iqhZm9fZKQTlLy174Y9InXg2y4HEZrp4tdUNS1JmUZbIFFYc9ccJ7/OZ/cuCtXG 6 | 3XKItvSIgHOeoHJrBuJn2QhhkIWTqwoFS7yii1G7ezOfKuRfwl6BgAf28BmCxMra 7 | JhPtE5CAli+CPh0Y4U6W5Z+RuN7m1NNzv5pJp6gj14SEiBJmV4/YXFM72SdZ4axZ 8 | kvzQl3gmpBVt5M0ymRqKVxfchKNQKg00mCum5Eow7kEZa/DDna77FraRa/C+ImAx 9 | DJ3ET6xF8vyTzzarQe8klIUlYxNx1gJzBbDChBNCnCaHYhEi0v4Wurzf/DpnR5nv 10 | yww5nid9wLjVZs/CXtMlLO3oiWLmd/24R0nhkCmXL4wNpesTE29a64PM4vBib+Bk 11 | 9/TImHLvDo7oncAwO4qcObb8g/l3nz0UCsoaJ+taXxuJmOHrdNTIjWHFf0mDV26P 12 | SSMD+k/rUctjOITOzkiuCXiF9t50j7H0MrosnoAUwIDPL5XJNFEjzV8hy2kE7oeP 13 | 54yfLoOT/vQ7sIJEaB/n3GECAwEAAQ== 14 | -----END RSA PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /test/e2e/fixtures/crypt/pub/publicKey1: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyB86bWMqz/59sI8TvRXy 3 | V1ieRsPKsblAAlSPIZ/YFq0kaJhyCF0w8F3k1ptUSmnPxIVpauMpFpHCQp9p9D3T 4 | taA7cpVkY2+I9U/mo0z+IZCDv4yWhMeNRCmO5XslZYRzabJid2+d1dFnbYbMZjmD 5 | BtUYLADF2Fat0c5ujetUgI/meSz3VHQ13bvB8+IX/e+Fbm+WpY/tlUiCpqwZ61lu 6 | wLvFdqaXZRJVa/OyFbA8tII5Q9JG19kXjr/LKQU6TzwPUbv78YrunHXgqXGgs0+F 7 | 1yicvPqoS66hot37cUyHbWnMYsJ+PAxiz4IobH4LxBmt++PbFDLF6Bttoy/de2wx 8 | whMHIw7cMzr389XeSkpoinJmt9TZjqiqhklByI8MnDlnQdfsQrM4/QwfRK3a+jhQ 9 | lUotZLAr9oLt1XHPtPMwXLUkgmqDYZdCd0H6WaO2EveenkYc3UWJy1xPlM7HHAzK 10 | zcU4hqYws12I84nI3jooZZdJ9iBjM4tNHnVAvopBMyApxt/cMOsDyUdiUWtuCTL3 11 | H2407qs9conkgJUlwZ1hM0kPPixDUVjwOAx4eVh6MZyBC+agB5JzlcmPVed29sWf 12 | Q56JgSkZPBfAmP98ARu+UrAClNZ6vPLa7xV7b+PUUglXgjm26eLTwX3JFk01s+xm 13 | LGEUd8UHjyc48JtLtFeZ6UMCAwEAAQ== 14 | -----END RSA PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /test/e2e/paas.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | // Helper functions for manipulating Paas resources in a test. 4 | 5 | import ( 6 | "context" 7 | "testing" 8 | 9 | api "github.com/belastingdienst/opr-paas/v4/api/v1alpha2" 10 | 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "sigs.k8s.io/e2e-framework/pkg/envconf" 13 | ) 14 | 15 | // getPaas retrieves the Paas with the associated name. 16 | func getPaas(ctx context.Context, name string, t *testing.T, cfg *envconf.Config) *api.Paas { 17 | return getOrFail(ctx, name, cfg.Namespace(), &api.Paas{}, t, cfg) 18 | } 19 | 20 | // deletePaasSync deletes the Paas with the associated name. 21 | func deletePaasSync(ctx context.Context, name string, t *testing.T, cfg *envconf.Config) { 22 | paas := &api.Paas{ObjectMeta: metav1.ObjectMeta{Name: name}} 23 | 24 | if err := deleteResourceSync(ctx, cfg, paas); err != nil { 25 | t.Fatal(err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api/paasresource.go: -------------------------------------------------------------------------------- 1 | // Package api has a Resource interface which can be used for functions that should work with multiple Paas 2 | // resources. 3 | package api 4 | 5 | import ( 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "sigs.k8s.io/e2e-framework/klient/k8s" 8 | ) 9 | 10 | // Resource represents a Paas Resource (e.a. Paas, PaasNS or PaasConfig) with a `.status.conditions` slice field of 11 | // conditions. This is a workaround to match our custom resource types; all our custom resource types have the same 12 | // `.status.conditions` fields, but Go generics do not currently allow accessing shared struct fields via generic types. 13 | // This is apparently a feature slated for Go 2. (https://github.com/golang/go/issues/48522#issuecomment-924380147) 14 | type Resource interface { 15 | k8s.Object 16 | GetConditions() *[]metav1.Condition 17 | GetGeneration() int64 18 | GetName() string 19 | } 20 | -------------------------------------------------------------------------------- /test/e2e/manifests/gitops-operator/README.md: -------------------------------------------------------------------------------- 1 | # GitOps-operator 2 | 3 | As described in the docs, we integrate with the GitOps-operator. 4 | In this way, we are able to deploy capabilities via a clusterwide `ArgoCD`. 5 | 6 | ## e2e-test 7 | 8 | In order to run e2e-tests on vanilla k8s, we need to install the `ArgoCD crd`. 9 | This CRD can be found in the accompanied [file](argoproj.io_applicationsets.yaml). 10 | 11 | ## LCM 12 | 13 | While implementing the e2e-tests, we assumed the upstream CRD updates are backwards compatible. 14 | Meaning, we assume we should be able to base the opr-paas on the current state of this CRD. 15 | Ofcourse this is short-sighted, we will come up with a way to keep up with the upstream CRD and test 16 | whether the e2e-tests succeed when a new release of the GitOps operator has been issued. 17 | 18 | 2025-04-08; we are exploring the options to get rid of the ArgoCD integration entirely by implementing 19 | an ApplicationSet generator plugin. -------------------------------------------------------------------------------- /internal/argocd-plugin-generator/metrics.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package argocd_plugin_generator 8 | 9 | import ( 10 | "github.com/prometheus/client_golang/prometheus" 11 | "sigs.k8s.io/controller-runtime/pkg/metrics" 12 | ) 13 | 14 | // PluginGeneratorRequestTotal is a prometheus metric which is a counter of 15 | // the total processed plugin generator requests. 16 | var PluginGeneratorRequestTotal = func() *prometheus.CounterVec { 17 | return prometheus.NewCounterVec( 18 | prometheus.CounterOpts{ 19 | Name: "opr_paas_plugin_generator_requests_total", 20 | Help: "Total number of plugin generator requests by HTTP status code.", 21 | }, 22 | []string{"code"}, 23 | ) 24 | }() 25 | 26 | func init() { 27 | // Register custom metrics with the global prometheus registry 28 | metrics.Registry.MustRegister(PluginGeneratorRequestTotal) 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/build-and-release-install.yml: -------------------------------------------------------------------------------- 1 | name: Build and add install.yaml to release 2 | on: 3 | workflow_dispatch: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | sbom: 10 | name: Generate and upload install.yaml 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | pull-requests: write 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 18 | with: 19 | persist-credentials: false 20 | 21 | - name: Install kustomize 22 | run: make kustomize 23 | 24 | - name: Generate install.yaml 25 | run: make build-installer 26 | env: 27 | IMAGE_TAG: ${{ github.ref_name }} 28 | 29 | - name: Add install.yaml to release 30 | uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 31 | with: 32 | files: manifests/install.yaml 33 | -------------------------------------------------------------------------------- /test/e2e/manifests/paas/deployment_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: paas-controller-manager 5 | namespace: paas-system 6 | labels: 7 | app.kubernetes.io/name: opr-paas 8 | app.kubernetes.io/managed-by: kustomize 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: manager 14 | args: 15 | - --leader-elect 16 | - --metrics-bind-address=:8080 17 | - --argocd-plugin-generator-bind-address=:4355 18 | - --metrics-secure=false 19 | ports: 20 | - containerPort: 4355 21 | name: argocd 22 | protocol: TCP 23 | - containerPort: 9443 24 | name: webhook-server 25 | protocol: TCP 26 | - containerPort: 8080 27 | name: metrics 28 | protocol: TCP 29 | envFrom: 30 | - secretRef: 31 | name: generator-token 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin/* 9 | testbin/* 10 | Dockerfile.cross 11 | *.tar 12 | 13 | # dev environment specific files 14 | go.work* 15 | 16 | # Test binary, build with `go test -c` 17 | *.test 18 | 19 | # Output of the go coverage tool, specifically when used with LiteIDE 20 | *.out 21 | 22 | # Kubernetes Generated files - skip generated files, except for vendored files 23 | 24 | !vendor/**/zz_generated.* 25 | 26 | # editor and IDE paraphernalia 27 | .idea 28 | *.swp 29 | *.swo 30 | *~ 31 | /manager 32 | /crypttool* 33 | /webservice 34 | 35 | site/ 36 | .cache/ 37 | dist/ 38 | 39 | # The install.yml file is not committed but generated and added to release artifacts 40 | manifests/install.yaml 41 | install.yaml 42 | 43 | .vscode/ 44 | 45 | /bundle/manifests/* 46 | !/bundle/manifests/opr-paas.clusterserviceversion.yaml 47 | !/bundle.Dockerfile 48 | !/bundle/metadata/ 49 | 50 | test/e2e/manifests/paas/generator-secret.yaml 51 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | // Package v1alpha1 contains API Schema definitions for the v1alpha1 API group 8 | // +kubebuilder:object:generate=true 9 | // +groupName=cpet.belastingdienst.nl 10 | package v1alpha1 11 | 12 | import ( 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | "sigs.k8s.io/controller-runtime/pkg/scheme" 15 | ) 16 | 17 | var ( 18 | // GroupVersion is group version used to register these objects 19 | GroupVersion = schema.GroupVersion{Group: "cpet.belastingdienst.nl", Version: "v1alpha1"} 20 | 21 | paasAPIVersion = GroupVersion.Group + "/" + GroupVersion.Version 22 | 23 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 24 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 25 | 26 | // AddToScheme adds the types in this group-version to the given scheme. 27 | AddToScheme = SchemeBuilder.AddToScheme 28 | ) 29 | -------------------------------------------------------------------------------- /api/v1alpha2/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | // Package v1alpha2 contains API Schema definitions for the v1alpha2 API group 8 | // +kubebuilder:object:generate=true 9 | // +groupName=cpet.belastingdienst.nl 10 | package v1alpha2 11 | 12 | import ( 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | "sigs.k8s.io/controller-runtime/pkg/scheme" 15 | ) 16 | 17 | var ( 18 | // GroupVersion is group version used to register these objects 19 | GroupVersion = schema.GroupVersion{Group: "cpet.belastingdienst.nl", Version: "v1alpha2"} 20 | 21 | paasAPIVersion = GroupVersion.Group + "/" + GroupVersion.Version 22 | 23 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 24 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 25 | 26 | // AddToScheme adds the types in this group-version to the given scheme. 27 | AddToScheme = SchemeBuilder.AddToScheme 28 | ) 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01_BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug for this project 4 | title: "bug: " 5 | labels: "bug" 6 | assignees: "" 7 | --- 8 | 9 | # Bug Report 10 | 11 | **Paas Operator version:** 12 | 13 | 14 | 15 | **Current behaviour:** 16 | 17 | 18 | 19 | **Expected behaviour:** 20 | 21 | 22 | 23 | **Steps to reproduce:** 24 | 25 | 26 | 27 | **Related code:** 28 | 29 | 30 | 31 | ``` 32 | insert short code snippets here 33 | ``` 34 | 35 | **Other information:** 36 | 37 | -------------------------------------------------------------------------------- /internal/argocd-plugin-generator/middleware.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package argocd_plugin_generator 8 | 9 | import ( 10 | "net/http" 11 | "strconv" 12 | ) 13 | 14 | type statusRecorder struct { 15 | http.ResponseWriter 16 | statusCode int 17 | } 18 | 19 | func newStatusRecorder(w http.ResponseWriter) *statusRecorder { 20 | // Default to 200 in case WriteHeader is not called 21 | return &statusRecorder{ResponseWriter: w, statusCode: http.StatusOK} 22 | } 23 | 24 | func (r *statusRecorder) WriteHeader(code int) { 25 | r.statusCode = code 26 | r.ResponseWriter.WriteHeader(code) 27 | } 28 | 29 | func withMetrics(next http.Handler) http.Handler { 30 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 | rec := newStatusRecorder(w) 32 | next.ServeHTTP(rec, r) 33 | 34 | PluginGeneratorRequestTotal. 35 | WithLabelValues(strconv.Itoa(rec.statusCode)). 36 | Inc() 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /manifests/webservice/configmap.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: paas-secrets-publickey 6 | namespace: paas 7 | data: 8 | publicKey: | 9 | -----BEGIN RSA PUBLIC KEY----- 10 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5rkpTO2NmIG7ybyfp2VG 11 | /QUCc0fhh1Hf4W4LYg/Lwqm6+dTGi9WYqDD0p+MjfmDooWVb1XpNdIi6qbJK+wW9 12 | AeeIHzuOIA3aVTQIxvfx0+F9TO9UOJuJ19j3jJmvLcdzPvRKHeirq8xtshOAgLqy 13 | 2iqhZm9fZKQTlLy174Y9InXg2y4HEZrp4tdUNS1JmUZbIFFYc9ccJ7/OZ/cuCtXG 14 | 3XKItvSIgHOeoHJrBuJn2QhhkIWTqwoFS7yii1G7ezOfKuRfwl6BgAf28BmCxMra 15 | JhPtE5CAli+CPh0Y4U6W5Z+RuN7m1NNzv5pJp6gj14SEiBJmV4/YXFM72SdZ4axZ 16 | kvzQl3gmpBVt5M0ymRqKVxfchKNQKg00mCum5Eow7kEZa/DDna77FraRa/C+ImAx 17 | DJ3ET6xF8vyTzzarQe8klIUlYxNx1gJzBbDChBNCnCaHYhEi0v4Wurzf/DpnR5nv 18 | yww5nid9wLjVZs/CXtMlLO3oiWLmd/24R0nhkCmXL4wNpesTE29a64PM4vBib+Bk 19 | 9/TImHLvDo7oncAwO4qcObb8g/l3nz0UCsoaJ+taXxuJmOHrdNTIjWHFf0mDV26P 20 | SSMD+k/rUctjOITOzkiuCXiF9t50j7H0MrosnoAUwIDPL5XJNFEjzV8hy2kE7oeP 21 | 54yfLoOT/vQ7sIJEaB/n3GECAwEAAQ== 22 | -----END RSA PUBLIC KEY----- 23 | -------------------------------------------------------------------------------- /internal/controller/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package controller 8 | 9 | import ( 10 | "maps" 11 | "strings" 12 | ) 13 | 14 | const ( 15 | paasKey = "paas" 16 | ) 17 | 18 | func join(argv ...string) string { 19 | return strings.Join(argv, "-") 20 | } 21 | 22 | // intersect finds the intersection of 2 lists of strings 23 | func intersect(l1 []string, l2 []string) (li []string) { 24 | s := map[string]bool{} 25 | for _, key := range l1 { 26 | s[key] = false 27 | } 28 | for _, key := range l2 { 29 | if _, exists := s[key]; exists { 30 | s[key] = true 31 | } 32 | } 33 | for key, value := range s { 34 | if value { 35 | li = append(li, key) 36 | } 37 | } 38 | return li 39 | } 40 | 41 | // Helper to merge secrets which returns a new map[string]string 42 | func mergeSecrets(base, override map[string]string) map[string]string { 43 | merged := make(map[string]string, len(base)+len(override)) 44 | maps.Copy(merged, base) 45 | maps.Copy(merged, override) 46 | return merged 47 | } 48 | -------------------------------------------------------------------------------- /api/plugin/main.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import "github.com/belastingdienst/opr-paas/v4/pkg/fields" 4 | 5 | // Request represents the expected request payload for the plug-in generator. 6 | // 7 | // The ApplicationSetName identifies the target ApplicationSet in Argo CD. 8 | // Input.Parameters is a map of user-provided parameters, where keys are 9 | // strings and values are strings as well. 10 | type Request struct { 11 | ApplicationSetName string `json:"applicationSetName"` 12 | Input Input `json:"input"` 13 | } 14 | 15 | // Input represents the input which is added to a Request 16 | type Input struct { 17 | Parameters fields.ElementMap `json:"parameters"` 18 | } 19 | 20 | // Response represents the response payload returned by the plug-in generator. 21 | // 22 | // Output.Parameters is a slice of maps, where each map contains a set of 23 | // key-value pairs representing generated parameters for the ApplicationSet. 24 | type Response struct { 25 | Output Output `json:"output"` 26 | } 27 | 28 | // Output represents the output data which is added to a Response 29 | type Output struct { 30 | Parameters []fields.ElementMap `json:"parameters"` 31 | } 32 | -------------------------------------------------------------------------------- /hack/update-manifests.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | set -x 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | SRCROOT="$(CDPATH='' cd -- "$(dirname "$0")/.." && pwd -P)" 8 | AUTOGENMSG="# This is an auto-generated file. DO NOT EDIT" 9 | 10 | KUSTOMIZE=kustomize 11 | [ -f "$SRCROOT/dist/kustomize" ] && KUSTOMIZE="$SRCROOT/dist/kustomize" 12 | 13 | IMAGE_NAMESPACE="${IMAGE_NAMESPACE:-ghcr.io/belastingdienst}" 14 | IMAGE_TAG="${IMAGE_TAG:-}" 15 | 16 | # if the tag has not been declared, and we are on a release branch, use the VERSION file. 17 | if [ "$IMAGE_TAG" = "" ]; then 18 | branch=$(git rev-parse --abbrev-ref HEAD) 19 | if [[ $branch = release-* ]]; then 20 | pwd 21 | IMAGE_TAG=v$(cat "$SRCROOT/VERSION") 22 | fi 23 | fi 24 | # otherwise, use latest 25 | if [ "$IMAGE_TAG" = "" ]; then 26 | IMAGE_TAG=latest 27 | fi 28 | 29 | $KUSTOMIZE version 30 | which "$KUSTOMIZE" 31 | 32 | cd "${SRCROOT}"/manifests/default && $KUSTOMIZE edit set image controller="${IMAGE_NAMESPACE}/opr-paas:${IMAGE_TAG}" 33 | 34 | echo "${AUTOGENMSG}" >"${SRCROOT}/manifests/install.yaml" 35 | $KUSTOMIZE build "${SRCROOT}/manifests/default" >>"${SRCROOT}/manifests/install.yaml" 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02_FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: "feat: " 5 | labels: "enhancement" 6 | assignees: "" 7 | --- 8 | 9 | # Feature Request 10 | 11 | **Describe the Feature Request** 12 | 13 | 14 | 15 | **Describe Preferred Solution** 16 | 17 | 18 | 19 | **Describe Alternatives** 20 | 21 | 22 | 23 | **Related Code** 24 | 25 | 26 | 27 | **Additional Context** 28 | 29 | 30 | 31 | **If the feature request is approved, would you be willing to submit a PR?** 32 | _(Help can be provided if you need assistance submitting a PR)_ 33 | 34 | - [ ] Yes 35 | - [ ] No -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | title: Project as a Service Operator 3 | message: >- 4 | If you use this software, please cite it using the 5 | metadata from this file. 6 | type: software 7 | authors: 8 | - name: Tax Administration of The Netherlands 9 | website: 'https://www.belastingdienst.nl' 10 | repository-code: 'https://github.com/orgs/belastingdienst/opr-paas/' 11 | url: 'https://github.com/orgs/belastingdienst/opr-paas/' 12 | abstract: >- 13 | The PaaS operator delivers an opiniated 'Project as a 14 | Service' implementation where development teams can 15 | request a 'Project as a Service' by defining a PaaS 16 | resource. 17 | 18 | A PaaS resource is used by the operator uses as an input 19 | to create namespaces limited by Cluster Resource Quota's, 20 | granting groups permissions and (together with a 21 | clusterwide ArgoCD) creating capabilities such as a PaaS 22 | specific deployment of ArgoCD (continuous deployment), 23 | Tekton (continuous integration), Grafana (observability), 24 | and KeyCloak (Application level Signle Sign On). 25 | keywords: 26 | - kubernetes 27 | - openshift 28 | - go 29 | - golang 30 | - operator 31 | - paas 32 | - project 33 | - service 34 | license: EUPL-1.2 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | 3 | FROM --platform=${BUILDPLATFORM} docker.io/golang:1.25 AS builder 4 | 5 | ARG TARGETOS 6 | ARG TARGETARCH 7 | ARG VERSION=v0.0.0-devel 8 | 9 | WORKDIR /workspace 10 | 11 | # Copy the go source 12 | COPY . . 13 | 14 | # Build 15 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 16 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 17 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 18 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 19 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -v -a -ldflags="-X 'github.com/belastingdienst/opr-paas/v3/internal/version.PaasVersion=${VERSION}'" -o manager ./cmd/manager 20 | 21 | # Use distroless as minimal base image to package the manager binary 22 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 23 | FROM gcr.io/distroless/static:nonroot 24 | 25 | LABEL MAINTAINER=belastingdienst 26 | WORKDIR / 27 | COPY --from=builder /workspace/manager ./ 28 | 29 | ENTRYPOINT ["/manager"] 30 | -------------------------------------------------------------------------------- /manifests/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/cpet.belastingdienst.nl_paasconfig.yaml 6 | - bases/cpet.belastingdienst.nl_paas.yaml 7 | - bases/cpet.belastingdienst.nl_paasns.yaml 8 | # +kubebuilder:scaffold:crdkustomizeresource 9 | 10 | patches: 11 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 12 | # patches here are for enabling the conversion webhook for each CRD 13 | - path: patches/webhook_in_paas.yaml 14 | - path: patches/webhook_in_paasconfig.yaml 15 | - path: patches/webhook_in_paasns.yaml 16 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 17 | 18 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 19 | # patches here are for enabling the CA injection for each CRD 20 | #- path: patches/cainjection_in_paas.yaml 21 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 22 | 23 | # [WEBHOOK] To enable webhook, uncomment the following section 24 | # the following config is for teaching kustomize how to do kustomization for CRDs. 25 | 26 | configurations: 27 | - kustomizeconfig.yaml 28 | -------------------------------------------------------------------------------- /pkg/quota/quota.go: -------------------------------------------------------------------------------- 1 | package quota 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | resourcev1 "k8s.io/apimachinery/pkg/api/resource" 6 | ) 7 | 8 | // Quota holds a map of resource quantities 9 | // The main reason for having this as a separate type is to add methods 10 | type Quota map[corev1.ResourceName]resourcev1.Quantity 11 | 12 | // MergeWith can be used to merge 2 Quota blocks 13 | func (pq Quota) MergeWith(targetQuota map[corev1.ResourceName]resourcev1.Quantity) (q Quota) { 14 | q = make(Quota) 15 | for key, value := range targetQuota { 16 | q[key] = value 17 | } 18 | for key, value := range pq { 19 | q[key] = value 20 | } 21 | return q 22 | } 23 | 24 | // Resized can be used to scale a quota block 25 | func (pq Quota) Resized(scale float64) (q Quota) { 26 | q = make(Quota) 27 | for key, value := range pq { 28 | resized := value.AsApproximateFloat64() * scale 29 | q[key] = *(resourcev1.NewQuantity(int64(resized), value.Format)) 30 | } 31 | return q 32 | } 33 | 34 | // DeepCopy is a deepcopy function, copying the receiver, writing into out. in must be non-nil. 35 | func (pq Quota) DeepCopy() Quota { 36 | in, out := &pq, &Quota{} 37 | *out = make(Quota, len(*in)) 38 | for key, val := range *in { 39 | (*out)[key] = val.DeepCopy() 40 | } 41 | return *out 42 | } 43 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Pull Request type 4 | 5 | 6 | 7 | Please check the type of change your PR introduces: 8 | 9 | - [ ] Bugfix 10 | - [ ] Feature 11 | - [ ] Code style update (formatting, renaming) 12 | - [ ] Refactoring (no functional changes, no API changes) 13 | - [ ] Build-related changes 14 | - [ ] Documentation content changes 15 | - [ ] Other (please describe): 16 | 17 | ## What is the current behavior? 18 | 19 | 20 | 21 | Fixes: # (issue) 22 | 23 | ## What is the new behavior? 24 | 25 | 26 | 27 | - 28 | - 29 | - 30 | 31 | ## Does this introduce a breaking change? 32 | 33 | - [ ] Yes 34 | - [ ] No 35 | 36 | 37 | 38 | ## Other information 39 | 40 | -------------------------------------------------------------------------------- /pkg/templating/yaml_to_test.go: -------------------------------------------------------------------------------- 1 | package templating 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/belastingdienst/opr-paas/v4/pkg/fields" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestYamlToMap(t *testing.T) { 11 | exampleYaml := ` 12 | key1: val1 13 | key2: val2 14 | key3: valc 15 | key4: vald 16 | ` 17 | parsed, err := yamlToMap([]byte(exampleYaml)) 18 | assert.NoError(t, err) 19 | assert.Equal(t, fields.ElementMap{ 20 | "key1": "val1", 21 | "key2": "val2", 22 | "key3": "valc", 23 | "key4": "vald", 24 | }, 25 | parsed, 26 | ) 27 | } 28 | 29 | func TestResultMerge(t *testing.T) { 30 | var ( 31 | tr1 = fields.ElementMap{ 32 | "key1": "val1", 33 | "key2": "val2", 34 | } 35 | tr2 = fields.ElementMap{ 36 | "key2": "1", 37 | "key3": "val3", 38 | } 39 | expected = fields.ElementMap{ 40 | "key1": "val1", 41 | "key2": "1", 42 | "key3": "val3", 43 | } 44 | ) 45 | assert.Equal(t, expected, tr1.Merge(tr2)) 46 | } 47 | 48 | func TestYamlToList(t *testing.T) { 49 | exampleYaml := ` 50 | - vala 51 | - valb 52 | - val3 53 | - val4 54 | ` 55 | parsed, err := yamlToList([]byte(exampleYaml)) 56 | assert.NoError(t, err) 57 | expected := fields.ElementList{ 58 | "vala", 59 | "valb", 60 | "val3", 61 | "val4", 62 | } 63 | assert.Equal(t, expected, parsed) 64 | } 65 | -------------------------------------------------------------------------------- /docs/user-guide/02_capabilities.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Utilizing capabilities 3 | summary: A short overview of how to use capabilities. 4 | authors: 5 | - hikarukin 6 | - devotional-phoenix-97 7 | date: 2025-01-20 8 | --- 9 | 10 | ## Capabilities 11 | 12 | One of the core features of the Paas operator is to enable Paas users with capabilities. 13 | Capabilities need to be created and added to the cluster wide configuration of the 14 | Paas operator by administrators. After that Paas users can easily add the capabilities 15 | to their Paas. 16 | 17 | Read more about Paas capabilities in our [core concepts](../overview/core_concepts/capabilities.md) documentation. 18 | 19 | !!! example 20 | 21 | ```yaml 22 | apiVersion: cpet.belastingdienst.nl/v1alpha2 23 | kind: Paas 24 | metadata: 25 | name: tst-tst 26 | spec: 27 | capabilities: 28 | grafana: {} 29 | sso: 30 | quota: 31 | limits.cpu: '5' 32 | limits.memory: 8Gi 33 | requests.cpu: '2' 34 | requests.memory: 2Gi 35 | requests.storage: 100Gi 36 | tekton: 37 | quota: 38 | limits.cpu: '32' 39 | limits.memory: 32Gi 40 | requests.cpu: '16' 41 | requests.memory: 16Gi 42 | requests.storage: 40Gi 43 | ``` 44 | -------------------------------------------------------------------------------- /pkg/quota/quota_test.go: -------------------------------------------------------------------------------- 1 | package quota_test 2 | 3 | import ( 4 | "testing" 5 | 6 | paasquota "github.com/belastingdienst/opr-paas/v4/pkg/quota" 7 | "github.com/stretchr/testify/assert" 8 | corev1 "k8s.io/api/core/v1" 9 | resourcev1 "k8s.io/apimachinery/pkg/api/resource" 10 | ) 11 | 12 | func TestPaasQuotas_QuotaWithDefaults(t *testing.T) { 13 | testQuotas := map[corev1.ResourceName]resourcev1.Quantity{ 14 | "limits.cpu": resourcev1.MustParse("3"), 15 | "limits.memory": resourcev1.MustParse("6Gi"), 16 | "requests.cpu": resourcev1.MustParse("800m"), 17 | "requests.memory": resourcev1.MustParse("4Gi"), 18 | } 19 | defaultQuotas := map[corev1.ResourceName]resourcev1.Quantity{ 20 | "limits.cpu": resourcev1.MustParse("2"), 21 | "limits.memory": resourcev1.MustParse("5Gi"), 22 | "requests.cpu": resourcev1.MustParse("700m"), 23 | } 24 | quotas := make(paasquota.Quota) 25 | for key, value := range testQuotas { 26 | quotas[key] = value 27 | } 28 | defaultedQuotas := quotas.MergeWith(defaultQuotas) 29 | for key, value := range defaultedQuotas { 30 | if original, exists := quotas[key]; exists { 31 | assert.Equal(t, original, value) 32 | } 33 | } 34 | assert.Equal(t, defaultedQuotas["requests.memory"], 35 | resourcev1.MustParse("4Gi")) 36 | assert.NotEqual(t, defaultedQuotas["requests.cpu"], 37 | resourcev1.MustParse("700m")) 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/draft-release-on-push.yml: -------------------------------------------------------------------------------- 1 | name: Draft release on push to main 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | # branches to consider in the event; optional, defaults to all 7 | branches: 8 | - main 9 | pull_request: 10 | types: [opened, reopened, synchronize] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | update_release_draft: 17 | permissions: 18 | # write permission is required to create a GitHub release 19 | contents: write 20 | # write permission is required for autolabeler 21 | # otherwise, read permission is required at least 22 | pull-requests: write 23 | issues: write 24 | runs-on: ubuntu-latest 25 | steps: 26 | # (Optional) GitHub Enterprise requires GHE_HOST variable set 27 | #- name: Set GHE_HOST 28 | # run: | 29 | # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV 30 | 31 | # Drafts your next Release notes as Pull Requests are merged into "master" 32 | - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0 33 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 34 | # with: 35 | # config-name: my-config.yml 36 | # disable-autolabeler: true 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /manifests/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. 4 | apiVersion: cert-manager.io/v1 5 | kind: Issuer 6 | metadata: 7 | labels: 8 | app.kubernetes.io/name: opr-paas 9 | app.kubernetes.io/managed-by: kustomize 10 | name: selfsigned-issuer 11 | namespace: system 12 | spec: 13 | selfSigned: {} 14 | --- 15 | apiVersion: cert-manager.io/v1 16 | kind: Certificate 17 | metadata: 18 | labels: 19 | app.kubernetes.io/name: certificate 20 | app.kubernetes.io/instance: serving-cert 21 | app.kubernetes.io/component: certificate 22 | app.kubernetes.io/created-by: opr-paas 23 | app.kubernetes.io/part-of: opr-paas 24 | app.kubernetes.io/managed-by: kustomize 25 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 26 | namespace: system 27 | spec: 28 | # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize 29 | dnsNames: 30 | - SERVICE_NAME.SERVICE_NAMESPACE.svc 31 | - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local 32 | issuerRef: 33 | kind: Issuer 34 | name: selfsigned-issuer 35 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 36 | -------------------------------------------------------------------------------- /test/e2e/manifests/openshift/README.md: -------------------------------------------------------------------------------- 1 | # OpenShift 2 | 3 | As described in the docs, we integrate with OpenShift. 4 | In this way, we are able to bootstrap OpenShift resources for PaaS customers. 5 | 6 | As there is no lightweight container distribution of OpenShift to use during e2e-tests, we 7 | use a vanilla k8s cluster and mock all OpenShift dependencies. This implies we can only validate 8 | whether these mocked resources are created correctly, not the behaviour OpenShift applies to those resources. 9 | 10 | ## LCM 11 | 12 | While starting on the e2e-tests, we assume the upstream CRD updates are backwards compatible. 13 | Meaning, we assume we should be able to base the opr-paas on the current state of the CRDs. 14 | Ofcourse this is short-sighted, we will come up with a way to keep up with the upstream CRD and test 15 | whether the e2e-tests succeed when a new release of the GitOps operator has been issued. 16 | 17 | The upstream CRD of ClusterResourceQuota can be found here: 18 | https://github.com/openshift/api/blob/release-4.14/quota/v1/0000_03_quota-openshift_01_clusterresourcequota.crd.yaml 19 | 20 | The Group resources however, is baked into the OpenShift API. Therefore, there is no CRD available which we can install. 21 | As we can't mock / reproduce such thing, we choose to build our own CRD, based on the Group struct. 22 | 23 | The upstream source of the Group struct can be found here: 24 | https://github.com/openshift/api/blob/release-4.14/user/v1/types.go#L148 -------------------------------------------------------------------------------- /api/v1alpha1/paasns_conversion_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package v1alpha1 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/belastingdienst/opr-paas/v4/api/v1alpha2" 13 | "github.com/stretchr/testify/assert" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | ) 16 | 17 | var paasNsExV1Alpha1 = &PaasNS{ 18 | ObjectMeta: metav1.ObjectMeta{ 19 | Name: "foo", 20 | Namespace: "bar", 21 | }, 22 | Spec: PaasNSSpec{ 23 | Paas: "", 24 | Groups: []string{}, 25 | SSHSecrets: map[string]string{}, 26 | }, 27 | } 28 | 29 | var paasNsExV1Alpha2 = &v1alpha2.PaasNS{ 30 | ObjectMeta: metav1.ObjectMeta{ 31 | Name: "foo", 32 | Namespace: "bar", 33 | }, 34 | Spec: v1alpha2.PaasNSSpec{ 35 | Paas: "", 36 | Groups: []string{}, 37 | Secrets: map[string]string{}, 38 | }, 39 | } 40 | 41 | // Test conversion FROM v1alpha2 TO v1alpha1 42 | func TestConvertPaasNsTo(t *testing.T) { 43 | src := paasNsExV1Alpha2.DeepCopy() 44 | dst := &PaasNS{} 45 | 46 | err := dst.ConvertFrom(src) 47 | 48 | assert.NoError(t, err) 49 | assert.Equal(t, paasNsExV1Alpha1, dst) 50 | } 51 | 52 | // Test conversion FROM v1alpha1 TO v1alpha2 53 | func TestConvertPaasNsFrom(t *testing.T) { 54 | src := paasNsExV1Alpha1.DeepCopy() 55 | dst := &v1alpha2.PaasNS{} 56 | 57 | err := src.ConvertTo(dst) 58 | 59 | assert.NoError(t, err) 60 | assert.Equal(t, paasNsExV1Alpha2, dst) 61 | } 62 | -------------------------------------------------------------------------------- /docs/administrators-guide/install.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installing the Operator 3 | summary: A simple guide on installing the Paas Operator 4 | authors: 5 | - hikarukin 6 | date: 2024-10-14 7 | --- 8 | 9 | # Introduction 10 | 11 | Deploy the operator using the following command: 12 | 13 | ``` 14 | kubectl apply -f https://github.com/belastingdienst/opr-paas/releases/latest/download/install.yaml 15 | kubectl apply -f https://raw.githubusercontent.com/belastingdienst/opr-paas/refs/heads/main/examples/resources/_v1alpha2_paasconfig.yaml 16 | ``` 17 | 18 | The second command will load an example PaasConfig resource from the main branch 19 | to get you going. Feel free to replace this with your own or a release specific 20 | version instead. 21 | 22 | This will install the operator using the `install.yaml` that was generated for the 23 | latest release. It will create: 24 | 25 | - a namespace called `paas-system`; 26 | - 3 CRDs (`Paas`, `PaasNs` and `PaasConfig`); 27 | - a service account, role, role binding, cluster role & cluster role binding for 28 | all permissions required by the operator; **As the operator binds role for others the serviceaccount gets the: `bind` permission. 29 | It is advised to follow the principle of least privilege and scope the `permission` to only allow binding of the roles set in your 30 | operator config by setting `resourcesNames` in your role.yaml** 31 | - a viewer & an editor cluster role for all crds; 32 | - a deployment running the operator; 33 | 34 | Feel free to change config as required. -------------------------------------------------------------------------------- /docs/administrators-guide/security.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Security notes 3 | summary: A set of security related notes and tips with regards to running the Paas operator. 4 | authors: 5 | - hikarukin 6 | date: 2024-08-21 7 | --- 8 | 9 | # Introduction 10 | 11 | For any piece of software, security is of paramount concern. With the Paas operator, 12 | we aim to provide safe, secure and sane defaults for our settings. If you have any 13 | improvements you'd like to share, feel free to create an issue or a pull request (PR) 14 | in our source code repository. 15 | 16 | For more information on contributing to this project, please see the [Contributing](../about/contributing.md) section, 17 | [Developers Guide](../development-guide/index.md) section in this documentation and the 18 | `CONTRIBUTING.md` file in the root of our source code repository. 19 | 20 | Should you find a security issue, please refer to the 21 | [Raising security issues](../development-guide/21_security-issues.md) section. 22 | 23 | ## Things to be aware of 24 | 25 | ### Automount is set to true 26 | 27 | The operator makes use of a service account token that is used to communicate 28 | with the Kubernetes APIs. This service account token is automatically mounted 29 | using K8S's automount feature. 30 | 31 | It is a common best-practice for normal pods to opt-out of automatically mounting 32 | a service account token using `automountServiceAccountToken: false`. 33 | 34 | However, since this concerns an operator that needs the service account for most 35 | things it does, we have opted to keep the token auto-mounted. -------------------------------------------------------------------------------- /.github/workflows/ci-check-licenses.yml: -------------------------------------------------------------------------------- 1 | name: Check licenses and compatibility on PR 2 | on: 3 | pull_request: 4 | types: [ opened, synchronize, reopened, ready_for_review ] 5 | 6 | jobs: 7 | sbom: 8 | name: Generate SBOM and evaluate licenses 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 15 | with: 16 | persist-credentials: false 17 | 18 | - name: Generate SBOM 19 | uses: CycloneDX/gh-gomod-generate-sbom@efc74245d6802c8cefd925620515442756c70d8f # v2.0.0 20 | with: 21 | version: v1 22 | # added assert-licenses as required by dependency-track 23 | args: mod -licenses -assert-licenses -output sources_linux_amd64.sbom.xml 24 | 25 | - name: Evaluate license compatibility 26 | uses: mvdkleijn/licenses-action@6a6e38196451b10d8e263745301ecd660cf45035 # v1.2.3 27 | with: 28 | # We only use the linux_amd64 variant here for generating the LICENSES.md 29 | sbom: sources_linux_amd64.sbom.xml 30 | type: xml 31 | filename: tmp-LICENSES.md 32 | evaluate: true 33 | template: | 34 | # Licenses 35 | 36 | The following third-party licenses are applicable to this project: 37 | 38 | {{range .SortedKeys}}## {{.}} 39 | 40 | {{range index $.ComponentsByLicense .}}- {{.Name}} ({{.Version}}) 41 | {{end}} 42 | {{end}} 43 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to PaaS 2 | 3 | Welcome! We are glad that you want to contribute to our PaaS Operator project! 💖 4 | 5 | As you get started, you are in the best position to give us feedbacks on areas of 6 | our project that we need help with, including: 7 | 8 | * Problems found while setting up the development environment; 9 | * Gaps in our documentation; 10 | * Bugs in our GitHub actions; 11 | * Promotion of PostgreSQL on Kubernetes with our operator; 12 | 13 | First, though, it is important that you read the Code of Conduct. 14 | 15 | The guidelines below are a starting point. We don't want to limit your creativity, 16 | passion, and initiative. If you think there's a better way, please feel free to 17 | bring it up in a GitHub discussion, or open a pull request. We're certain there 18 | are always better ways to do things, we just need to start some constructive 19 | dialogue! 20 | 21 | ## Ways to contribute 22 | 23 | We welcome many types of contributions including: 24 | 25 | * New features; 26 | * Builds, CI/CD changes; 27 | * Bug fixes; 28 | * Documentation; 29 | * Issue Triage; 30 | * Answering questions on Github Discussions; 31 | * Communications / Social Media / Blog Posts; 32 | * Events participation; 33 | * Release management; 34 | 35 | For more details on development contributions, please refer to the "Developer Guide" 36 | section on the documentation site. 37 | 38 | ## Raising Issues 39 | 40 | When you want to raise an issue, use [GitHub Issues](https://github.com/belastingdienst/opr-paas/issues/new/choose). 41 | 42 | If you are trying to report a vulnerability, please make sure to read the [security policy](SECURITY.md). 43 | -------------------------------------------------------------------------------- /examples/resources/_v1alpha2_paas.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: cpet.belastingdienst.nl/v1alpha2 3 | kind: Paas 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: paas 7 | app.kubernetes.io/instance: paas-sample 8 | app.kubernetes.io/part-of: opr-paas 9 | app.kubernetes.io/managed-by: kustomize 10 | app.kubernetes.io/created-by: opr-paas 11 | name: aap-aap 12 | spec: 13 | namespaces: 14 | test: {} 15 | prod: 16 | groups: 17 | - appa 18 | - appart 19 | secrets: 20 | foo: c29tZXRoaW5nIHNlY3JldAo= 21 | requestor: acme 22 | groups: 23 | appa: 24 | users: 25 | - aap 26 | - paa 27 | appart: 28 | query: CN=appatest,OU=paas,OU=clusters,OU=corp,DC=prod,DC=acme,DC=org 29 | roles: 30 | - viewer 31 | appart2: 32 | query: CN=appatest,OU=paas,OU=clusters,OU=corp,DC=prod,DC=acme,DC=org 33 | roles: 34 | - viewer 35 | quota: 36 | limits.cpu: '13' 37 | limits.memory: 42Gi 38 | requests.cpu: '10' 39 | requests.memory: 32Gi 40 | requests.storage: 1024Gi 41 | thin.storageclass.storage.k8s.io/persistentvolumeclaims: '0' 42 | capabilities: 43 | argocd: 44 | quota: 45 | limits.cpu: '2' 46 | limits.memory: 5Gi 47 | requests.cpu: '1' 48 | requests.memory: 4Gi 49 | requests.storage: 20Gi 50 | custom_fields: 51 | git_url: ssh://git@scm.org/repo.git 52 | sso: {} 53 | tekton: 54 | quota: 55 | limits.cpu: '2' 56 | limits.memory: 5Gi 57 | requests.cpu: '1' 58 | requests.memory: 4Gi 59 | requests.storage: 20Gi 60 | -------------------------------------------------------------------------------- /docs/user-guide/01_basic-usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic usage 3 | summary: A description of the basic usage from the perspective of an end user. 4 | authors: 5 | - hikarukin 6 | date: 2024-07-04 7 | --- 8 | 9 | # Basic Usage 10 | 11 | ## Minimal Paas managed by another Paas 12 | 13 | Creating a configuration file to define a Paas is fairly straight forward. The 14 | configuration file should use the current API version `cpet.belastingdienst.nl/v1alpha2` 15 | and define a `kind: Paas`. 16 | 17 | The most minimal configuration requires at least a `name` in the `metadata` section 18 | and either a capability `argocd` that is `enabled`, or a `managedByPaas` entry. 19 | 20 | In the following example, we'll use the latter. The `managedByPaas` entry should 21 | contain the name of the Paas that is allowed to manage this Paas. 22 | 23 | Example Paas definition being managed by another Paas: 24 | 25 | !!! example 26 | 27 | ```yaml 28 | --- 29 | apiVersion: cpet.belastingdienst.nl/v1alpha2 30 | kind: Paas 31 | metadata: 32 | name: tst-tst 33 | spec: 34 | managedByPaas: trd-prt 35 | ``` 36 | 37 | ## Minimal Paas, self-managed using ArgoCD 38 | 39 | Example Paas definition, using its own ArgoCD: 40 | 41 | !!! example 42 | 43 | ```yaml 44 | --- 45 | apiVersion: cpet.belastingdienst.nl/v1alpha2 46 | kind: Paas 47 | metadata: 48 | name: tst-tst 49 | spec: 50 | capabilities: 51 | argocd: 52 | custom_fields: 53 | git_path: environments/production 54 | git_revision: main 55 | git_url: >- 56 | ssh://git@git.example.nl/example/example-repo.git 57 | ``` 58 | -------------------------------------------------------------------------------- /internal/config/informer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package config 8 | 9 | import ( 10 | "context" 11 | 12 | "sigs.k8s.io/controller-runtime/pkg/cache" 13 | "sigs.k8s.io/controller-runtime/pkg/manager" 14 | 15 | "github.com/belastingdienst/opr-paas/v4/api/v1alpha2" 16 | "github.com/belastingdienst/opr-paas/v4/internal/logging" 17 | ) 18 | 19 | type configInformer struct { 20 | mgr manager.Manager 21 | } 22 | 23 | // SetupPaasConfigInformer will add an informer to the manager and inform on PaasConfig changes 24 | func SetupPaasConfigInformer(mgr manager.Manager) error { 25 | // Adds informer for PaasConfig to force the cache to sync 26 | _, err := mgr.GetCache().GetInformer(context.Background(), &v1alpha2.PaasConfig{}) 27 | if err != nil { 28 | return err 29 | } 30 | return mgr.Add(&configInformer{mgr: mgr}) 31 | } 32 | 33 | // Start is the runnable for the PaasConfigInformer 34 | func (w *configInformer) Start(ctx context.Context) error { 35 | ctx, logger := logging.GetLogComponent(ctx, logging.ConfigComponent) 36 | logger.Info().Msg("starting config informer") 37 | 38 | <-ctx.Done() // Keep the goroutine alive 39 | return nil 40 | } 41 | 42 | func (w *configInformer) NeedLeaderElection() bool { 43 | // Returning false means that this runnable does not need LeaderElection 44 | return false // All replicas need to do this even though they might not be a leader 45 | } 46 | 47 | // GetCache satisfies hasCache interface so the manager knows to put this runnable in the cache group 48 | func (w *configInformer) GetCache() cache.Cache { 49 | return w.mgr.GetCache() 50 | } 51 | -------------------------------------------------------------------------------- /examples/resources/_v1alpha1_paas.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: cpet.belastingdienst.nl/v1alpha1 3 | kind: Paas 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: paas 7 | app.kubernetes.io/instance: paas-sample 8 | app.kubernetes.io/part-of: opr-paas 9 | app.kubernetes.io/managed-by: kustomize 10 | app.kubernetes.io/created-by: opr-paas 11 | name: aap-aap 12 | spec: 13 | namespaces: 14 | - test 15 | - prod 16 | requestor: acme 17 | groups: 18 | appa: 19 | users: 20 | - aap 21 | - paa 22 | appart: 23 | query: CN=appatest,OU=paas,OU=clusters,OU=corp,DC=prod,DC=acme,DC=org 24 | users: 25 | - test 26 | roles: 27 | - viewer 28 | appart2: 29 | query: CN=appatest,OU=paas,OU=clusters,OU=corp,DC=prod,DC=acme,DC=org 30 | users: 31 | - test 32 | roles: 33 | - viewer 34 | quota: 35 | limits.cpu: '13' 36 | limits.memory: 42Gi 37 | requests.cpu: '10' 38 | requests.memory: 32Gi 39 | requests.storage: 1024Gi 40 | thin.storageclass.storage.k8s.io/persistentvolumeclaims: '0' 41 | capabilities: 42 | argocd: 43 | enabled: true 44 | quota: 45 | limits.cpu: '2' 46 | limits.memory: 5Gi 47 | requests.cpu: '1' 48 | requests.memory: 4Gi 49 | requests.storage: 20Gi 50 | thin.storageclass.storage.k8s.io/persistentvolumeclaims: '0' 51 | gitUrl: https:// 52 | tekton: 53 | quota: 54 | limits.cpu: '2' 55 | limits.memory: 5Gi 56 | requests.cpu: '1' 57 | requests.memory: 4Gi 58 | requests.storage: 20Gi 59 | thin.storageclass.storage.k8s.io/persistentvolumeclaims: '0' 60 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package utils 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | ) 15 | 16 | // Join can be used to join 2 or more parts of a name to return a full name. Parts are joined by dashes. 17 | func Join(argv ...string) string { 18 | return strings.Join(argv, "-") 19 | } 20 | 21 | // PathToFileList can be fed multiple paths, which it will walk and return a fill list of all files in the path / 22 | // subdirectories 23 | func PathToFileList(paths []string) ([]string, error) { 24 | files := make(map[string]bool) 25 | for _, path := range paths { 26 | err := filepath.Walk(path, func(path string, info os.FileInfo, walkErr error) error { 27 | if walkErr != nil { 28 | return fmt.Errorf("error while walking the path: %w", walkErr) 29 | } 30 | 31 | resolvedPath, err := filepath.EvalSymlinks(path) 32 | if err != nil { 33 | return fmt.Errorf("failed to resolve symlink %s: %w", path, err) 34 | } 35 | 36 | absPath, err := filepath.Abs(resolvedPath) 37 | if err != nil { 38 | return fmt.Errorf("failed to get absolute path for %s: %w", resolvedPath, err) 39 | } 40 | 41 | absMode, err := os.Stat(absPath) 42 | if err != nil { 43 | return fmt.Errorf("failed to get filemode for %s: %w", absPath, err) 44 | } 45 | 46 | if absMode.Mode().IsRegular() { 47 | files[absPath] = true 48 | } 49 | 50 | return nil 51 | }) 52 | if err != nil { 53 | return nil, err 54 | } 55 | } 56 | var fileList []string 57 | for key := range files { 58 | fileList = append(fileList, key) 59 | } 60 | return fileList, nil 61 | } 62 | -------------------------------------------------------------------------------- /docs/administrators-guide/cluster-wide-quotas/basic-usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic usage 3 | summary: A basic usage description of how to use CWQs. 4 | authors: 5 | - devotional-phoenix-97 6 | - hikarukin 7 | date: 2024-07-01 8 | --- 9 | 10 | Basic usage of CWQs 11 | =================== 12 | 13 | With Cluster Wide Quotas, cluster admins can bring all resources for all Paas'es 14 | belonging to a capability together in one cluster wide resource pool. This brings 15 | down over commit at the expense of the risks associated with resource sharing. 16 | 17 | Use a quota per Paas 18 | -------------------- 19 | 20 | Set: 21 | 22 | - `paasconfig.spec.capabilities['tekton'].quotas.clusterwide` to `false` 23 | 24 | Use CWQs with one hard-set value 25 | -------------------------------- 26 | 27 | You can use CWQs with a single hard-set value (e.a. 10). 28 | 29 | Set: 30 | 31 | - `paasconfig.spec.capabilities['tekton'].quotas.clusterwide` to `true` 32 | - `paasconfig.spec.capabilities['tekton'].quotas.ratio` to `0` 33 | - `paasconfig.spec.capabilities['tekton'].quotas.min` to `10` 34 | 35 | Use CWQs with autoscaling 36 | ------------------------- 37 | 38 | You can use cluster wide quotas with an autoscaling feature. 39 | 40 | For this example: every Paas is expected to use 1 CPU, and a minimum of 3 CPU 41 | should always be available. Additionally, a maximum of 10 CPU can be reserved, 42 | and we scale down to 10% of normal usage. 43 | 44 | Set: 45 | 46 | - `paasconfig.spec.capabilities['tekton'].quotas.clusterwide` to `true` 47 | - `paasconfig.spec.capabilities['tekton'].quotas.default` to `1` 48 | - `paasconfig.spec.capabilities['tekton'].quotas.min` to `3` 49 | - `paasconfig.spec.capabilities['tekton'].quotas.max` to `10` 50 | - `paasconfig.spec.capabilities['tekton'].quotas.ratio` to `0.1` (10%) 51 | -------------------------------------------------------------------------------- /api/v1alpha1/paasns_conversion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package v1alpha1 8 | 9 | import ( 10 | "context" 11 | "fmt" 12 | 13 | "github.com/belastingdienst/opr-paas/v4/api/v1alpha2" 14 | "github.com/belastingdienst/opr-paas/v4/internal/logging" 15 | "sigs.k8s.io/controller-runtime/pkg/conversion" 16 | ) 17 | 18 | // ConvertFrom converts the Hub version (v1alpha2) to this Paas (v1alpha1). 19 | func (p *PaasNS) ConvertFrom(srcRaw conversion.Hub) error { 20 | src, ok := srcRaw.(*v1alpha2.PaasNS) 21 | if !ok { 22 | return fmt.Errorf("cannot convert to v1alpha1: got %T", srcRaw) 23 | } 24 | 25 | _, logger := logging.GetLogComponent(context.TODO(), logging.ApiComponent) 26 | logger.Debug().Msg("Starting conversion from hub (v1alpha2) to spoke (v1alpha1)") 27 | 28 | p.ObjectMeta = src.ObjectMeta 29 | // Deprecated: not required once paas controller is managing the PaasNS resources. 30 | // The `metadata.name` of the Paas which created the namespace in which this PaasNS is applied 31 | p.Spec.Paas = "" 32 | p.Spec.Groups = src.Spec.Groups 33 | p.Spec.SSHSecrets = src.Spec.Secrets 34 | 35 | return nil 36 | } 37 | 38 | // ConvertTo converts this Paas (v1alpha1) to the Hub version (v1alpha2). 39 | func (p *PaasNS) ConvertTo(dstRaw conversion.Hub) error { 40 | dst, ok := dstRaw.(*v1alpha2.PaasNS) 41 | if !ok { 42 | return fmt.Errorf("cannot convert from v1alpha1: got %T", dstRaw) 43 | } 44 | 45 | _, logger := logging.GetLogComponent(context.TODO(), logging.ApiComponent) 46 | logger.Debug().Msg("Starting conversion from spoke (v1alpha1) to hub (v1alpha2)") 47 | 48 | dst.ObjectMeta = p.ObjectMeta 49 | dst.Spec.Groups = p.Spec.Groups 50 | dst.Spec.Secrets = p.Spec.SSHSecrets 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /api/v1alpha1/validations.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import "regexp" 4 | 5 | // PaasConfigTypeValidations can have custom validations for a specific CRD (e.a. paas, paasConfig or PaasNs). 6 | // Refer to https://belastingdienst.github.io/opr-paas/latest/administrators-guide/validations/ for more info. 7 | type PaasConfigTypeValidations map[string]string 8 | 9 | // PaasConfigValidations is a map which holds all validations, 10 | // with key being the (lower case) name of the crd and value being a PaasConfigTypeValidations object. 11 | type PaasConfigValidations map[string]PaasConfigTypeValidations 12 | 13 | // getValidationRE is an internal function which checks if a validation RE is configured 14 | // and returns a Regexp object if it is, or nil if it isn't 15 | func (pctv PaasConfigTypeValidations) getValidationRE(fieldName string) *regexp.Regexp { 16 | validation, exists := pctv[fieldName] 17 | if !exists { 18 | return nil 19 | } 20 | return regexp.MustCompile(validation) 21 | } 22 | 23 | // GetValidationRE can be used to get a validation for a crd by name 24 | // and returns a Regexp object if it is, or nil if it isn't 25 | func (pcv PaasConfigValidations) GetValidationRE(crd string, fieldName string) *regexp.Regexp { 26 | validations, exists := pcv[crd] 27 | if !exists { 28 | return nil 29 | } 30 | return validations.getValidationRE(fieldName) 31 | } 32 | 33 | // GetValidationRE can be used to get a validation for a crd by name 34 | // and returns a Regexp object if it is, or nil if it isn't 35 | // This method exists for a PaasConfig and for a PaasConfigValidations, where the former is safe to use 36 | // even when paasConfig.Spec.Validations is not set (making it nil) 37 | func (pc PaasConfig) GetValidationRE(crd string, fieldName string) *regexp.Regexp { 38 | if pc.Spec.Validations == nil { 39 | return nil 40 | } 41 | return pc.Spec.Validations.GetValidationRE(crd, fieldName) 42 | } 43 | -------------------------------------------------------------------------------- /test/e2e/steps.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | // Reusable step functions for end-to-end tests 4 | 5 | import ( 6 | "context" 7 | "testing" 8 | 9 | api "github.com/belastingdienst/opr-paas/v4/api/v1alpha2" 10 | 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "sigs.k8s.io/e2e-framework/pkg/envconf" 13 | "sigs.k8s.io/e2e-framework/pkg/types" 14 | ) 15 | 16 | // createPaasFn accepts a valid Paas spec object and a name and creates the Paas resource, 17 | // waiting for successful creation. 18 | func createPaasFn(name string, paasSpec api.PaasSpec) types.StepFunc { 19 | return createPaasWithCondFn(name, paasSpec, api.TypeReadyPaas) 20 | } 21 | 22 | // createPaasWithCondFn accepts an invalid Paas spec object and a name and creates the Paas resource, 23 | // waiting for the given condition to be true. 24 | func createPaasWithCondFn(name string, paasSpec api.PaasSpec, readyCondition string) types.StepFunc { 25 | return func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { 26 | paas := &api.Paas{ 27 | ObjectMeta: metav1.ObjectMeta{Name: name}, 28 | Spec: paasSpec, 29 | } 30 | 31 | if err := createSync(ctx, cfg, paas, readyCondition); err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | return ctx 36 | } 37 | } 38 | 39 | // teardownPaasFn deletes the Paas if it still exists (e.g. if deleting the Paas is not part of the test steps, or if an 40 | // earlier assertion failed causing the deletion step to be skipped). 41 | // Can be called as `.Teardown(teardownPaasFn("paas-name"))` 42 | func teardownPaasFn(paasName string) types.StepFunc { 43 | return func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { 44 | paas := &api.Paas{ObjectMeta: metav1.ObjectMeta{Name: paasName}} 45 | 46 | // Paas is deleted synchronously to prevent race conditions between test invocations 47 | _ = deleteResourceSync(ctx, cfg, paas) 48 | 49 | return ctx 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/update-trivy-cache.yml: -------------------------------------------------------------------------------- 1 | # Note: This workflow only updates the cache. You should create a separate workflow 2 | # for your actual Trivy scans. In your scan workflow, set TRIVY_SKIP_DB_UPDATE=true 3 | # and TRIVY_SKIP_JAVA_DB_UPDATE=true. 4 | # 5 | # src: https://github.com/marketplace/actions/aqua-security-trivy#cache 6 | name: Update Trivy Cache 7 | 8 | on: 9 | schedule: 10 | - cron: '23 0 * * *' # Run daily at 23 past midnight UTC (23 chosen randomly) 11 | workflow_dispatch: # Allow manual triggering 12 | 13 | # In order to update cache, needs `write` permission, see: 14 | # https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps?apiVersion=2022-11-28#repository-permissions-for-actions 15 | permissions: 16 | actions: write 17 | 18 | jobs: 19 | update-trivy-db: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Get current date 23 | id: date 24 | run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT 25 | 26 | - uses: oras-project/setup-oras@22ce207df3b08e061f537244349aac6ae1d214f6 # v1.2.4 27 | 28 | - name: Download and extract the vulnerability DB 29 | run: | 30 | mkdir -p $GITHUB_WORKSPACE/.cache/trivy/db 31 | oras pull ghcr.io/aquasecurity/trivy-db:2 32 | tar -xzf db.tar.gz -C $GITHUB_WORKSPACE/.cache/trivy/db 33 | rm db.tar.gz 34 | 35 | - name: Download and extract the Java DB 36 | run: | 37 | mkdir -p $GITHUB_WORKSPACE/.cache/trivy/java-db 38 | oras pull ghcr.io/aquasecurity/trivy-java-db:1 39 | tar -xzf javadb.tar.gz -C $GITHUB_WORKSPACE/.cache/trivy/java-db 40 | rm javadb.tar.gz 41 | 42 | - name: Cache DBs 43 | uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 44 | with: 45 | path: ${{ github.workspace }}/.cache/trivy 46 | key: cache-trivy-${{ steps.date.outputs.date }} -------------------------------------------------------------------------------- /manifests/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: paas-manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - namespaces 11 | - secrets 12 | verbs: 13 | - create 14 | - delete 15 | - get 16 | - list 17 | - patch 18 | - update 19 | - watch 20 | - apiGroups: 21 | - argoproj.io 22 | resources: 23 | - applicationsets 24 | verbs: 25 | - get 26 | - list 27 | - patch 28 | - watch 29 | - apiGroups: 30 | - cpet.belastingdienst.nl 31 | resources: 32 | - paas 33 | - paasconfig 34 | - paasns 35 | verbs: 36 | - create 37 | - delete 38 | - get 39 | - list 40 | - patch 41 | - update 42 | - watch 43 | - apiGroups: 44 | - cpet.belastingdienst.nl 45 | resources: 46 | - paas/finalizers 47 | - paasconfig/finalizers 48 | - paasns/finalizers 49 | verbs: 50 | - update 51 | - apiGroups: 52 | - cpet.belastingdienst.nl 53 | resources: 54 | - paas/status 55 | - paasconfig/status 56 | - paasns/status 57 | verbs: 58 | - get 59 | - patch 60 | - update 61 | - apiGroups: 62 | - quota.openshift.io 63 | resources: 64 | - clusterresourcequotas 65 | verbs: 66 | - create 67 | - delete 68 | - get 69 | - list 70 | - patch 71 | - update 72 | - watch 73 | - apiGroups: 74 | - rbac.authorization.k8s.io 75 | resources: 76 | - clusterrolebindings 77 | - rolebindings 78 | verbs: 79 | - create 80 | - delete 81 | - get 82 | - list 83 | - patch 84 | - update 85 | - watch 86 | - apiGroups: 87 | - rbac.authorization.k8s.io 88 | resources: 89 | - clusterroles 90 | verbs: 91 | - bind 92 | - apiGroups: 93 | - user.openshift.io 94 | resources: 95 | - groups 96 | verbs: 97 | - create 98 | - delete 99 | - get 100 | - list 101 | - patch 102 | - update 103 | - watch 104 | -------------------------------------------------------------------------------- /manifests/webservice/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: paas-webservice 5 | namespace: system 6 | labels: 7 | app.kubernetes.io/component: webservice 8 | app.kubernetes.io/part-of: opr-paas 9 | spec: 10 | selector: 11 | matchLabels: 12 | app.kubernetes.io/component: webservice 13 | app.kubernetes.io/part-of: opr-paas 14 | replicas: 1 15 | template: 16 | metadata: 17 | labels: 18 | app.kubernetes.io/component: webservice 19 | app.kubernetes.io/part-of: opr-paas 20 | spec: 21 | automountServiceAccountToken: false 22 | securityContext: 23 | runAsNonRoot: true 24 | containers: 25 | - command: 26 | - /webservice 27 | env: 28 | - name: PAAS_PUBLIC_KEY_PATH 29 | value: /secrets/paas/publicKey 30 | - name: PAAS_WS_ALLOWED_ORIGINS 31 | value: http://www.example.com 32 | image: webservice:latest 33 | imagePullPolicy: Always 34 | name: webservice 35 | livenessProbe: 36 | httpGet: 37 | path: /healthz 38 | port: 8080 39 | initialDelaySeconds: 15 40 | periodSeconds: 20 41 | readinessProbe: 42 | httpGet: 43 | path: /readyz 44 | port: 8080 45 | initialDelaySeconds: 15 46 | periodSeconds: 10 47 | resources: 48 | limits: 49 | cpu: 200m 50 | memory: 150Mi 51 | requests: 52 | cpu: 100m 53 | memory: 75Mi 54 | volumeMounts: 55 | - name: paas-public-key 56 | mountPath: /secrets/paas 57 | terminationGracePeriodSeconds: 10 58 | volumes: 59 | - name: paas-public-key 60 | configMap: 61 | name: paas-secrets-publickey 62 | defaultMode: 420 63 | -------------------------------------------------------------------------------- /internal/logging/components_test.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestComponentToString(t *testing.T) { 12 | var nonexistentComponent Component = -1 13 | assert.Equal(t, componentToString(nonexistentComponent), componentToString(UnknownComponent)) 14 | } 15 | 16 | func TestNewComponentsFromString(t *testing.T) { 17 | var ( 18 | components = []string{ 19 | "plugin_generator", 20 | "config_watcher", 21 | "undefined_component", 22 | "unittest_component", 23 | } 24 | commaSeparatedString = strings.Join(components, ",") 25 | result = NewComponentsFromString(commaSeparatedString) 26 | ) 27 | for _, compName := range components { 28 | comp, exists := componentConverter[compName] 29 | require.True(t, exists) 30 | require.Contains(t, result, comp) 31 | assert.True(t, result[comp]) 32 | } 33 | } 34 | 35 | func TestNewComponentsFromStringMap(t *testing.T) { 36 | var ( 37 | trueComponents = []string{ 38 | "plugin_generator", 39 | "undefined_component", 40 | } 41 | falseComponents = []string{ 42 | "config_watcher", 43 | "unittest_component", 44 | } 45 | strMap = map[string]bool{} 46 | ) 47 | for _, compName := range trueComponents { 48 | strMap[compName] = true 49 | } 50 | for _, compName := range falseComponents { 51 | strMap[compName] = false 52 | } 53 | result := NewComponentsFromStringMap(strMap) 54 | 55 | for _, compName := range trueComponents { 56 | comp, exists := componentConverter[compName] 57 | require.True(t, exists) 58 | enabled, exists := result[comp] 59 | require.True(t, exists) 60 | require.True(t, enabled) 61 | } 62 | for _, compName := range falseComponents { 63 | comp, exists := componentConverter[compName] 64 | require.True(t, exists) 65 | enabled, exists := result[comp] 66 | require.True(t, exists) 67 | require.False(t, enabled) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /docs/user-guide/02_groups-and-users.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Managing permissions 3 | summary: A short overview of defining authorization for users and groups 4 | authors: 5 | - hikarukin 6 | - devotional-phoenix-97 7 | date: 2025-01-21 8 | --- 9 | 10 | ## Groups and Users 11 | 12 | For every Paas it is possible to define which k8s groups have permissions on resources belonging to the Paas. 13 | Additionally, Administrators can define [rolemappings](../overview/core_concepts/authorization.md#paasconfig), 14 | and groups in a Paas can have these functional roles applied. 15 | 16 | It is possible to manage group membership externally, with an LDAP sync solution based on `oc adm group sync`. 17 | 18 | for now, it is also possible to have group membership managed by the Paas operator, by specifying users. 19 | But, we are working towards getting rid of user management through Paas, relying only on externally managed groups. 20 | 21 | For more information on authorization, please see [Core Concepts - Authorization](../overview/core_concepts/authorization.md). 22 | 23 | !!! note 24 | 25 | When both an LDAP query and a list of users is defined, the LDAP query takes precedence 26 | above the users. The paas operator will, in that case, not create a group, relying on the `oc adm group sync` to manage it. 27 | 28 | !!! example 29 | 30 | ```yaml 31 | apiVersion: cpet.belastingdienst.nl/v1alpha2 32 | kind: Paas 33 | metadata: 34 | name: tst-tst 35 | spec: 36 | groups: 37 | example_group: 38 | query: >- 39 | CN=example_group,OU=example,OU=UID,DC=example,DC=nl 40 | # Apply edit permissions for users in this group ; see PaasConfig rolemappings for more info 41 | roles: 42 | - edit 43 | second_example_group: 44 | users: 45 | - jdsmith 46 | # Apply admin permissions for users in this group ; see PaasConfig rolemappings for more info 47 | roles: 48 | - admin 49 | ``` -------------------------------------------------------------------------------- /docs/administrators-guide/feature-flags.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuring feature flags 3 | summary: A detailed description of configuring features. 4 | authors: 5 | - Devotional Phoenix 6 | date: 2025-07-21 7 | --- 8 | 9 | # Configuring features 10 | 11 | To offer a configurable path to introduce new features, deprecate obsolete features and fine tune some implemented features, 12 | the Paas operator offers feature flags. 13 | 14 | ## Warn or block groups with user management 15 | 16 | Currently the only implemented Feature Flag is for the behavior when users have defined usernames in the Paas.Spec.Groups blocks. 17 | 18 | ### Allow (default) 19 | 20 | When specifying `allow` (or leave empty), the operator reports no errors / warnings. 21 | 22 | !!! example 23 | 24 | ```yml 25 | apiVersion: cpet.belastingdienst.nl/v1alpha1 26 | kind: PaasConfig 27 | metadata: 28 | name: opr-paas-config 29 | spec: 30 | feature_flags: 31 | group_user_management: allow 32 | ``` 33 | 34 | ### Warn 35 | 36 | The option `warn` can be used to have the WebHook warn about users being set, without declining the request, 37 | and have the controller log warnings to console and the Paas Status block. 38 | 39 | !!! example 40 | 41 | ```yml 42 | apiVersion: cpet.belastingdienst.nl/v1alpha2 43 | kind: PaasConfig 44 | metadata: 45 | name: opr-paas-config 46 | spec: 47 | feature_flags: 48 | group_user_management: warn 49 | ``` 50 | 51 | ### Block 52 | 53 | The option `block` can be set to decline requests with users being set in the Groups block, 54 | have the controller log warnings to console and the Paas Status block, 55 | and have the controller remove groups that have previously been defined. 56 | 57 | !!! example 58 | 59 | ```yml 60 | apiVersion: cpet.belastingdienst.nl/v1alpha2 61 | kind: PaasConfig 62 | metadata: 63 | name: opr-paas-config 64 | spec: 65 | feature_flags: 66 | group_user_management: block 67 | ``` -------------------------------------------------------------------------------- /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: cpet.belastingdienst.nl 6 | layout: 7 | - go.kubebuilder.io/v4 8 | plugins: 9 | manifests.sdk.operatorframework.io/v2: {} 10 | scorecard.sdk.operatorframework.io/v2: {} 11 | projectName: opr-paas 12 | repo: github.com/belastingdienst/opr-paas 13 | resources: 14 | - api: 15 | crdVersion: v1 16 | controller: true 17 | domain: cpet.belastingdienst.nl 18 | kind: Paas 19 | path: github.com/belastingdienst/opr-paas/api/v1alpha1 20 | version: v1alpha1 21 | webhooks: 22 | validation: true 23 | conversion: true 24 | spoke: 25 | - v1alpha2 26 | webhookVersion: v1 27 | - api: 28 | crdVersion: v1 29 | namespaced: true 30 | controller: true 31 | domain: cpet.belastingdienst.nl 32 | kind: PaasNS 33 | path: github.com/belastingdienst/opr-paas/api/v1alpha1 34 | version: v1alpha1 35 | webhooks: 36 | validation: true 37 | #conversion: true 38 | spoke: 39 | - v1alpha2 40 | webhookVersion: v1 41 | - api: 42 | crdVersion: v1 43 | controller: true 44 | domain: cpet.belastingdienst.nl 45 | kind: PaasConfig 46 | path: github.com/belastingdienst/opr-paas/api/v1alpha1 47 | version: v1alpha1 48 | - api: 49 | crdVersion: v1 50 | controller: true 51 | domain: cpet.belastingdienst.nl 52 | kind: PaasConfig 53 | path: github.com/belastingdienst/opr-paas/api/v1alpha2 54 | version: v1alpha2 55 | - api: 56 | crdVersion: v1 57 | domain: cpet.belastingdienst.nl 58 | kind: Paas 59 | path: github.com/belastingdienst/opr-paas/api/v1alpha2 60 | version: v1alpha2 61 | - api: 62 | crdVersion: v1 63 | namespaced: true 64 | controller: true 65 | domain: cpet.belastingdienst.nl 66 | kind: PaasNS 67 | path: github.com/belastingdienst/opr-paas/api/v1alpha2 68 | version: v1alpha2 69 | version: "3" 70 | -------------------------------------------------------------------------------- /api/v1alpha2/validations.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package v1alpha2 8 | 9 | import "regexp" 10 | 11 | // PaasConfigTypeValidations can have custom validations for a specific CRD (e.a. paas, paasConfig or PaasNs). 12 | // Refer to https://belastingdienst.github.io/opr-paas/latest/administrators-guide/validations/ for more info. 13 | type PaasConfigTypeValidations map[string]string 14 | 15 | // PaasConfigValidations is a map which holds all validations, 16 | // with key being the (lower case) name of the crd and value being a PaasConfigTypeValidations object. 17 | type PaasConfigValidations map[string]PaasConfigTypeValidations 18 | 19 | // getValidationRE is an internal function which checks if a validation RE is configured 20 | // and returns a Regexp object if it is, or nil if it isn't 21 | func (pctv PaasConfigTypeValidations) getValidationRE(fieldName string) *regexp.Regexp { 22 | validation, exists := pctv[fieldName] 23 | if !exists { 24 | return nil 25 | } 26 | return regexp.MustCompile(validation) 27 | } 28 | 29 | // GetValidationRE can be used to get a validation for a crd by name 30 | // and returns a Regexp object if it is, or nil if it isn't 31 | func (pcv PaasConfigValidations) GetValidationRE(crd string, fieldName string) *regexp.Regexp { 32 | validations, exists := pcv[crd] 33 | if !exists { 34 | return nil 35 | } 36 | return validations.getValidationRE(fieldName) 37 | } 38 | 39 | // GetValidationRE can be used to get a validation for a crd by name 40 | // and returns a Regexp object if it is, or nil if it isn't 41 | // This method exists for a PaasConfig and for a PaasConfigValidations, where the former is safe to use 42 | // even when paasConfig.Spec.Validations is not set (making it nil) 43 | func (pc PaasConfig) GetValidationRE(crd string, fieldName string) *regexp.Regexp { 44 | if pc.Spec.Validations == nil { 45 | return nil 46 | } 47 | return pc.Spec.Validations.GetValidationRE(crd, fieldName) 48 | } 49 | -------------------------------------------------------------------------------- /docs/administrators-guide/validations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Validating field names 3 | summary: Options to configure validation of field names 4 | authors: 5 | - devotional-phoenix 6 | date: 2025-02-27 7 | --- 8 | 9 | # Introduction 10 | 11 | Most fields in the CRD are checked directly with kubebuilder validations, 12 | But some fields can be validated with a regular expression that is configurable through the PaasConfig: 13 | 14 | Below snippet shows how validations can be configured for the complete set of vailable validations: 15 | 16 | !!! example 17 | 18 | ```yml 19 | apiVersion: cpet.belastingdienst.nl/v1alpha2 20 | kind: PaasConfig 21 | metadata: 22 | name: opr-paas-config 23 | spec: 24 | validations: 25 | paas: 26 | # (v1.12) Validate name of Paas 27 | name: "^[a-z0-9-]*$" 28 | # (v1.12) Validate name of groups in paas 29 | groupName: "^[a-z0-9-]*$" 30 | # (v1.12) Validate name of namespaces in paas 31 | namespaceName: "^[a-z0-9-]*$" 32 | # (v1.12) Validate requestor field in paas 33 | requestor: "^[a-z0-9-]*$" 34 | # (v3.6) Allow quotas (by default all is allowed, by setting a regular expressions you can limit 35 | # allowed quotas). Below example disallows limits.cpu (a.o.) and only allows the 4 types as stated. 36 | # This option has effect on a Paas, but also all quota's in PaasConfig.spec.capabilities[*].quotas. 37 | allowedQuotas: "^(limits.memory|requests.cpu|requests.memory|requests.storage)$" 38 | paasConfig: 39 | # (v1.12) Validate name of capability in config 40 | capabilityName: "^[a-z0-9-]*$" 41 | paasNs: 42 | # (v1.12) Validate name of paasNs 43 | name: "^[a-z0-9-]*$" 44 | ... 45 | ``` 46 | 47 | !!! note 48 | 49 | If only one of `PaasConfig.spec.validations.paas.namespaceName`, and `PaasConfig.validations.paasNs.name` is set, 50 | both PaasNs names and Paas.Spec.Namespaces are validated with the same validation rule. 51 | -------------------------------------------------------------------------------- /pkg/fields/entries.go: -------------------------------------------------------------------------------- 1 | package fields 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 8 | ) 9 | 10 | // Entries represents all entries in the list of the listgenerator 11 | // This is a map so that values are unique, the key is the paas entry 12 | type Entries map[string]ElementMap 13 | 14 | // Merge merges all key/value pairs from another Entries on top of this and returns the resulting total Entries set 15 | func (en Entries) Merge(added Entries) (entries Entries) { 16 | entries = make(Entries) 17 | for key, value := range en { 18 | entries[key] = value 19 | } 20 | for key, value := range added { 21 | if sourceValue, exists := entries[key]; exists { 22 | entries[key] = sourceValue.Merge(value) 23 | } else { 24 | entries[key] = value 25 | } 26 | } 27 | return entries 28 | } 29 | 30 | // AsJSON can be used to convert Entries into JSON data 31 | func (en Entries) AsJSON() ([]apiextensionsv1.JSON, error) { 32 | var list []apiextensionsv1.JSON 33 | keys := make([]string, 0, len(en)) 34 | for k := range en { 35 | keys = append(keys, k) 36 | } 37 | sort.Strings(keys) 38 | for _, key := range keys { 39 | entry := en[key] 40 | data, err := entry.AsJSON() 41 | if err != nil { 42 | return nil, err 43 | } 44 | list = append(list, apiextensionsv1.JSON{Raw: data}) 45 | } 46 | return list, nil 47 | } 48 | 49 | // FromJSON can be used to pass a list of json data and fill the values of this Entries with the result 50 | func (en *Entries) FromJSON(key string, data []apiextensionsv1.JSON) error { 51 | self := *en 52 | for _, raw := range data { 53 | entry, err := ElementMapFromJSON(raw.Raw) 54 | if err != nil { 55 | return err 56 | } 57 | value, exists := entry[key] 58 | if !exists { 59 | return fmt.Errorf(`json data "%s" does not contain a "%s" field`, raw, key) 60 | } 61 | name, ok := value.(string) 62 | if !ok { 63 | return fmt.Errorf(`json data "%s" has a "%s" field, but it is not a string`, raw, key) 64 | } 65 | self[name] = entry 66 | } 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /internal/argocd-plugin-generator/middleware_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package argocd_plugin_generator 8 | 9 | import ( 10 | "io" 11 | "net/http" 12 | "net/http/httptest" 13 | "strconv" 14 | "testing" 15 | 16 | "github.com/prometheus/client_golang/prometheus/testutil" 17 | ) 18 | 19 | func TestWithMetrics_IncrementsCounter(t *testing.T) { 20 | // Reset metrics before test 21 | PluginGeneratorRequestTotal.Reset() 22 | 23 | tests := []struct { 24 | name string 25 | handler http.HandlerFunc 26 | wantStatus int 27 | }{ 28 | { 29 | name: "200 OK", 30 | handler: func(w http.ResponseWriter, r *http.Request) { 31 | w.WriteHeader(http.StatusOK) 32 | _, err := io.WriteString(w, "ok") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | }, 37 | wantStatus: http.StatusOK, 38 | }, 39 | { 40 | name: "404 Not Found", 41 | handler: func(w http.ResponseWriter, r *http.Request) { 42 | http.NotFound(w, r) 43 | }, 44 | wantStatus: http.StatusNotFound, 45 | }, 46 | { 47 | name: "500 Internal Server Error", 48 | handler: func(w http.ResponseWriter, r *http.Request) { 49 | http.Error(w, "fail", http.StatusInternalServerError) 50 | }, 51 | wantStatus: http.StatusInternalServerError, 52 | }, 53 | } 54 | 55 | for _, tt := range tests { 56 | t.Run(tt.name, func(t *testing.T) { 57 | rec := httptest.NewRecorder() 58 | req := httptest.NewRequest(http.MethodGet, "/", nil) 59 | 60 | handler := withMetrics(tt.handler) 61 | handler.ServeHTTP(rec, req) 62 | 63 | if rec.Code != tt.wantStatus { 64 | t.Errorf("expected status %d, got %d", tt.wantStatus, rec.Code) 65 | } 66 | 67 | // Verify counter increment 68 | got := testutil.ToFloat64(PluginGeneratorRequestTotal.WithLabelValues(strconv.Itoa(tt.wantStatus))) 69 | if got != 1 { 70 | t.Errorf("expected counter 1 for status %d, got %v", tt.wantStatus, got) 71 | } 72 | 73 | // Reset for next run 74 | PluginGeneratorRequestTotal.Reset() 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /api/v1alpha1/paas_types_ginkgo_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package v1alpha1 8 | 9 | import ( 10 | "context" 11 | "fmt" 12 | 13 | "github.com/belastingdienst/opr-paas/v4/pkg/quota" 14 | . "github.com/onsi/ginkgo/v2" 15 | . "github.com/onsi/gomega" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | ) 18 | 19 | var _ = Describe("Namespace Validation", func() { 20 | const resourceName = "test-paas" 21 | 22 | Context("Valid namespacesnames", func() { 23 | It("should accept a valid namespace", func() { 24 | validNamespace := []string{"valid-namespace-example"} 25 | paas := &Paas{ 26 | ObjectMeta: metav1.ObjectMeta{ 27 | Name: resourceName, 28 | }, 29 | Spec: PaasSpec{ 30 | Quota: make(quota.Quota), 31 | Requestor: "valid-requestor", 32 | Namespaces: validNamespace, 33 | }, 34 | } 35 | 36 | err := k8sClient.Create(context.TODO(), paas) 37 | Expect(err).NotTo(HaveOccurred()) 38 | Expect(k8sClient.Delete(context.Background(), paas)).To(Succeed()) 39 | }) 40 | }) 41 | 42 | Context("Invalid namespacenames", func() { 43 | DescribeTable("should reject invalid names", 44 | func(namespaces []string) { 45 | paas := &Paas{ 46 | ObjectMeta: metav1.ObjectMeta{ 47 | Name: resourceName, 48 | }, 49 | Spec: PaasSpec{ 50 | Quota: make(quota.Quota), 51 | Requestor: "valid-requestor", 52 | Namespaces: namespaces, 53 | }, 54 | } 55 | 56 | err := k8sClient.Create(context.TODO(), paas) 57 | Expect(err).To(HaveOccurred()) // Expect validation to fail 58 | }, 59 | Entry("starts with a hyphen", []string{"-invalid.com"}), 60 | Entry("contains a dot", []string{"valid", "invalid.com"}), 61 | Entry("ends with a hyphen", []string{"invalid-"}), 62 | Entry("contains uppercase letters", []string{"Invalid-com"}), 63 | Entry("contains special characters", []string{"invalid!name.com"}), 64 | Entry("exceeds max length", []string{fmt.Sprintf("%s-com", string(make([]byte, 254)))}), 65 | ) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /.github/workflows/build-and-release-sbom.yml: -------------------------------------------------------------------------------- 1 | name: Build add SBOM and LICENSES.md to release 2 | on: 3 | release: 4 | types: 5 | - published 6 | 7 | jobs: 8 | sbom: 9 | name: Generate and upload SBOM 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | strategy: 14 | matrix: 15 | goos: [linux, darwin] 16 | goarch: [amd64, arm64] 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 20 | with: 21 | persist-credentials: false 22 | 23 | - name: Generate SBOM 24 | uses: CycloneDX/gh-gomod-generate-sbom@efc74245d6802c8cefd925620515442756c70d8f # v2.0.0 25 | with: 26 | version: v1 27 | # added assert-licenses as required by dependency-track 28 | args: mod -licenses -assert-licenses -output sources_${{ matrix.goos }}_${{ matrix.goarch }}.sbom.xml 29 | env: 30 | GOARCH: ${{ matrix.goarch }} 31 | GOOS: ${{ matrix.goos }} 32 | 33 | - name: Generate LICENSES.md and evaluate licenses 34 | if: ${{ matrix.goarch == 'amd64' && matrix.goos == 'linux' }} 35 | uses: mvdkleijn/licenses-action@6a6e38196451b10d8e263745301ecd660cf45035 # v1.2.3 36 | with: 37 | # We only use the linux_amd64 variant here for generating the LICENSES.md 38 | sbom: sources_linux_amd64.sbom.xml 39 | type: xml 40 | filename: LICENSES.md 41 | evaluate: true 42 | template: | 43 | # Licenses 44 | 45 | The following third-party licenses are applicable to this project: 46 | 47 | {{range .SortedKeys}}## {{.}} 48 | 49 | {{range index $.ComponentsByLicense .}}- {{.Name}} ({{.Version}}) 50 | {{end}} 51 | {{end}} 52 | 53 | - name: Add SBOM and LICENSES.md to release 54 | uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 55 | with: 56 | files: | 57 | sources_${{ matrix.goos }}_${{ matrix.goarch }}.sbom.xml 58 | LICENSES.md 59 | -------------------------------------------------------------------------------- /docs/administrators-guide/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Administrator's Guide 3 | summary: The section for administrators setting up and maintaining the Paas Operator. 4 | authors: 5 | - hikarukin 6 | date: 2025-06-23 7 | --- 8 | 9 | # Administrator’s Guide 10 | 11 | Welcome to the Administrator’s Guide. This guide is intended for administrators 12 | and operators responsible for deploying, configuring, securing, and maintaining 13 | the Paas Operator in production environments. 14 | 15 | --- 16 | 17 | ## 📘 Contents 18 | 19 | - [Installation](install/) 20 | Step-by-step instructions to deploy the operator in your cluster. 21 | 22 | - [Configuration](configuration/) 23 | Guidance on customizing system behavior via `PaasConfig`. 24 | 25 | - [Cluster‑Wide Quotas](cluster-wide-quotas/) 26 | Instructions for enforcing resource usage limits across namespaces. 27 | 28 | - [Capabilities](capabilities/) 29 | Modular, plugin‑style features like ArgoCD, Tekton, Grafana, and Keycloak. 30 | 31 | - [Secrets](secrets/) 32 | Secure management of secrets within the operator. 33 | 34 | - [Security](security/) 35 | Best practices and hardening guidelines for production deployments. 36 | 37 | - [Validations](validations/) 38 | Built‑in checks to ensure correct configurations and prevent misconfigurations. 39 | 40 | - [API Version migration](v1alph1-conversion/) 41 | Docs regarding migrating v1alpha1 resources to v1alpha2 42 | 43 | _For development workflows, release procedures, and contributor guidelines, see the [Developer’s Guide](../development-guide/index.md)._ 44 | 45 | --- 46 | 47 | ## Version Support 48 | 49 | We follow a **roll‑forward support model**. Only the **latest major version** is 50 | supported. Previous major versions are considered **end-of-life (EOL)** and do not 51 | receive updates, security patches, or fixes. 52 | 53 | Administrators are expected to upgrade to the latest available version to remain supported. 54 | 55 | For detailed information about our support policy, including versioning, hotfixes, 56 | and the no‑backport rule, please refer to the [Support Policy in the Developer’s Guide](../development-guide/25_support-policy.md). 57 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package config 8 | 9 | import ( 10 | "context" 11 | "fmt" 12 | "testing" 13 | 14 | "github.com/belastingdienst/opr-paas/v4/api/v1alpha2" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func Test_getConfigFromContext(t *testing.T) { 19 | type args struct { 20 | ctx context.Context 21 | } 22 | tests := []struct { 23 | name string 24 | args args 25 | want v1alpha2.PaasConfig 26 | wantErr assert.ErrorAssertionFunc 27 | }{ 28 | { 29 | name: "config exists in context", 30 | args: args{ 31 | ctx: context.WithValue(context.Background(), ContextKeyPaasConfig, v1alpha2.PaasConfig{ 32 | Spec: v1alpha2.PaasConfigSpec{ 33 | Debug: true, 34 | }, 35 | }), 36 | }, 37 | want: v1alpha2.PaasConfig{ 38 | Spec: v1alpha2.PaasConfigSpec{ 39 | Debug: true, 40 | }, 41 | }, 42 | wantErr: assert.NoError, 43 | }, 44 | { 45 | name: "no config in context", 46 | args: args{ 47 | ctx: context.Background(), 48 | }, 49 | want: v1alpha2.PaasConfig{}, 50 | wantErr: assert.Error, 51 | }, 52 | { 53 | name: "wrong type in context", 54 | args: args{ 55 | ctx: context.WithValue(context.Background(), ContextKeyPaasConfig, "not-a-config"), 56 | }, 57 | want: v1alpha2.PaasConfig{}, 58 | wantErr: assert.Error, 59 | }, 60 | { 61 | name: "config fails in context as pointer", 62 | args: args{ 63 | ctx: context.WithValue(context.Background(), ContextKeyPaasConfig, &v1alpha2.PaasConfig{ 64 | Spec: v1alpha2.PaasConfigSpec{ 65 | Debug: true, 66 | }, 67 | }), 68 | }, 69 | want: v1alpha2.PaasConfig{}, 70 | wantErr: assert.Error, 71 | }, 72 | } 73 | for _, tt := range tests { 74 | t.Run(tt.name, func(t *testing.T) { 75 | got, err := GetConfigFromContext(tt.args.ctx) 76 | if !tt.wantErr(t, err, fmt.Sprintf("getConfigFromContext(%v)", tt.args.ctx)) { 77 | return 78 | } 79 | assert.Equalf(t, tt.want, got, "getConfigFromContext(%v)", tt.args.ctx) 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /docs/overview/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | summary: A short introduction. 4 | authors: 5 | - hikarukin 6 | date: 2024-07-01 7 | --- 8 | 9 | # Introduction to the Paas Operator 10 | 11 | In a microservice environment organizations can easily build and maintain thousands 12 | of apps. For each app there is a development process which consists of many types of 13 | technologies utilizing many types of resources. 14 | 15 | Some examples include: 16 | 17 | - Git repositories with code, configuration, documentation, etc.; 18 | - CI infrastructure; 19 | - CD infrastructure; 20 | - image repositories to hold image artifacts; 21 | - the actual namespace running the end application; 22 | - and more... 23 | 24 | Many of these artifacts can be deployed separately for every app, and would then run in their own namespace. 25 | 26 | The idea behind the Paas Operator is to bring all of these many pieces of the development process 27 | together in a single context we like to call a 'Project as a Service', e.a. Paas. 28 | The Paas operator then can be used to define a Paas for every App, and will deploy all 29 | the required artifacts accordingly. On top of that, the Paas operator implements multi-tenancy 30 | between the many Paas resources. 31 | 32 | Which means that, by leveraging the Paas operator, an organization can: 33 | 34 | - bring together all resources belonging to an App into a single unit called a Paas 35 | - maintain multi-tenancy between Paas instances 36 | - enable developers with capabilities to be used as part of the process behind maintaining the App 37 | 38 | This documentation site is arranged into a generic section called overview, a user section, 39 | an administrator section, and a developer section. The [Core Concepts](./core_concepts/) pages 40 | in the overview section are usually a good starting point. 41 | 42 | If you have any questions or feel that certain parts of the documentation can be improved or expanded, 43 | feel free to create a [PR](https://github.com/belastingdienst/opr-paas/pulls) (Pull Request). 44 | 45 | For full contribution guidelines, see the `CONTRIBUTING.md` file in the root of 46 | the repository, the [About >> Contributing](/about/contributing/) section and/or the 47 | [Development Guide](/development-guide/). 48 | -------------------------------------------------------------------------------- /docs/about/branding/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Branding 3 | summary: Our branding assets (logo) and how to use it. 4 | authors: 5 | - hikarukin 6 | date: 2025-01-30 7 | --- 8 | 9 | # Branding 10 | 11 | The Paas Operator, like many open source projects, has a logo. You can find the 12 | source of this logo (.svg format, made with Inkscape) in the `docs/about/branding` 13 | directory of the Git repository. 14 | 15 | You can also find a set of larger and smaller .png versions in that same directory, 16 | which are included on this page. 17 | 18 | !!! Info "© 2025 Tax Administration of The Netherlands." 19 | 20 | The branding, logo and its related assets are all: © copyright 2025 Tax 21 | Administration of The Netherlands. 22 | 23 | ## Logo 24 | 25 | ### SVG formatted source 26 | 27 | The file was created with Inkscape 1.4. 28 | 29 |
30 | ![Paas Operator Logo in SVG format](./paas-logo-v1.svg){ loading=lazy } 31 |
SVG source file
32 |
33 | 34 | ### PNG formatted size variants 35 | 36 |
37 | ![16x16 sized logo in PNG format](./paas-logo-v1-16x16px.png){ loading=lazy } 38 |
16x16px
39 |
40 | 41 |
42 | ![32x32 sized logo in PNG format](./paas-logo-v1-32x32px.png){ loading=lazy } 43 |
32x32px
44 |
45 | 46 |
47 | ![120x80 sized logo in PNG format](./paas-logo-v1-120x80px.png){ loading=lazy } 48 |
120x80px
49 |
50 | 51 |
52 | ![149x100 sized logo in PNG format](./paas-logo-v1-149x100px.png){ loading=lazy } 53 |
149x100px
54 |
55 | 56 |
57 | ![180x180 sized logo in PNG format](./paas-logo-v1-180x180px.png){ loading=lazy } 58 |
180x180px
59 |
60 | 61 |
62 | ![400x400 sized logo in PNG format](./paas-logo-v1-400x400px.png){ loading=lazy } 63 |
400x400px
64 |
65 | 66 |
67 | ![500x335 sized logo in PNG format](./paas-logo-v1-500x335px.png){ loading=lazy } 68 |
500x335px
69 |
70 | -------------------------------------------------------------------------------- /manifests/manager/opr-paas-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: paas-controller-manager 5 | namespace: system 6 | labels: 7 | control-plane: paas-controller-manager 8 | app.kubernetes.io/name: deployment 9 | app.kubernetes.io/instance: paas-controller-manager 10 | app.kubernetes.io/component: manager 11 | app.kubernetes.io/created-by: opr-paas 12 | app.kubernetes.io/part-of: opr-paas 13 | app.kubernetes.io/managed-by: kustomize 14 | spec: 15 | selector: 16 | matchLabels: 17 | control-plane: paas-controller-manager 18 | replicas: 1 19 | template: 20 | metadata: 21 | annotations: 22 | kubectl.kubernetes.io/default-container: manager 23 | labels: 24 | control-plane: paas-controller-manager 25 | spec: 26 | containers: 27 | - command: 28 | - /manager 29 | args: 30 | - --leader-elect 31 | image: controller:latest 32 | imagePullPolicy: IfNotPresent 33 | name: manager 34 | securityContext: 35 | allowPrivilegeEscalation: false 36 | capabilities: 37 | drop: 38 | - "ALL" 39 | livenessProbe: 40 | httpGet: 41 | path: /healthz 42 | port: 8081 43 | initialDelaySeconds: 15 44 | periodSeconds: 20 45 | readinessProbe: 46 | httpGet: 47 | path: /readyz 48 | port: 8081 49 | initialDelaySeconds: 5 50 | periodSeconds: 10 51 | resources: 52 | limits: 53 | cpu: 500m 54 | memory: 128Mi 55 | requests: 56 | cpu: 10m 57 | memory: 64Mi 58 | volumeMounts: 59 | - name: example-keys 60 | mountPath: /tmp/paas-e2e/secrets/priv 61 | ports: 62 | - containerPort: 8080 63 | name: metrics 64 | protocol: TCP 65 | serviceAccountName: paas-controller-manager 66 | terminationGracePeriodSeconds: 10 67 | volumes: 68 | - name: example-keys 69 | secret: 70 | secretName: example-keys 71 | defaultMode: 420 72 | -------------------------------------------------------------------------------- /publiccode.yml: -------------------------------------------------------------------------------- 1 | # This repository adheres to the publiccode.yml standard by including this 2 | # metadata file that makes public software easily discoverable. 3 | # More info at https://github.com/publiccodeyml/publiccode.yml 4 | 5 | publiccodeYmlVersion: "0.3" 6 | 7 | name: Paas Operator 8 | url: "https://github.com/belastingdienst/opr-paas" 9 | softwareType: standalone/other 10 | releaseDate: "1970-01-01" 11 | platforms: 12 | - kubernetes 13 | - openshift 14 | categories: 15 | - agile-project-management 16 | - cloud-management 17 | - it-development 18 | - it-service-management 19 | - project-management 20 | - resource-management 21 | - workflow-management 22 | developmentStatus: stable 23 | dependsOn: 24 | open: 25 | - name: Kubernetes / Openshift 26 | optional: false 27 | description: 28 | en: 29 | longDescription: > 30 | The PaaS operator delivers an opinionated 'Project as a Service' implementation 31 | where development teams can request a 'Project as a Service' by defining a PaaS 32 | resource. 33 | 34 | A PaaS resource is used by the operator as an input to create namespaces 35 | limited by Cluster Resource Quota's, granting groups permissions and (together 36 | with a clusterwide ArgoCD) creating capabilities such as: 37 | 38 | - a PaaS specific deployment of ArgoCD (continuous deployment); 39 | - Tekton (continuous integration); 40 | - Grafana (observability); and 41 | - KeyCloak (Application level Single Sign On); 42 | 43 | A PaaS is all a team needs to hit the ground running. 44 | 45 | shortDescription: An operator providing a multi tenancy solution which allows DevOps teams to request a context for their project, called a 'Project as a service'. 46 | 47 | documentation: https://belastingdienst.github.io/opr-paas/ 48 | 49 | features: 50 | - CRUD for Paas K8S Resources 51 | - Capabilities management 52 | 53 | legal: 54 | license: EUPL-1.2 55 | mainCopyrightOwner: Tax Administration of The Netherlands (Belastingdienst) 56 | repoOwner: Tax Administration of The Netherlands (Belastingdienst) 57 | 58 | localisation: 59 | availableLanguages: 60 | - en 61 | localisationReady: false 62 | 63 | maintenance: 64 | type: internal 65 | -------------------------------------------------------------------------------- /internal/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package utils 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | "path/filepath" 13 | "slices" 14 | "strings" 15 | "testing" 16 | 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | func TestPathDoesNotExist(t *testing.T) { 21 | paths := []string{"path1", "path2"} 22 | // expectedFiles := []string{"file1", "file2"} 23 | 24 | _, err := PathToFileList(paths) 25 | assert.NotNil(t, err) 26 | // assert.Equal(t, expectedFiles, files) 27 | assert.ErrorContains(t, err, "error while walking the path: lstat path1: no such file or directory") 28 | } 29 | 30 | func TestPathHappyFlow(t *testing.T) { 31 | tempDir, err := os.MkdirTemp("", "utils_test") 32 | if err != nil { 33 | t.Fatalf("Error creating temporary directory: %v", err) 34 | } 35 | defer os.RemoveAll(tempDir) // Clean up after test 36 | 37 | // Create some directories and files in the temporary directory 38 | for i := 0; i < 3; i++ { 39 | err = os.Mkdir(filepath.Join(tempDir, fmt.Sprintf("path%d", i)), 0o755) // revive:disable-line:add-constant 40 | if err != nil { 41 | t.Fatalf("Error creating directory: %v", err) 42 | } 43 | err = os.WriteFile(filepath.Join(tempDir, 44 | fmt.Sprintf("path%d/file%d", i, i)), 45 | []byte(fmt.Sprintf("content%d", i)), 0o644) // revive:disable-line:add-constant 46 | if err != nil { 47 | t.Fatalf("Error creating file: %v", err) 48 | } 49 | } 50 | 51 | paths := []string{filepath.Join(tempDir, "path1"), filepath.Join(tempDir, "path2")} 52 | expectedFiles := []string{filepath.Join(tempDir, "path1", "file1"), filepath.Join(tempDir, "path2", "file2")} 53 | 54 | files, err := PathToFileList(paths) 55 | 56 | // Check if files are prefixed with "/private", meaning we're on macOS 57 | for _, file := range files { 58 | if strings.HasPrefix(file, "/private") { 59 | // Update expectedFiles to include the correct path prefix for macOS 60 | expectedFiles = []string{ 61 | filepath.Join("/private", tempDir, "path1", "file1"), 62 | filepath.Join("/private", tempDir, "path2", "file2"), 63 | } 64 | } 65 | } 66 | 67 | assert.Nil(t, err) 68 | slices.Sort(expectedFiles) 69 | slices.Sort(files) 70 | assert.Equal(t, expectedFiles, files) 71 | } 72 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: opr-paas 2 | site_url: https://belastingdienst.github.io/opr-paas/ 3 | site_author: Dutch Tax Office 4 | 5 | # Repository 6 | repo_name: opr-paas 7 | repo_url: https://github.com/belastingdienst/opr-paas 8 | edit_uri: edit/main/docs 9 | 10 | theme: 11 | name: material 12 | custom_dir: mkdocs_overrides 13 | logo: about/branding/paas-logo-v1-180x180px.png 14 | favicon: about/branding/paas-logo-v1-32x32px.png 15 | language: en 16 | hljs_languages: 17 | - yaml 18 | - go 19 | - bash 20 | features: 21 | - content.action.edit 22 | - navigation.expand 23 | - navigation.path 24 | - navigation.tracking 25 | - navigation.tabs 26 | - navigation.tabs.sticky 27 | - navigation.indexes 28 | - navigation.top 29 | - navigation.prune 30 | - search.suggest 31 | - search.share 32 | - toc.follow 33 | copyright: > 34 | Copyright © 2024 Tax Administration of The Netherlands, software and documentation licensed under EUPL. 35 | 36 | extra: 37 | version: 38 | provider: mike 39 | alias: true 40 | 41 | markdown_extensions: 42 | - abbr 43 | - admonition 44 | - attr_list 45 | - def_list 46 | - footnotes 47 | - md_in_html 48 | - toc: 49 | title: On this page 50 | permalink: true 51 | toc_depth: 3 52 | - tables 53 | - pymdownx.highlight: 54 | anchor_linenums: true 55 | auto_title: false 56 | pygments_lang_class: true 57 | - pymdownx.inlinehilite 58 | - pymdownx.snippets 59 | - pymdownx.superfences 60 | - pymdownx.tasklist: 61 | custom_checkbox: true 62 | 63 | plugins: 64 | - search 65 | - social 66 | - tags 67 | - privacy: 68 | assets_exclude: 69 | - kroki.io/* 70 | - kroki:8000/* 71 | - kroki: 72 | server_url: !ENV [KROKI_SERVER_URL, 'https://kroki.io'] 73 | http_method: POST 74 | - literate-nav: 75 | nav_file: README.md 76 | implicit_index: true 77 | - redirects: 78 | redirect_maps: 79 | 'index.md': 'overview/index.md' 80 | 81 | # Navigation 82 | nav: 83 | - Overview: overview/ 84 | - Administrator Guide: administrators-guide/ 85 | - User Guide: user-guide/ 86 | - Developer Guide: development-guide/ 87 | - About: 88 | - Branding: about/branding/ 89 | - Contributing: about/contributing.md 90 | - Code of Conduct: about/code-of-conduct.md 91 | - License: about/license.md 92 | -------------------------------------------------------------------------------- /docs/development-guide/40_e2e-tests.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: End to end testing 3 | summary: How we do end to end testing. 4 | authors: 5 | - hikarukin 6 | date: 2024-08-21 7 | --- 8 | 9 | # Introduction 10 | 11 | We have a bunch of end-to-end tests that can be used by you to verify whether the 12 | operator still works as expected after you made any code changes. These tests are 13 | also part of our continues integration pipeline on GitHub. 14 | 15 | ## Prerequisites 16 | 17 | Ensure you have a vanilla Kubernetes or OpenShift cluster running. We can heartily 18 | recommend using [kind](https://kind.sigs.k8s.io). 19 | 20 | !!! example 21 | 22 | ```kind create cluster``` 23 | 24 | ## Running the tests 25 | 26 | 1. In case of a vanilla kubernetes cluster, run: `make setup-e2e`
27 | This will apply mocks, etc. needed to run the operator. 28 | 2. Start the operator: `make run` 29 | 3. Finally, run the actual e2e tests: `make test-e2e` 30 | 31 | ## Design considerations 32 | 33 | We've decided to use the e2e-framework from K8S. The advantages of this framework 34 | are that any connection to a K8S cluster can be used to execute these tests against 35 | that cluster. 36 | 37 | This makes the tests loosely coupled, and thus usable against various types of 38 | clusters. For example a K3S cluster spun up on a developer's machine or in one in 39 | GitHub actions. 40 | 41 | We can use our favorite programming language to write the tests. The framework 42 | uses a kubernetes client to execute K8S commands to the connected cluster. 43 | The cluster should have a Paas operator installed to reconcile Paas'es during 44 | the execution of these tests. The tests assert whether the expected resources 45 | are created on the cluster. 46 | 47 | ## Setup 48 | 49 | The host running these tests, must have an active connection to a K8S cluster in 50 | it's kubeConfig. It must be logged in and have the appropriate permissions to 51 | apply the resources used in this test. 52 | 53 | The tests, by default, run in a namespace: `paas-e2e` which will be created during 54 | test setup (`main_test.go`) and deleted afterward. If you would like to use an 55 | existing namespace, set the environment variable: `PAAS_E2E_NS` to the namespace 56 | name. 57 | 58 | !!! Info 59 | The tests do not create the custom namespace for you in case it happens to 60 | be missing, so make sure to create it or be prepared to enjoy the error message. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # opr-paas 2 | 3 | ## Goal 4 | 5 | The Paas operator delivers an opinionated 'Project as a Service' implementation where 6 | development teams can request a 'Project as a Service' by defining a Paas resource. 7 | 8 | A Paas resource is used by the operator as an input to create namespaces limited 9 | by Cluster Resource Quota's, granting groups permissions and (together with a clusterwide 10 | ArgoCD) creating capabilities such as: 11 | 12 | - a Paas specific deployment of ArgoCD (continuous deployment); 13 | - Tekton (continuous integration); 14 | - Grafana (observability); and 15 | - KeyCloak (Application level Single Sign On); 16 | 17 | A Paas is all a team needs to hit the ground running. 18 | 19 | ## Quickstart 20 | 21 | Deploy the operator using the following commands: 22 | 23 | ``` 24 | kubectl apply -f https://github.com/belastingdienst/opr-paas/releases/latest/download/install.yaml 25 | kubectl apply -f https://raw.githubusercontent.com/belastingdienst/opr-paas/refs/heads/main/examples/resources/_v1alpha2_paasconfig.yaml 26 | ``` 27 | 28 | The second command will load an example PaasConfig resource from the main branch 29 | to get you going. Feel free to replace this with your own or a release specific 30 | version instead. 31 | 32 | This will install the operator using the `install.yaml` that was generated for the 33 | latest release. It will create: 34 | 35 | - a namespace called `paas-system`; 36 | - 3 CRDs (`Paas`, `PaasNs` and `PaasConfig`); 37 | - a service account, role, role binding, cluster role and cluster role binding for 38 | all permissions required by the operator; 39 | - a viewer & an editor cluster role for all crds; 40 | - a deployment running the operator; 41 | 42 | Feel free to change config as required. 43 | 44 | ## Background information 45 | 46 | - [build-kubernetes-operator-six-steps](https://developers.redhat.com/articles/2021/09/07/build-kubernetes-operator-six-steps#setup_and_prerequisites) 47 | - [operator sdk installation instructions](https://sdk.operatorframework.io/docs/installation/) 48 | 49 | ## Contributing 50 | 51 | Please refer to our documentation in the [CONTRIBUTING.md](./CONTRIBUTING.md) file 52 | and the Developer Guide section of the documentation site if you want to help us 53 | improve the Paas Operator. 54 | 55 | ## License 56 | 57 | Copyright 2024, Tax Administration of The Netherlands. 58 | Licensed under the EUPL 1.2. 59 | 60 | See [LICENSE.md](./LICENSE.md) for details. 61 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | The Dutch Tax Office (Dutch: Belastingdienst) takes the security of our products 4 | and services seriously. 5 | 6 | If you believe you have found a security vulnerability, please report it before 7 | sharing it with the outside world. This way, we can take measures first. This is 8 | called '[Coordinated Vulnerability Disclosure](https://www.belastingdienst.nl/wps/wcm/connect/bldcontenten/standaard_functies/individuals/contact/data-leak-vulnerability-abuse-computer-systems/coordinated-vulnerability-disclosure)' (CVD). 9 | 10 | ## Reporting Security Issues 11 | 12 | **Please do not report security vulnerabilities through public GitHub issues.** 13 | 14 | The Coordinated Vulnerability Disclosure page on our webpage explains how to securely 15 | report your finding. 16 | 17 | In summary: 18 | 19 | - Send us your findings by e-mail: cvd@belastingdienst.nl 20 | - If possible, encrypt your findings with our PGP-key on the [Coordinated Vulnerability Disclosure](https://www.belastingdienst.nl/wps/wcm/connect/bldcontenten/standaard_functies/individuals/contact/data-leak-vulnerability-abuse-computer-systems/coordinated-vulnerability-disclosure) page. 21 | - provide sufficient information to be able to reproduce the problem, so that we 22 | can rectify this as quickly as possible. The URL of the system affected and a 23 | description of the vulnerability are sufficient, but more information may be 24 | required for more complex vulnerabilities. 25 | - leave your contact details so that our Security Operations Centre can contact 26 | you in order to jointly find a safe solution. Leave at least an e-mail address 27 | or telephone number. 28 | - do not share the information regarding the security problem with other people 29 | until we have solved it. 30 | - handle the information regarding the security problem responsibly by not performing 31 | any actions that go further than necessary to demonstrate the security problem. 32 | - realize that any information in our systems falls under the (fiscal) duty of 33 | confidentiality and that further dissemination of the said information is a 34 | punishable offense. 35 | 36 | ## Policy 37 | 38 | - The Dutch Tax Office (Dutch: Belastingdienst) follows the principle of [Coordinated Vulnerability Disclosure](https://www.belastingdienst.nl/wps/wcm/connect/bldcontenten/standaard_functies/individuals/contact/data-leak-vulnerability-abuse-computer-systems/coordinated-vulnerability-disclosure). 39 | 40 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION 🌈' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | exclude-labels: 4 | - 'skip-changelog' 5 | replacers: 6 | - search: '/CVE-(\d{4})-(\d+)/g' 7 | replace: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-$1-$2' 8 | categories: 9 | - title: '💥 BREAKING CHANGES' 10 | labels: 11 | - 'breaking' 12 | - title: '🚀 Features' 13 | labels: 14 | - 'feature' 15 | - 'enhancement' 16 | - title: '🐛 Bug Fixes' 17 | labels: 18 | - 'fix' 19 | - 'bugfix' 20 | - 'bug' 21 | - title: '🧰 Maintenance' 22 | labels: 23 | - 'chore' 24 | - 'documentation' 25 | - 'ci' 26 | - 'refactor' 27 | - 'style' 28 | - 'test' 29 | - title: '🔒 Security' 30 | labels: 31 | - 'dependencies' 32 | - 'security' 33 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 34 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 35 | version-resolver: 36 | major: 37 | labels: 38 | - 'major' 39 | - 'breaking' 40 | minor: 41 | labels: 42 | - 'minor' 43 | - 'refactor' 44 | - 'enhancement' 45 | patch: 46 | labels: 47 | - 'patch' 48 | - 'documentation' 49 | - 'ci' 50 | - 'style' 51 | - 'test' 52 | default: patch 53 | autolabeler: 54 | # Enforce breaking label 55 | - label: 'breaking' 56 | title: 57 | - '/!:/' 58 | body: 59 | - '/BREAKING CHANGE/' 60 | # Enforce minor label if manifests are changed 61 | - label: 'minor' 62 | files: 63 | - 'manifests/**/*' 64 | # All labels below are set SOLELY on the basis of the title to prevent double labelling 65 | - label: 'chore' 66 | title: 67 | - '/^chore(\(.*\))?!?:/i' 68 | - label: 'ci' 69 | title: 70 | - '/^(ci|build)(\(.*\))?!?:/i' 71 | - label: 'documentation' 72 | title: 73 | - '/^docs(\(.*\))?!?:/i' 74 | - label: 'enhancement' 75 | title: 76 | - '/^(feat|perf)(\(.*\))?!?:/i' 77 | - label: 'bug' 78 | title: 79 | - '/^(fix|hotfix|bug)(\(.*\))?!?:/i' 80 | - label: 'refactor' 81 | title: 82 | - '/^refactor(\(.*\))?!?:/i' 83 | - label: 'revert' 84 | title: 85 | - '/^revert(\(.*\))?!?:/i' 86 | - label: 'style' 87 | title: 88 | - '/^style(\(.*\))?!?:/i' 89 | - label: 'test' 90 | title: 91 | - '/^test(\(.*\))?!?:/i' 92 | template: | 93 | ## Changes 94 | 95 | $CHANGES 96 | -------------------------------------------------------------------------------- /internal/controller/main_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package controller 8 | 9 | import ( 10 | "maps" 11 | "reflect" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestMain_intersection(t *testing.T) { 18 | l1 := []string{"v1", "v2", "v2", "v3", "v4"} 19 | l2 := []string{"v2", "v2", "v3", "v5"} 20 | li := intersect(l1, l2) 21 | // Expected to have only all values that exist in list 1 and 2, only once (unique) 22 | lExpected := []string{"v2", "v3"} 23 | assert.ElementsMatch(t, li, lExpected, "result of intersection not as expected") 24 | } 25 | 26 | func TestMergeSecrets(t *testing.T) { 27 | tests := []struct { 28 | name string 29 | base map[string]string 30 | override map[string]string 31 | want map[string]string 32 | }{ 33 | { 34 | name: "empty base and override", 35 | base: map[string]string{}, 36 | override: map[string]string{}, 37 | want: map[string]string{}, 38 | }, 39 | { 40 | name: "base only", 41 | base: map[string]string{"a1": "1"}, 42 | override: map[string]string{}, 43 | want: map[string]string{"a1": "1"}, 44 | }, 45 | { 46 | name: "override only", 47 | base: map[string]string{}, 48 | override: map[string]string{"b": "b2"}, 49 | want: map[string]string{"b": "b2"}, 50 | }, 51 | { 52 | name: "override replaces base", 53 | base: map[string]string{"c": "c1"}, 54 | override: map[string]string{"c": "c2"}, 55 | want: map[string]string{"c": "c2"}, 56 | }, 57 | { 58 | name: "override adds to base", 59 | base: map[string]string{"a": "1"}, 60 | override: map[string]string{"b": "2"}, 61 | want: map[string]string{"a": "1", "b": "2"}, 62 | }, 63 | { 64 | name: "multiple overrides", 65 | base: map[string]string{"f": "1", "c": "3"}, 66 | override: map[string]string{"f": "10", "g": "20"}, 67 | want: map[string]string{"f": "10", "g": "20", "c": "3"}, 68 | }, 69 | } 70 | 71 | for _, tt := range tests { 72 | t.Run(tt.name, func(t *testing.T) { 73 | // copy maps to avoid mutating original test cases 74 | baseCopy := maps.Clone(tt.base) 75 | overrideCopy := maps.Clone(tt.override) 76 | 77 | got := mergeSecrets(baseCopy, overrideCopy) 78 | if !reflect.DeepEqual(got, tt.want) { 79 | t.Errorf("mergeSecrets() = %v, want %v", got, tt.want) 80 | } 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /docs/development-guide/25_support-policy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Support Policy 3 | summary: The support policy adhered to by this project. 4 | authors: 5 | - hikarukin 6 | date: 2025-06-23 7 | --- 8 | 9 | # Support Policy 10 | 11 | By enforcing this support and versioning strategy, we can focus on delivering timely, 12 | reliable updates and maintaining a streamlined release process for all users. 13 | 14 | We follow a **roll-forward-only support model**. This ensures that all users 15 | benefit from the latest improvements, fixes, and security updates without fragmenting 16 | support across outdated versions. 17 | 18 | ## Supported Versions 19 | 20 | - We only support the **latest active major release series.** 21 | - Fixes, improvements, and security updates will only be applied to the most recent 22 | release. 23 | - Previous major release series are considered **end-of-life (EOL)** and will not 24 | receive backported fixes or patches. 25 | - Users are expected to upgrade to the latest available version to remain supported. 26 | 27 | ## No Backports 28 | 29 | - **We do not backport fixes to older versions.** 30 | - If an issue is identified in an older release, the resolution will be provided 31 | in a new release based on the current active version. 32 | - Hotfixes and patches are provided only within the current supported release series. 33 | 34 | ## Hotfixes 35 | 36 | - Hotfixes are supported **exclusively** for the currently supported release series. 37 | - Hotfixes are created from the latest release tag. 38 | - All hotfix branches must be merged back into `main` after release to ensure continuity. 39 | 40 | ## Versioning 41 | 42 | We adhere to [Semantic Versioning v2.0.0](https://semver.org/spec/v2.0.0.html) with 43 | the following conventions: 44 | - All version numbers are prefixed with the letter **'v'** (e.g., `v2.1.0`). 45 | - **vX.Y.Z** structure: 46 | - **X**: Major version – may introduce breaking changes. 47 | - **Y**: Minor version – adds functionality in a backward-compatible manner. 48 | - **Z**: Patch version – backward-compatible fixes. 49 | 50 | > **Note:** Only the latest major version is supported. Minor and patch releases 51 | within the current major version are eligible for updates. 52 | 53 | ## Commits 54 | 55 | We strictly follow the [Conventional Commits v1.0.0](https://www.conventionalcommits.org/en/v1.0.0/) 56 | specification. This ensures that commit messages are: 57 | - Clear and machine-readable. 58 | - Aligned with semantic versioning for automated changelog generation and release drafting. 59 | -------------------------------------------------------------------------------- /internal/argocd-plugin-generator/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package argocd_plugin_generator 8 | 9 | import ( 10 | "fmt" 11 | "path/filepath" 12 | "runtime" 13 | "slices" 14 | "testing" 15 | 16 | "github.com/belastingdienst/opr-paas/v4/api/v1alpha2" 17 | "github.com/go-logr/zerologr" 18 | . "github.com/onsi/ginkgo/v2" 19 | . "github.com/onsi/gomega" 20 | "github.com/rs/zerolog" 21 | "github.com/rs/zerolog/log" 22 | "k8s.io/client-go/rest" 23 | ctrl "sigs.k8s.io/controller-runtime" 24 | "sigs.k8s.io/controller-runtime/pkg/client" 25 | "sigs.k8s.io/controller-runtime/pkg/envtest" 26 | ) 27 | 28 | var ( 29 | cfg *rest.Config 30 | k8sClient client.Client 31 | testEnv *envtest.Environment 32 | ) 33 | 34 | func TestArgoCDPluginGenerator(t *testing.T) { 35 | RegisterFailHandler(Fail) 36 | RunSpecs(t, "GeneratorServer Suite") 37 | } 38 | 39 | var _ = BeforeSuite(func() { 40 | log.Logger = log.Level(zerolog.DebugLevel). 41 | Output(zerolog.ConsoleWriter{Out: GinkgoWriter}) 42 | ctrl.SetLogger(zerologr.New(&log.Logger)) 43 | 44 | By("bootstrapping test environment") 45 | binDirs, _ := filepath.Glob(filepath.Join("..", "..", "bin", "k8s", 46 | fmt.Sprintf("*-%s-%s", runtime.GOOS, runtime.GOARCH))) 47 | slices.Sort(binDirs) 48 | testEnv = &envtest.Environment{ 49 | CRDDirectoryPaths: []string{ 50 | filepath.Join("..", "..", "manifests", "crd", "bases"), 51 | }, 52 | ErrorIfCRDPathMissing: true, 53 | 54 | // The BinaryAssetsDirectory is only required if you want to run the tests directly 55 | // without call the makefile target test. If not informed it will look for the 56 | // default path defined in controller-runtime which is /usr/local/kubebuilder/. 57 | // Note that you must have the required binaries setup under the bin directory to perform 58 | // the tests directly. When we run make test it will be setup and used automatically. 59 | BinaryAssetsDirectory: binDirs[len(binDirs)-1], 60 | } 61 | var err error 62 | cfg, err = testEnv.Start() 63 | Expect(err).NotTo(HaveOccurred()) 64 | Expect(cfg).NotTo(BeNil()) 65 | 66 | err = v1alpha2.AddToScheme(testEnv.Scheme) 67 | Expect(err).NotTo(HaveOccurred()) 68 | 69 | k8sClient, err = client.New(cfg, client.Options{Scheme: testEnv.Scheme}) 70 | Expect(err).NotTo(HaveOccurred()) 71 | Expect(k8sClient).NotTo(BeNil()) 72 | }) 73 | 74 | var _ = AfterSuite(func() { 75 | By("tearing down the test environment") 76 | err := testEnv.Stop() 77 | Expect(err).NotTo(HaveOccurred()) 78 | }) 79 | -------------------------------------------------------------------------------- /api/v1alpha2/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package v1alpha2 8 | 9 | import ( 10 | "fmt" 11 | "path/filepath" 12 | "runtime" 13 | "slices" 14 | "testing" 15 | 16 | "github.com/go-logr/zerologr" 17 | . "github.com/onsi/ginkgo/v2" 18 | . "github.com/onsi/gomega" 19 | "github.com/rs/zerolog" 20 | "github.com/rs/zerolog/log" 21 | "k8s.io/client-go/kubernetes/scheme" 22 | "k8s.io/client-go/rest" 23 | ctrl "sigs.k8s.io/controller-runtime" 24 | "sigs.k8s.io/controller-runtime/pkg/client" 25 | "sigs.k8s.io/controller-runtime/pkg/envtest" 26 | // +kubebuilder:scaffold:imports 27 | ) 28 | 29 | var ( 30 | cfg *rest.Config 31 | k8sClient client.Client 32 | testEnv *envtest.Environment 33 | ) 34 | 35 | func TestApiV1Alpha2(t *testing.T) { 36 | RegisterFailHandler(Fail) 37 | 38 | RunSpecs(t, "v1alpha2 api Suite") 39 | } 40 | 41 | var _ = BeforeSuite(func() { 42 | log.Logger = log.Level(zerolog.DebugLevel). 43 | Output(zerolog.ConsoleWriter{Out: GinkgoWriter}) 44 | ctrl.SetLogger(zerologr.New(&log.Logger)) 45 | 46 | By("bootstrapping test environment") 47 | binDirs, _ := filepath.Glob(filepath.Join("..", "..", "bin", "k8s", 48 | fmt.Sprintf("*-%s-%s", runtime.GOOS, runtime.GOARCH))) 49 | slices.Sort(binDirs) 50 | testEnv = &envtest.Environment{ 51 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "manifests", "crd", "bases")}, 52 | ErrorIfCRDPathMissing: true, 53 | 54 | // The BinaryAssetsDirectory is only required if you want to run the tests directly 55 | // without call the makefile target test. If not informed it will look for the 56 | // default path defined in controller-runtime which is /usr/local/kubebuilder/. 57 | // Note that you must have the required binaries setup under the bin directory to perform 58 | // the tests directly. When we run make test it will be setup and used automatically. 59 | BinaryAssetsDirectory: binDirs[len(binDirs)-1], 60 | } 61 | 62 | var err error 63 | cfg, err = testEnv.Start() 64 | Expect(err).NotTo(HaveOccurred()) 65 | Expect(cfg).NotTo(BeNil()) 66 | 67 | err = AddToScheme(scheme.Scheme) 68 | Expect(err).NotTo(HaveOccurred()) 69 | 70 | // +kubebuilder:scaffold:scheme 71 | 72 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 73 | Expect(err).NotTo(HaveOccurred()) 74 | Expect(k8sClient).NotTo(BeNil()) 75 | }) 76 | 77 | var _ = AfterSuite(func() { 78 | By("tearing down the test environment") 79 | err := testEnv.Stop() 80 | Expect(err).NotTo(HaveOccurred()) 81 | }) 82 | -------------------------------------------------------------------------------- /internal/controller/rsa_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/belastingdienst/opr-paas-crypttool/pkg/crypt" 7 | "github.com/belastingdienst/opr-paas/v4/internal/config" 8 | "github.com/belastingdienst/opr-paas/v4/internal/logging" 9 | corev1 "k8s.io/api/core/v1" 10 | "k8s.io/apimachinery/pkg/types" 11 | ) 12 | 13 | var ( 14 | // crypts contains a maps of crypt against a Paas name 15 | crypts map[string]*crypt.Crypt 16 | decryptPrivateKeys *crypt.PrivateKeys 17 | ) 18 | 19 | // resetCrypts removes all crypts and resets decryptSecretPrivateKeys 20 | func resetCrypts() { 21 | crypts = map[string]*crypt.Crypt{} 22 | decryptPrivateKeys = nil 23 | } 24 | 25 | // getRsaPrivateKeys fetches secret, compares to cached private keys, resets crypts if needed, and returns keys 26 | func (r *PaasReconciler) getRsaPrivateKeys( 27 | ctx context.Context, 28 | ) (*crypt.PrivateKeys, error) { 29 | ctx, logger := logging.GetLogComponent(ctx, logging.ControllerSecretComponent) 30 | rsaSecret := &corev1.Secret{} 31 | cfg, err := config.GetConfigFromContext(ctx) 32 | if err != nil { 33 | return nil, err 34 | } 35 | namespacedName := cfg.Spec.DecryptKeysSecret 36 | 37 | err = r.Get(ctx, types.NamespacedName{ 38 | Name: namespacedName.Name, 39 | Namespace: namespacedName.Namespace, 40 | }, rsaSecret) 41 | if err != nil { 42 | return nil, err 43 | } 44 | // Create new set of keys from data in secret 45 | keys, err := crypt.NewPrivateKeysFromSecretData(rsaSecret.Data) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | if decryptPrivateKeys != nil { 51 | if keys.Compare(*decryptPrivateKeys) { 52 | // It already was the same secret 53 | logger.Debug().Msg("reusing decrypt keys") 54 | return decryptPrivateKeys, nil 55 | } 56 | } 57 | 58 | logger.Debug().Msgf("setting (%d) new keys", len(keys)) 59 | resetCrypts() 60 | decryptPrivateKeys = &keys 61 | return decryptPrivateKeys, nil 62 | } 63 | 64 | // getRsa returns a crypt.Crypt for a specified paasName 65 | func (r *PaasReconciler) getRsa(ctx context.Context, paasName string) (*crypt.Crypt, error) { 66 | var c *crypt.Crypt 67 | if keys, err := r.getRsaPrivateKeys(ctx); err != nil { 68 | return nil, err 69 | } else if rsa, exists := crypts[paasName]; exists { 70 | return rsa, nil 71 | } else if c, err = crypt.NewCryptFromKeys(*keys, "", paasName); err != nil { 72 | return nil, err 73 | } 74 | _, logger := logging.GetLogComponent(ctx, logging.ControllerSecretComponent) 75 | logger.Debug().Msgf("creating new crypt for %s", paasName) 76 | crypts[paasName] = c 77 | return c, nil 78 | } 79 | -------------------------------------------------------------------------------- /test/e2e/capability-external_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | api "github.com/belastingdienst/opr-paas/v4/api/v1alpha2" 8 | "github.com/belastingdienst/opr-paas/v4/pkg/quota" 9 | quotav1 "github.com/openshift/api/quota/v1" 10 | "github.com/stretchr/testify/assert" 11 | corev1 "k8s.io/api/core/v1" 12 | "sigs.k8s.io/e2e-framework/pkg/envconf" 13 | "sigs.k8s.io/e2e-framework/pkg/features" 14 | ) 15 | 16 | const ( 17 | paasWithCapabilityExternal = "capexternalpaas" 18 | paasCapExternalNs = "capexternalpaas-capexternal" 19 | capExternalApplicationSet = "capexternalas" 20 | ) 21 | 22 | func TestCapExternal(t *testing.T) { 23 | paasSpec := api.PaasSpec{ 24 | Requestor: "paas-user", 25 | Quota: make(quota.Quota), 26 | Capabilities: api.PaasCapabilities{ 27 | "capexternal": api.PaasCapability{}, 28 | }, 29 | } 30 | 31 | testenv.Test( 32 | t, 33 | features.New("Capability External"). 34 | Setup(createPaasFn(paasWithCapabilityExternal, paasSpec)). 35 | Assess("is created", assertCapExternalCreated). 36 | Assess("is deleted when PaaS is deleted", assertCapExternalDeleted). 37 | Teardown(teardownPaasFn(paasWithCapabilityExternal)). 38 | Feature(), 39 | ) 40 | } 41 | 42 | func assertCapExternalCreated(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { 43 | paas := getPaas(ctx, paasWithCapabilityExternal, t, cfg) 44 | 45 | // no namespace should be created for an external capability 46 | t.Log("checking if namespace is created") 47 | failWhenExists(ctx, paasCapExternalNs, cfg.Namespace(), &corev1.Namespace{}, t, cfg) 48 | 49 | t.Log("checking if clusterquota is created") 50 | // no quota should be created for an external capability 51 | failWhenExists(ctx, paasCapExternalNs, cfg.Namespace(), "av1.ClusterResourceQuota{}, t, cfg) 52 | 53 | // ClusterResource is created with the same name as the PaaS 54 | assert.Equal(t, paasWithCapabilityExternal, paas.Name) 55 | 56 | // capExternal should be enabled 57 | assert.Contains(t, paas.Spec.Capabilities, "capexternal") 58 | 59 | return ctx 60 | } 61 | 62 | func assertCapExternalDeleted(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { 63 | deletePaasSync(ctx, paasWithCapabilityExternal, t, cfg) 64 | 65 | // Namespace is deleted 66 | var namespaceList corev1.NamespaceList 67 | if err := cfg.Client().Resources().List(ctx, &namespaceList); err != nil { 68 | t.Fatalf("Failed to retrieve Namespace list: %v", err) 69 | } 70 | 71 | // Namespace list not contains paas 72 | assert.NotContains(t, namespaceList.Items, paasWithCapabilityExternal) 73 | 74 | return ctx 75 | } 76 | -------------------------------------------------------------------------------- /api/v1alpha1/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package v1alpha1 8 | 9 | import ( 10 | "fmt" 11 | "path/filepath" 12 | "runtime" 13 | "slices" 14 | "strings" 15 | "testing" 16 | 17 | "github.com/go-logr/zerologr" 18 | . "github.com/onsi/ginkgo/v2" 19 | . "github.com/onsi/gomega" 20 | "github.com/rs/zerolog" 21 | "github.com/rs/zerolog/log" 22 | "k8s.io/client-go/kubernetes/scheme" 23 | "k8s.io/client-go/rest" 24 | ctrl "sigs.k8s.io/controller-runtime" 25 | "sigs.k8s.io/controller-runtime/pkg/client" 26 | "sigs.k8s.io/controller-runtime/pkg/envtest" 27 | // +kubebuilder:scaffold:imports 28 | ) 29 | 30 | var ( 31 | cfg *rest.Config 32 | k8sClient client.Client 33 | testEnv *envtest.Environment 34 | ) 35 | 36 | func TestApiV1Alpha1(t *testing.T) { 37 | RegisterFailHandler(Fail) 38 | 39 | RunSpecs(t, "v1alpha1 api Suite") 40 | } 41 | 42 | func join(argv ...string) string { 43 | return strings.Join(argv, "-") 44 | } 45 | 46 | var _ = BeforeSuite(func() { 47 | log.Logger = log.Level(zerolog.DebugLevel). 48 | Output(zerolog.ConsoleWriter{Out: GinkgoWriter}) 49 | ctrl.SetLogger(zerologr.New(&log.Logger)) 50 | 51 | By("bootstrapping test environment") 52 | binDirs, _ := filepath.Glob(filepath.Join("..", "..", "bin", "k8s", 53 | fmt.Sprintf("*-%s-%s", runtime.GOOS, runtime.GOARCH))) 54 | slices.Sort(binDirs) 55 | testEnv = &envtest.Environment{ 56 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "manifests", "crd", "bases")}, 57 | ErrorIfCRDPathMissing: true, 58 | 59 | // The BinaryAssetsDirectory is only required if you want to run the tests directly 60 | // without call the makefile target test. If not informed it will look for the 61 | // default path defined in controller-runtime which is /usr/local/kubebuilder/. 62 | // Note that you must have the required binaries setup under the bin directory to perform 63 | // the tests directly. When we run make test it will be setup and used automatically. 64 | BinaryAssetsDirectory: binDirs[len(binDirs)-1], 65 | } 66 | 67 | var err error 68 | cfg, err = testEnv.Start() 69 | Expect(err).NotTo(HaveOccurred()) 70 | Expect(cfg).NotTo(BeNil()) 71 | 72 | err = AddToScheme(scheme.Scheme) 73 | Expect(err).NotTo(HaveOccurred()) 74 | 75 | // +kubebuilder:scaffold:scheme 76 | 77 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 78 | Expect(err).NotTo(HaveOccurred()) 79 | Expect(k8sClient).NotTo(BeNil()) 80 | }) 81 | 82 | var _ = AfterSuite(func() { 83 | By("tearing down the test environment") 84 | err := testEnv.Stop() 85 | Expect(err).NotTo(HaveOccurred()) 86 | }) 87 | -------------------------------------------------------------------------------- /pkg/fields/elementmap.go: -------------------------------------------------------------------------------- 1 | package fields 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "maps" 8 | ) 9 | 10 | // ElementMap represents all key, value pairs for one entry in the list of the listgenerator 11 | type ElementMap map[string]Element 12 | 13 | // ElementMapFromJSON can be used to import key, value pairs from JSON 14 | func ElementMapFromJSON(raw []byte) (ElementMap, error) { 15 | newElementMap := make(ElementMap) 16 | if err := json.Unmarshal(raw, &newElementMap); err != nil { 17 | return nil, err 18 | } 19 | return newElementMap, nil 20 | } 21 | 22 | // GetElementAsString gets a value and returns as string 23 | // This should be a method on Element, but a method cannot exist on interface datatypes 24 | func (em ElementMap) GetElementAsString(key string) string { 25 | value, err := em.TryGetElementAsString(key) 26 | if err != nil { 27 | return "" 28 | } 29 | return value 30 | } 31 | 32 | // TryGetElementAsString gets a value and returns as string 33 | // This should be a method on Element, but a method cannot exist on interface datatypes 34 | func (em ElementMap) TryGetElementAsString(key string) (string, error) { 35 | element, exists := em[key] 36 | if !exists { 37 | return "", errors.New("element does not exist") 38 | } 39 | value, ok := element.(string) 40 | if ok { 41 | return value, nil 42 | } 43 | j, err := json.Marshal(element) 44 | if err != nil { 45 | return "", err 46 | } 47 | return string(j), nil 48 | } 49 | 50 | // Merge merges all key/value pairs from another Entries on top of this and returns the resulting total Entries set 51 | func (em ElementMap) Merge(added ElementMap) ElementMap { 52 | merged := maps.Clone(em) 53 | for key, value := range added { 54 | merged[key] = value 55 | } 56 | return merged 57 | } 58 | 59 | // AsJSON can be used to export all elements as JSON 60 | func (em ElementMap) AsJSON() ([]byte, error) { 61 | return json.Marshal(em) 62 | } 63 | 64 | // AsLabels will convert this any map into a string map 65 | func (em ElementMap) AsLabels() map[string]string { 66 | result := map[string]string{} 67 | for key, value := range em { 68 | result[key] = fmt.Sprintf("%v", value) 69 | } 70 | return result 71 | } 72 | 73 | // AsElementMap will convert this any map into a string map 74 | func (em ElementMap) AsElementMap() ElementMap { 75 | return em 76 | } 77 | 78 | // Prefix will return a new ElementMap with all keys prefixed with a value 79 | func (em ElementMap) Prefix(prefix string) ElementMap { 80 | prefixed := ElementMap{} 81 | for key, value := range em { 82 | if key == "" { 83 | key = prefix 84 | } else if prefix != "" { 85 | key = fmt.Sprintf("%s-%s", prefix, key) 86 | } 87 | prefixed[key] = value 88 | } 89 | return prefixed 90 | } 91 | -------------------------------------------------------------------------------- /api/v1alpha2/paasns_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | //revive:disable:exported 8 | 9 | package v1alpha2 10 | 11 | import ( 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | ) 14 | 15 | // Definitions to manage status conditions 16 | const ( 17 | // TypeReadyPaasNs represents the status of the PaasNs reconciliation 18 | TypeReadyPaasNs = "Ready" 19 | // TypeHasErrorsPaasNs represents the status used when the PaasNs reconciliation holds errors. 20 | TypeHasErrorsPaasNs = "HasErrors" 21 | // TypeDegradedPaasNs represents the status used when the PaasNs is deleted 22 | // and the finalizer operations are yet to occur. 23 | TypeDegradedPaasNs = "Degraded" 24 | 25 | instanceLabel = "app.kubernetes.io/instance" 26 | ) 27 | 28 | // PaasNSSpec defines the desired state of PaasNS 29 | type PaasNSSpec struct { 30 | // Deprecated: this has no function anymore and will be deleted in the next version. 31 | // +kubebuilder:validation:MinLength=1 32 | // +kubebuilder:validation:Optional 33 | Paas string `json:"paas,omitempty"` 34 | // Keys of the groups, as defined in the related `paas`, which should get access to 35 | // the namespace created by this PaasNS. When not set, all groups as defined in the related 36 | // `paas` get access to the namespace created by this PaasNS. 37 | // +kubebuilder:validation:Optional 38 | Groups []string `json:"groups,omitempty"` 39 | // Secrets which should exist in the namespace created through this PaasNS, 40 | // the values are the encrypted secrets through Crypt 41 | // +kubebuilder:validation:Optional 42 | Secrets map[string]string `json:"secrets,omitempty"` 43 | } 44 | 45 | // +kubebuilder:object:root=true 46 | // +kubebuilder:subresource:status 47 | // +kubebuilder:storageversion 48 | // +kubebuilder:conversion:hub 49 | // +kubebuilder:resource:path=paasns,scope=Namespaced 50 | 51 | // PaasNS is the Schema for the PaasNS API 52 | type PaasNS struct { 53 | metav1.TypeMeta `json:""` 54 | metav1.ObjectMeta `json:"metadata,omitempty"` 55 | 56 | Spec PaasNSSpec `json:"spec,omitempty"` 57 | } 58 | 59 | // +kubebuilder:object:root=true 60 | 61 | // PaasNSList contains a list of PaasNS 62 | type PaasNSList struct { 63 | metav1.TypeMeta `json:""` 64 | metav1.ListMeta `json:"metadata,omitempty"` 65 | Items []PaasNS `json:"items"` 66 | } 67 | 68 | func init() { 69 | SchemeBuilder.Register(&PaasNS{}, &PaasNSList{}) 70 | } 71 | 72 | func (pns PaasNS) ClonedLabels() map[string]string { 73 | labels := make(map[string]string) 74 | for key, value := range pns.Labels { 75 | if key != "app.kubernetes.io/instance" { 76 | labels[key] = value 77 | } 78 | } 79 | return labels 80 | } 81 | -------------------------------------------------------------------------------- /docs/user-guide/02_application-namespaces.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Adding Application namespaces 3 | summary: How to add application namespaces to a paas 4 | authors: 5 | - hikarukin 6 | - devotional-phoenix-97 7 | date: 2025-01-20 8 | --- 9 | 10 | # Application namespaces 11 | 12 | To deploy a (micro) service, usually a Paas would be extended by one or more namespaces. 13 | Mostly these namespaces would be used for running the actual application components. 14 | All application namespaces use a combined quota belonging specifically to this Paas. 15 | 16 | ## Setting Paas application namespace quota 17 | 18 | Each Paas spec has a `required` field for specifying quota. 19 | Each quota has a name referring to the exact [Resource Type](https://kubernetes.io/docs/concepts/policy/resource-quotas/#compute-resource-quota) 20 | and has a value defined as a [k8s Resource Quantity](https://kubernetes.io/docs/reference/kubernetes-api/common-definitions/quantity/). 21 | 22 | This setting is applied to a Cluster Resource Quota which is applied to all application 23 | namespaces created for this Paas. 24 | 25 | !!! note 26 | 27 | Capabilities have their own separate quotas which can be set from the capability block of a Paas. 28 | Capability quotas do not need to be included in the application quota. 29 | 30 | !!! example 31 | 32 | ```yaml 33 | apiVersion: cpet.belastingdienst.nl/v1alpha2 34 | kind: Paas 35 | metadata: 36 | name: tst-tst 37 | spec: 38 | quota: 39 | limits.cpu: '40' 40 | limits.memory: 64Gi 41 | requests.cpu: '20' 42 | requests.memory: 32Gi 43 | requests.storage: 200Gi 44 | ``` 45 | 46 | ## Adding namespaces in the Paas spec 47 | 48 | It is possible to define a list of extra namespaces to be created within the Paas. 49 | These can be used for various purposes like dev, test and prod or for example a 50 | team member's personal test. 51 | 52 | These namespaces count towards the global quota requested by the Paas. 53 | 54 | !!! example 55 | 56 | ```yaml 57 | apiVersion: cpet.belastingdienst.nl/v1alpha2 58 | kind: Paas 59 | metadata: 60 | name: tst-tst 61 | spec: 62 | namespaces: 63 | mark: {} 64 | tst: {} 65 | acceptance: {} 66 | prod: {} 67 | joel: {} 68 | ``` 69 | 70 | ## Adding PaasNs resources 71 | 72 | Alternatively, a PaasNS resource could be added to a namespace belonging to the Paas. 73 | Read more about this feature in [the PaasNS documentation](../overview/core_concepts/paasns.md). 74 | 75 | !!! example 76 | 77 | ```yaml 78 | --- 79 | apiVersion: cpet.belastingdienst.nl/v1alpha2 80 | kind: PaasNS 81 | metadata: 82 | name: my-ns 83 | namespace: my-paas-argocd 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/overview/core_concepts/paas.md: -------------------------------------------------------------------------------- 1 | # Paas 2 | 3 | The whole idea is to create a multi tenancy solution which allows DevOps teams 4 | to request a context for their project, which we like to call a 'Project as a Service', 5 | e.a. Paas. 6 | 7 | The Paas is a higher level construct, which consists of many parts, including: 8 | 9 | - namespaces; 10 | - Quotas; 11 | - authorization; 12 | - capabilities; 13 | 14 | DevOps teams request this Paas context by defining a Paas resource through the K8S API. 15 | 16 | At the very least a Paas resource has the following defined: 17 | 18 | - `apiVersion`, kind (as needs to be defined for every other k8s resource) 19 | - `metadata.name`, which is unique (cluster-wide) 20 | - `spec.requestor`, which is an informational field representing the requestor of 21 | this Paas, for administrative purposes 22 | - `quota`, which sets the amount of quota for all namespaces that are part of 23 | this Paas (capability namespaces excluded) 24 | 25 | Additionally, the following optional settings can also be defined: 26 | 27 | - `capabilities`, which can be used to enable Paas extensions such as an ArgoCD to 28 | manage all Paas namespaces, Grafana to monitor Paas namespaces, etc. More information 29 | can be found in our [capabilities](capabilities.md) documentation. 30 | - `spec.secrets`, which can be used to seed secrets that ArgoCD requires for 31 | accessing repositories. See [secrets](secrets.md) for more information. 32 | - `spec.groups`, which can be used to configure authorization. See [authorization](authorization.md) 33 | for more information. 34 | - `spec.namespaces`, which can be used to define namespaces as part of the Paas. 35 | Alternatively, they can be manually defined as [PaasNs](paasns.md) resources. 36 | 37 | ## Example Paas 38 | 39 | !!! example 40 | 41 | ```yaml 42 | apiVersion: cpet.belastingdienst.nl/v1alpha2 43 | kind: Paas 44 | metadata: 45 | name: my-paas 46 | spec: 47 | capabilities: 48 | # Define argocd 49 | argocd: 50 | custom_fields: 51 | # Bootstrap application to point to the root folder 52 | gitPath: . 53 | # Bootstrap application to point to the main branch 54 | gitRevision: main 55 | # Bootstrap application to point to this repo 56 | gitUrl: "ssh://git@github.com/belastingdienst/my-paas-repo.git" 57 | # Define grafana 58 | grafana: 59 | quota: 60 | limits.cpu: "5" 61 | limits.memory: "2Gi" 62 | ``` 63 | 64 | !!! notes 65 | 66 | Labels defined on Paas resources are copied to child resources such as PaasNs, 67 | quotas, groups, ArgoApps, ArgoProjects, etc. 68 | 69 | The only exception is the `app.kubernetes.io/instance`. 70 | -------------------------------------------------------------------------------- /.github/workflows/build-and-release-images.yml: -------------------------------------------------------------------------------- 1 | name: Build images and add to ghcr.io upon release 2 | on: 3 | release: 4 | types: 5 | - published 6 | 7 | jobs: 8 | operator_image: 9 | name: Push operator image to ghcr.io 10 | runs-on: ubuntu-latest 11 | permissions: 12 | packages: write 13 | contents: write 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 17 | with: 18 | persist-credentials: false 19 | 20 | # https://github.com/docker/setup-qemu-action 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 23 | 24 | # https://github.com/docker/setup-buildx-action 25 | - name: Set up Docker Buildx 26 | id: buildx 27 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 28 | 29 | - name: Login to GHCR 30 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 31 | with: 32 | registry: ghcr.io 33 | username: ${{ github.actor }} 34 | password: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Docker meta 37 | id: opr-paas-meta 38 | uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 39 | with: 40 | images: | 41 | name=ghcr.io/belastingdienst/opr-paas,enable=true 42 | tags: | 43 | type=semver,pattern={{raw}} 44 | type=raw,value=latest 45 | type=sha 46 | 47 | - name: Build and push operator image 48 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 49 | with: 50 | context: . 51 | push: true 52 | platforms: 'linux/amd64,linux/arm64' 53 | tags: ${{ steps.opr-paas-meta.outputs.tags }} 54 | labels: ${{ steps.opr-paas-meta.outputs.labels }} 55 | build-args: VERSION=${{ github.ref_name }} 56 | 57 | - name: Generate opr-paas image SBOM 58 | uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1 59 | with: 60 | image-ref: 'ghcr.io/belastingdienst/opr-paas' 61 | scan-type: image 62 | format: 'github' 63 | output: 'opr-paas_image.sbom.json' 64 | github-pat: ${{ secrets.GITHUB_TOKEN }} 65 | severity: 'MEDIUM,HIGH,CRITICAL' 66 | scanners: 'vuln' 67 | env: 68 | TRIVY_SKIP_DB_UPDATE: true 69 | TRIVY_SKIP_JAVA_DB_UPDATE: true 70 | 71 | - name: Add SBOM to release 72 | uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 73 | with: 74 | files: '${{ github.workspace }}/opr-paas_image.sbom.json' -------------------------------------------------------------------------------- /docs/development-guide/30_submitting-a-pr.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Submitting a Pull Request 3 | summary: How to submit a Pull Request 4 | authors: 5 | - devotional-phoenix-97 6 | - hikarukin 7 | date: 2024-09-23 8 | --- 9 | 10 | > First and foremost: as a potential contributor, your changes and ideas are 11 | > welcome at any hour of the day or night, weekdays, weekends, and holidays. 12 | > Please do not ever hesitate to ask a question or send a PR. 13 | 14 | !!! tip 15 | 16 | Before you submit a pull request, please read this document from the Istio 17 | documentation which contains very good insights and best practices: 18 | ["Writing Good Pull Requests"](31_writing-good-pull-request.md). 19 | 20 | If you have written code for an improvement to Paas or a bug fix, please follow 21 | this procedure to submit a pull request: 22 | 23 | 1. Create a fork of the Paas Operator project; 24 | 2. Add a comment to the related issue to let us know you're working on it; 25 | 3. Develop your feature or fix on your forked repository; 26 | 3. Run the e2e tests in your forked repository, see our [related e2e testing](40_e2e-tests.md) 27 | documentation; 28 | 4. Once development is finished, create a pull request from your forked project 29 | to the Paas project. 30 | Please make sure the pull request title and message follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 31 | 32 | One of the maintainers will then proceed with the first review and approve the 33 | CI workflow to run in the Paas project. The second reviewer will run 34 | end-to-end test against the changes in fork pull request. If testing passes, 35 | the pull request will be labeled with `ok-to-merge` and will be ready for 36 | merge. 37 | 38 | Sign your work 39 | -------------- 40 | 41 | We use the Developer Certificate of Origin (DCO) as an additional safeguard for 42 | the Paas project. This is a well established and widely used mechanism to assure 43 | contributors have confirmed their right to license their contribution under the 44 | project's license. 45 | 46 | Please read [https://developercertificate.org](https://developercertificate.org). 47 | 48 | If you can certify it, then just add a line to every git commit message: 49 | 50 | !!! example 51 | 52 | ``` 53 | Signed-off-by: Random J Developer 54 | ``` 55 | 56 | or use the command `git commit -s -m "commit message comes here"` to sign-off on your commits. 57 | 58 | Use your real name (sorry, no pseudonyms or anonymous contributions). 59 | If you set your `user.name` and `user.email` git configs, you can sign your 60 | commit automatically with `git commit -s`. 61 | 62 | You can also use git [aliases](https://git-scm.com/book/en/v2/Git-Basics-Git-Aliases) 63 | like `git config --global alias.ci 'commit -s'`. Now you can commit with `git ci` 64 | and the commit will be signed. -------------------------------------------------------------------------------- /internal/webhook/v1alpha1/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025, Tax Administration of The Netherlands. 3 | Licensed under the EUPL 1.2. 4 | See LICENSE.md for details. 5 | */ 6 | 7 | package v1alpha1 8 | 9 | import ( 10 | "context" 11 | 12 | "github.com/belastingdienst/opr-paas-crypttool/pkg/crypt" 13 | "github.com/belastingdienst/opr-paas/v4/internal/config" 14 | "github.com/belastingdienst/opr-paas/v4/internal/logging" 15 | corev1 "k8s.io/api/core/v1" 16 | "k8s.io/apimachinery/pkg/types" 17 | "sigs.k8s.io/controller-runtime/pkg/client" 18 | ) 19 | 20 | var ( 21 | crypts map[string]*crypt.Crypt 22 | decryptPrivateKeys *crypt.PrivateKeys 23 | ) 24 | 25 | // TODO: devotional-phoenix-97: We should refine this code and the entire crypt implementation including caching. 26 | 27 | // resetCrypts removes all crypts and resets decryptSecretPrivateKeys 28 | func resetCrypts() { 29 | crypts = map[string]*crypt.Crypt{} 30 | decryptPrivateKeys = nil 31 | } 32 | 33 | // getRsaPrivateKeys fetches secret, compares to cached private keys, resets crypts if needed, and returns keys 34 | func getRsaPrivateKeys(ctx context.Context, c client.Client) (*crypt.PrivateKeys, error) { 35 | ctx, logger := logging.GetLogComponent(ctx, logging.WebhookUtilsComponentV1) 36 | rsaSecret := &corev1.Secret{} 37 | conf, err := config.GetConfigFromContextV1(ctx) 38 | if err != nil { 39 | return nil, err 40 | } 41 | namespacedName := conf.Spec.DecryptKeysSecret 42 | 43 | err = c.Get(ctx, types.NamespacedName{ 44 | Name: namespacedName.Name, 45 | Namespace: namespacedName.Namespace, 46 | }, rsaSecret) 47 | if err != nil { 48 | return nil, err 49 | } 50 | // Create new set of keys from data in secret 51 | keys, err := crypt.NewPrivateKeysFromSecretData(rsaSecret.Data) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | if decryptPrivateKeys != nil { 57 | if keys.Compare(*decryptPrivateKeys) { 58 | // It already was the same secret 59 | logger.Debug().Msg("reusing decrypt keys") 60 | return decryptPrivateKeys, nil 61 | } 62 | } 63 | 64 | logger.Debug().Msgf("setting (%d) new keys", len(keys)) 65 | resetCrypts() 66 | decryptPrivateKeys = &keys 67 | return decryptPrivateKeys, nil 68 | } 69 | 70 | // getRsa returns a crypt.Crypt for a specified paasName 71 | func getRsa(ctx context.Context, c client.Client, paasName string) (*crypt.Crypt, error) { 72 | var mycrypt *crypt.Crypt 73 | if keys, err := getRsaPrivateKeys(ctx, c); err != nil { 74 | return nil, err 75 | } else if rsa, exists := crypts[paasName]; exists { 76 | return rsa, nil 77 | } else if mycrypt, err = crypt.NewCryptFromKeys(*keys, "", paasName); err != nil { 78 | return nil, err 79 | } 80 | _, logger := logging.GetLogComponent(ctx, logging.WebhookUtilsComponentV1) 81 | logger.Debug().Msgf("creating new crypt for %s", paasName) 82 | crypts[paasName] = mycrypt 83 | return mycrypt, nil 84 | } 85 | -------------------------------------------------------------------------------- /internal/utils/notifier.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/fsnotify/fsnotify" 8 | ) 9 | 10 | // FileWatcher is a struct that can watch for file changes 11 | type FileWatcher struct { 12 | watcher *fsnotify.Watcher 13 | files []string 14 | count int 15 | lastCount int 16 | } 17 | 18 | // NewFileWatcher creates a FileWatcher resource and runs the watch method in a separate thread 19 | func NewFileWatcher(paths ...string) *FileWatcher { 20 | fw := &FileWatcher{ 21 | files: paths, 22 | count: 0, 23 | } 24 | go func() { 25 | _ = fw.watch() 26 | }() 27 | return fw 28 | } 29 | 30 | // WasTriggered is true when a filechange was noticed 31 | func (fw *FileWatcher) WasTriggered() bool { 32 | if fw.lastCount != fw.count { 33 | fw.lastCount = fw.count 34 | // kubernetes removes and creates a file when a mounted secret or configmap is changed 35 | // refresh will re-add the newly created files after they have been changed 36 | _ = fw.Refresh() 37 | return true 38 | } 39 | return false 40 | } 41 | 42 | // Refresh can be used to (re-)add files to the watcher (e.a. if filechanges have been noticed which might mean that 43 | // directory contents have changed) 44 | func (fw *FileWatcher) Refresh() (err error) { 45 | // Notes from fsnotify.Watcher.Add(): 46 | // - A path can only be watched once; watching it more than once is a no-op and will not return an error. 47 | // - Paths that do not yet exist on the filesystem cannot be watched. 48 | // - A watch will be automatically removed if the watched path is deleted or renamed. T 49 | for _, p := range fw.files { 50 | err = fw.watcher.Add(p) 51 | if err != nil { 52 | return fmt.Errorf("%q: %w", p, err) 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | func (fw *FileWatcher) watch() (err error) { 59 | fw.watcher, err = fsnotify.NewWatcher() 60 | if err != nil { 61 | return fmt.Errorf("issue %w while creating a watcher for these files: %v", err, fw.files) 62 | } 63 | defer fw.watcher.Close() 64 | 65 | go fw.watchLoop() 66 | if err = fw.Refresh(); err != nil { 67 | return err 68 | } 69 | 70 | <-make(chan struct{}) // Block forever 71 | return nil 72 | } 73 | 74 | // watchLoop is the inner function which loops until a change is noticed and then runs callBack func 75 | func (fw *FileWatcher) watchLoop() { 76 | for { 77 | select { 78 | case err, ok := <-fw.watcher.Errors: 79 | if !ok { // Channel was closed (i.e. Watcher.Close() was called). 80 | return 81 | } 82 | log.Printf("ERROR: %s", err) 83 | case e, ok := <-fw.watcher.Events: 84 | if !ok { // Channel was closed (i.e. Watcher.Close() was called). 85 | return 86 | } 87 | 88 | // Just print the event nicely aligned, and keep track how many 89 | // events we've seen. 90 | fw.count++ 91 | log.Printf("secret notification: %3d/%3d %s", fw.count, fw.lastCount, e) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/e2e/clusterresourcequota_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | corev1 "k8s.io/api/core/v1" 8 | 9 | api "github.com/belastingdienst/opr-paas/v4/api/v1alpha2" 10 | quotav1 "github.com/openshift/api/quota/v1" 11 | "github.com/stretchr/testify/assert" 12 | "k8s.io/apimachinery/pkg/api/resource" 13 | "sigs.k8s.io/e2e-framework/pkg/envconf" 14 | "sigs.k8s.io/e2e-framework/pkg/features" 15 | ) 16 | 17 | const paasWithQuota = "paas-with-quota" 18 | 19 | func TestClusterResourceQuota(t *testing.T) { 20 | paasSpec := api.PaasSpec{ 21 | Requestor: "paas-user", 22 | Quota: map[corev1.ResourceName]resource.Quantity{ 23 | "cpu": resource.MustParse("200m"), 24 | "memory": resource.MustParse("256Mi"), 25 | }, 26 | } 27 | 28 | testenv.Test( 29 | t, 30 | features.New("ClusterResourceQuota"). 31 | Setup(createPaasFn(paasWithQuota, paasSpec)). 32 | Assess("is created", assertCRQCreated). 33 | Assess("is updated", assertCRQUpdated). 34 | Assess("is deleted when Paas is deleted", assertCRQDeleted). 35 | Teardown(teardownPaasFn(paasWithQuota)). 36 | Feature(), 37 | ) 38 | } 39 | 40 | func assertCRQCreated(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { 41 | crq := getCRQ(ctx, t, cfg) 42 | 43 | // ClusterResourceQuota is created with the same name as the Paas 44 | assert.Equal(t, paasWithQuota, crq.Name) 45 | // The label selector matches the Paas name 46 | assert.Equal(t, paasWithQuota, crq.Spec.Selector.LabelSelector.MatchLabels["q.lbl"]) 47 | // The quota size matches those passed in the Paas spec 48 | assert.Equal(t, resource.MustParse("200m"), *crq.Spec.Quota.Hard.Cpu()) 49 | assert.Equal(t, resource.MustParse("256Mi"), *crq.Spec.Quota.Hard.Memory()) 50 | 51 | return ctx 52 | } 53 | 54 | func assertCRQUpdated(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { 55 | paas := getPaas(ctx, paasWithQuota, t, cfg) 56 | 57 | paas.Spec.Quota = map[corev1.ResourceName]resource.Quantity{ 58 | "cpu": resource.MustParse("100m"), 59 | "memory": resource.MustParse("128Mi"), 60 | } 61 | 62 | if err := updateSync(ctx, cfg, paas, api.TypeReadyPaas); err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | crq := getCRQ(ctx, t, cfg) 67 | 68 | assert.Equal(t, resource.MustParse("100m"), *crq.Spec.Quota.Hard.Cpu()) 69 | assert.Equal(t, resource.MustParse("128Mi"), *crq.Spec.Quota.Hard.Memory()) 70 | 71 | return ctx 72 | } 73 | 74 | func assertCRQDeleted(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { 75 | deletePaasSync(ctx, paasWithQuota, t, cfg) 76 | crqs := listOrFail(ctx, "", "av1.ClusterResourceQuotaList{}, t, cfg) 77 | 78 | assert.Empty(t, crqs.Items) 79 | 80 | return ctx 81 | } 82 | 83 | func getCRQ(ctx context.Context, t *testing.T, cfg *envconf.Config) *quotav1.ClusterResourceQuota { 84 | return getOrFail(ctx, paasWithQuota, cfg.Namespace(), "av1.ClusterResourceQuota{}, t, cfg) 85 | } 86 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2' 3 | run: 4 | allow-parallel-runners: true 5 | linters: 6 | default: none 7 | enable: 8 | - ginkgolinter 9 | - gocyclo 10 | - govet 11 | - ineffassign 12 | - misspell 13 | - revive 14 | - staticcheck 15 | #- unused 16 | settings: 17 | govet: 18 | enable: 19 | - shadow 20 | settings: 21 | shadow: 22 | # Whether to be strict about shadowing; can be noisy. 23 | # Default: false 24 | strict: true 25 | revive: 26 | rules: 27 | - name: add-constant 28 | arguments: 29 | - allowInts: 0,1,2,3 30 | allowStrs: '""' 31 | ignoreFuncs: assert\.Len,require\.Len 32 | maxLitCount: '5' 33 | - name: line-length-limit 34 | arguments: 35 | - 120 36 | severity: warning 37 | exclude: 38 | - '' 39 | - name: comment-spacings 40 | - name: indent-error-flow 41 | - name: use-errors-new 42 | - name: bare-return 43 | - name: cognitive-complexity 44 | # TODO: wait for https://github.com/golangci/golangci-lint/pull/5663 45 | disabled: true 46 | - name: context-as-argument 47 | - name: cyclomatic 48 | disabled: true 49 | - name: dot-imports 50 | arguments: 51 | - allowedPackages: 52 | - github.com/onsi/ginkgo/v2 53 | - github.com/onsi/gomega 54 | - name: early-return 55 | - name: empty-block 56 | - name: empty-lines 57 | - name: exported 58 | - name: function-length 59 | - name: if-return 60 | - name: import-alias-naming 61 | - name: import-shadowing 62 | - name: increment-decrement 63 | - name: max-control-nesting 64 | - name: max-public-structs 65 | arguments: 66 | - 14 67 | - name: redefines-builtin-id 68 | - name: receiver-naming 69 | - name: redundant-import-alias 70 | - name: struct-tag 71 | - name: superfluous-else 72 | - name: unchecked-type-assertion 73 | - name: unexported-naming 74 | staticcheck: 75 | checks: 76 | - all 77 | - '-ST1000' 78 | - '-ST1003' 79 | - '-ST1016' 80 | - '-SA1019' 81 | - '-ST1020' 82 | - '-ST1021' 83 | - '-ST1022' 84 | exclusions: 85 | generated: lax 86 | rules: 87 | - linters: 88 | - dupl 89 | path: internal/* 90 | paths: 91 | - third_party$ 92 | - builtin$ 93 | - examples$ 94 | formatters: 95 | enable: 96 | - gofmt 97 | - goimports 98 | exclusions: 99 | generated: lax 100 | paths: 101 | - third_party$ 102 | - builtin$ 103 | - examples$ 104 | -------------------------------------------------------------------------------- /internal/webhook/v1alpha1/validated_secrets.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "crypto/sha512" 5 | "fmt" 6 | 7 | "github.com/belastingdienst/opr-paas-crypttool/pkg/crypt" 8 | "github.com/belastingdienst/opr-paas/v4/api/v1alpha1" 9 | "k8s.io/apimachinery/pkg/util/validation/field" 10 | ) 11 | 12 | type validatedHash [64]byte 13 | 14 | func hashFromString(s string) (h validatedHash) { 15 | return sha512.Sum512([]byte(s)) 16 | } 17 | 18 | // validatedSecrets is a small helper struct to hold hashes from a Paas / oldPaasNS and compares secrets 19 | // from another PaasNS. If it is in here it is already validated and does not require validation to safe resources. 20 | type validatedSecrets struct { 21 | v map[validatedHash]bool 22 | } 23 | 24 | // appendFromPaas appends validated secrets from a Paas 25 | func (vs *validatedSecrets) appendFromPaas(paas v1alpha1.Paas) { 26 | if vs.v == nil { 27 | vs.v = map[validatedHash]bool{} 28 | } 29 | for _, secret := range paas.Spec.SSHSecrets { 30 | hash := hashFromString(secret) 31 | vs.v[hash] = true 32 | } 33 | for _, capability := range paas.Spec.Capabilities { 34 | for _, secret := range capability.SSHSecrets { 35 | hash := sha512.Sum512([]byte(secret)) 36 | vs.v[hash] = true 37 | } 38 | } 39 | } 40 | 41 | // appendFromPaasNS appends validated secrets from a PaasNS 42 | func (vs *validatedSecrets) appendFromPaasNS(paasns v1alpha1.PaasNS) { 43 | if vs.v == nil { 44 | vs.v = make(map[validatedHash]bool) 45 | } 46 | for _, secret := range paasns.Spec.SSHSecrets { 47 | hash := hashFromString(secret) 48 | vs.v[hash] = true 49 | } 50 | } 51 | 52 | // Is can be used to check if a hash is already validated 53 | func (vs *validatedSecrets) Is(hash validatedHash) bool { 54 | _, exists := vs.v[hash] 55 | return exists 56 | } 57 | 58 | // compareSecrets can check all secrets from a PaasNS. 59 | // It first checks against the secrets in the validated struct, 60 | // and if not present in there it uses the getRsaFunc to get a crypt and try decrypting the secret 61 | func (vs validatedSecrets) compareSecrets( 62 | unvalidated map[string]string, 63 | getRsaFunc func() (*crypt.Crypt, error), 64 | ) (errs field.ErrorList) { 65 | // Err when an sshSecret can't be decrypted 66 | for secretName, secret := range unvalidated { 67 | if vs.Is(hashFromString(secret)) { 68 | continue 69 | } 70 | if cryptObj, err := getRsaFunc(); err != nil { 71 | errs = append(errs, &field.Error{ 72 | Type: field.ErrorTypeInvalid, 73 | Field: field.NewPath("spec").Child("sshSecrets").Key(secretName).String(), 74 | Detail: fmt.Errorf("failed to get crypt: %w", err).Error(), 75 | }) 76 | } else if _, err = cryptObj.Decrypt(secret); err != nil { 77 | errs = append(errs, &field.Error{ 78 | Type: field.ErrorTypeInvalid, 79 | Field: field.NewPath("spec").Child("sshSecrets").Key(secretName).String(), 80 | BadValue: secret, 81 | Detail: fmt.Errorf("failed to decrypt secret: %w", err).Error(), 82 | }) 83 | } 84 | } 85 | return errs 86 | } 87 | -------------------------------------------------------------------------------- /docs/administrators-guide/v1alpha1-conversion.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Converting v1alpha1 to v1alpha2 3 | summary: A detailed description of how to convert a v1alpha1.PaasConfig to v1alpha2.PaasConfig. 4 | authors: 5 | - Devotional Phoenix 6 | date: 2025-07-09 7 | --- 8 | 9 | # Introduction 10 | 11 | With release v2 we also released a new api v1alpha2 which has a slightly changed definition. 12 | The following has changed between v1alpha1.PaasConfig and v1alpha2.PaasConfig: 13 | 14 | - The following endpoints where deprecated in v1alpha1 and have been removed in v1alpha2: 15 | - GroupSyncList, GroupSyncListKey, LDAP 16 | (we no longer manage the LDAP GroupSyncList implementation) 17 | - ArgoPermissions, ArgoEnabled, ExcludeAppSetName 18 | (these setting belong to a hardcoded implementation which is replaced by a more flexible implementation) 19 | - A new label implementation replaces the following label options which are removed in v1alpha2 20 | - RequestorLabel 21 | - ManagedByLabel 22 | - ManagedBySuffix 23 | - **note** QuotaLabel is replaced, will be deprecated in v1alpha2 and removed in v1alpha3 24 | 25 | Additionally, some new implementations require additional config. Which is documented in the rest of the 26 | Administrators guide, but documented here as well, so that Administrators converting from v1alpha1 to v1alpha2 27 | also add these changes as part of the PaasConfig migration process. 28 | 29 | ## Conversion 30 | 31 | The Paas operator can work with both v1alpha1 and v1alpha2. 32 | Internally v1alpha1.PaasConfig is converted and stored as v1alpha2.PaasConfig, and converted back if the client requests 33 | a v1alpha1.PaasConfig. Switching to v1alpha2.PaasConfig is therefore recommended, but can be separately from deploying v2. 34 | 35 | ## Changing v1alpha1 to v1alpha2 36 | 37 | ### Removing deprecated fields 38 | 39 | The following fields in PaasConfig.Spec are removed in v1alpha2 and should be removed from the PaasConfig: 40 | - argoenabled 41 | - argopermissions 42 | - exclude_appset_name 43 | - groupsynclist 44 | - groupsynclist_key 45 | - ldap 46 | 47 | ### Adding custom fields with validations 48 | 49 | With v2, the previous implementation (hardcoded fields in all capabilities, just required by ArgoCD) is removed. 50 | There is now only [Custom fields](./capabilities.md#configuring-custom-fields). 51 | We advise to add the following custom fields to your argocd capability: 52 | 53 | !!! example 54 | 55 | ``` 56 | apiVersion: cpet.belastingdienst.nl/v1alpha2 57 | kind: PaasConfig 58 | metadata: 59 | name: learn-paas-config 60 | spec: 61 | capabilities: 62 | argocd: 63 | custom_fields: 64 | git_url: 65 | required: true 66 | git_revision: 67 | default: "main" 68 | git_path: 69 | default: "." 70 | ``` 71 | 72 | ### Labels and Capability fields 73 | 74 | - [Custom fields for all capabilities](./go-templating.md#custom-fields-for-all-capabilities) 75 | - [Labels with go templating](./go-templating.md#labels-with-go-templating) (v3 only) -------------------------------------------------------------------------------- /pkg/templating/main.go: -------------------------------------------------------------------------------- 1 | package templating 2 | 3 | import ( 4 | "bytes" 5 | "text/template" 6 | 7 | "github.com/go-sprout/sprout" 8 | "github.com/go-sprout/sprout/group/all" 9 | 10 | "github.com/belastingdienst/opr-paas/v4/api" 11 | "github.com/belastingdienst/opr-paas/v4/api/v1alpha1" 12 | "github.com/belastingdienst/opr-paas/v4/api/v1alpha2" 13 | "github.com/belastingdienst/opr-paas/v4/pkg/fields" 14 | ) 15 | 16 | // PaasUnion is an interface representing either a v1alpha1.Paas or a v1alpha2.Paas 17 | type PaasUnion interface { 18 | v1alpha1.Paas | v1alpha2.Paas 19 | } 20 | 21 | // Templater is a struct that can hold a Paas and a PaasConfig and can run go-templates using these as input 22 | type Templater[P PaasUnion, C api.PaasConfig[S], S any] struct { 23 | Paas P 24 | Config C 25 | } 26 | 27 | // NewTemplater returns an initialized Templater from a Paas and PaasConfig 28 | func NewTemplater[P PaasUnion, C api.PaasConfig[S], S any](paas P, config C) Templater[P, C, S] { 29 | return Templater[P, C, S]{ 30 | Paas: paas, 31 | Config: config, 32 | } 33 | } 34 | 35 | func (t Templater[P, C, S]) getSproutFuncs() (template.FuncMap, error) { 36 | handler := sprout.New() 37 | err := handler.AddGroups(all.RegistryGroup()) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return handler.Build(), nil 42 | } 43 | 44 | // Verify can verify a template (just parsing it, not running it against a Paas / PaasConfig) 45 | func (t Templater[P, C, S]) Verify(name string, templatedText string) error { 46 | funcs, err := t.getSproutFuncs() 47 | if err != nil { 48 | return err 49 | } 50 | _, err = template.New(name).Funcs(funcs).Parse(templatedText) 51 | return err 52 | } 53 | 54 | // TemplateToString can be used to parse a go-template and return a string value 55 | func (t Templater[P, C, S]) TemplateToString(name string, templatedText string) (string, error) { 56 | buf := new(bytes.Buffer) 57 | funcs, err := t.getSproutFuncs() 58 | if err != nil { 59 | return "", err 60 | } 61 | tmpl, err := template.New(name).Funcs(funcs).Parse(templatedText) 62 | if err != nil { 63 | return "", err 64 | } 65 | err = tmpl.Execute(buf, t) 66 | if err != nil { 67 | return "", err 68 | } 69 | return buf.String(), nil 70 | } 71 | 72 | // TemplateToMap can be used to parse a go-template and try to parse as map or list. 73 | // If it can be parsed, it will prefix map keys / list indexes by `name` and return the map. 74 | // If it cannot be parsed as map / list, it will return a map with one key, value pair, where key = `name` and value 75 | // is the result. 76 | func (t Templater[P, C, S]) TemplateToMap(name string, templatedText string) (fields.ElementMap, error) { 77 | yamlData, templateErr := t.TemplateToString(name, templatedText) 78 | if templateErr != nil { 79 | return nil, templateErr 80 | } 81 | if myMap, err := yamlToMap([]byte(yamlData)); err == nil { 82 | return myMap.Prefix(name), nil 83 | } 84 | if myList, err := yamlToList([]byte(yamlData)); err == nil { 85 | return myList.AsElementMap().Prefix(name), nil 86 | } 87 | return fields.ElementMap{name: yamlData}, nil 88 | } 89 | -------------------------------------------------------------------------------- /docs/overview/core_concepts/paasns.md: -------------------------------------------------------------------------------- 1 | # PaasNs 2 | 3 | We wanted to enable our DevOps teams without them requiring self-provisioner 4 | permissions. The main reason is that self-provisioner is too broadly usable and 5 | abusable, and as such we could not enforce the guardrails we felt that a true 6 | multi-tenancy solution should protect. 7 | 8 | However, we also wanted to bring them as much self-service as we could think of 9 | and dynamically creating and destroying namespaces felt like part of the flexibility 10 | that would be required. 11 | 12 | For this exact reason, we have introduced the concept of PaasNs. 13 | 14 | The concept works as follows: 15 | 16 | ![paasns architecture](./paasns.png) 17 | 18 | The operator creates an overview of all namespaces that should be there. 19 | These namespaces could be required by: 20 | 21 | - a capability 22 | - an entry in the paas.Spec.Namespaces block 23 | - a PaasNs 24 | 25 | As an example, assuming a Paas called `my-paas` with: 26 | 27 | !!! example 28 | 29 | ```yaml 30 | --- 31 | apiVersion: cpet.belastingdienst.nl/v1alpha2 32 | kind: Paas 33 | metadata: 34 | name: my-paas 35 | spec: 36 | capabilities: 37 | argocd: {} 38 | requestor: my-team 39 | quota: 40 | limits.cpu: "40" 41 | secrets: 42 | 'ssh://git@my-git-host/my-git-repo.git': >- 43 | 2wkeKe...g== 44 | ``` 45 | 46 | To add user namespaces, the following options are available: 47 | 48 | - In this Paas, the `spec.namespaces` map could have a definitions of namespaces. 49 | If this was set to (just as an example) `{ ns1: {}, ns2: {}, ns3: {} }`, 50 | the Paas controller would create three PaasNs resources in a namespace called `my-paas`. 51 | 52 | The PaasNs controller would process them as being part of `my-paas` and create 53 | the following namespaces: `my-paas-ns1`, `my-paas-ns2` and `my-paas-ns3`. 54 | 55 | - Another option would be to manually create a PaasNs resource in a namespace 56 | which already belongs to `my-paas`. 57 | 58 | !!! example 59 | 60 | ```yaml 61 | --- 62 | apiVersion: cpet.belastingdienst.nl/v1alpha2 63 | kind: PaasNS 64 | metadata: 65 | name: my-ns 66 | namespace: my-paas-argocd 67 | ``` 68 | 69 | - Yet another option would be to create a PaasNs resource using automation such as 70 | `argocd` or `tekton`. 71 | It is advised to create them in the namespace belonging to the capability that 72 | is being used (e.a. `my-paas-argocd` or `my-paas-tekton`). 73 | 74 | - A cool feature is that PaasNs resources could be stacked. This means that a 75 | PaasNs resource could be in a namespace which is the product of a PaasNs 76 | resources in a namespace, which... 77 | 78 | As the top namespace is the product of a PaasNs resource in the namespace 79 | called after the Paas, all child PaasNs's are assumed to be part of the same Paas. 80 | 81 | !!! note 82 | 83 | Note that besides creating the namespaces, the PaasNs controller also properly 84 | sets up the namespace with the proper quota and the proper [authorization](authorization.md). 85 | -------------------------------------------------------------------------------- /docs/development-guide/maintaining.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Maintenance 3 | summary: Introduction to contributing to the Paas operator. 4 | authors: 5 | - devotional-phoenix-97 6 | - hikarukin 7 | date: 2024-07-04 8 | --- 9 | 10 | Introduction 11 | ============ 12 | 13 | This file documents the methods and standards that should be applied by the maintainers 14 | of this project. For example: how to create a new release. 15 | 16 | Standards used 17 | -------------- 18 | 19 | ### Commits 20 | 21 | We adhere to the [Conventional Commits v1.0](https://www.conventionalcommits.org/en/v1.0.0/) 22 | standard. 23 | 24 | ### Versioning 25 | 26 | For versioning purposes, we adhere to the [SemVer v2.0.0](https://semver.org/spec/v2.0.0.html) 27 | standard with a side note that we always prefix the semantic version number with 28 | the character 'v'. This stands for "version". 29 | 30 | As a quick summary, this means we use version numbers in the style of vX.Y.Z. 31 | With: 32 | 33 | - X being major, including breaking, changes; 34 | - Y being minor, possibly including patch but never breaking, changes; 35 | - Z being a patch, but never breaking, changes; 36 | 37 | Methods used 38 | ------------ 39 | 40 | ### Creating a Release 41 | 42 | We release from `main`. All changes to `main` are made through PRs. Merging a PR 43 | triggers the release drafter action to create a draft release. 44 | 45 | The process to create a release is mostly automated. To start it: 46 | 47 | * Merge one or more PRs to `main`; 48 | * Ensure completeness; 49 | * Edit the draft release and publish it. 50 | 51 | #### Important: No Backports Policy 52 | 53 | We do not support backports to previous release versions. 54 | Fixes are only provided for the **current main release series**. For example, if 55 | version 2.x.x is the active release series, no fixes will be made or backported 56 | to the 1.x.x line. 57 | 58 | We follow a **roll-forward support model**: 59 | 60 | * All users are expected to upgrade to the latest available release to receive fixes. 61 | * Fix releases (patch versions) are provided for the current main release series only. 62 | * Users remaining on older versions do so at their own risk, as we do not provide 63 | maintenance or security updates for them. 64 | 65 | This approach allows us to focus our efforts on improving the latest version without 66 | fragmenting support across multiple release lines. 67 | 68 | --- 69 | 70 | ### Creating a Hotfix Release 71 | 72 | Hotfix releases are created from the relevant tag. The process is similar to 73 | creating a regular release. 74 | 75 | The process is as follows: 76 | 77 | * Create a new branch based on the **latest release tag of the current main release series** 78 | that needs the fix; 79 | * Merge one or more PRs to this branch; 80 | * Ensure completeness; 81 | * Edit the draft release and publish it; 82 | Ensure the release only contains the hotfix! 83 | * Merge the hotfix branch back into `main` to keep `main` up to date. 84 | 85 | > **Note:** Hotfix releases are only supported for the actively maintained release 86 | series. We do not create hotfixes for previous major versions. 87 | --------------------------------------------------------------------------------