├── .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 | { loading=lazy }
31 | SVG source file
32 |
33 |
34 | ### PNG formatted size variants
35 |
36 |
37 | { loading=lazy }
38 | 16x16px
39 |
40 |
41 |
42 | { loading=lazy }
43 | 32x32px
44 |
45 |
46 |
47 | { loading=lazy }
48 | 120x80px
49 |
50 |
51 |
52 | { loading=lazy }
53 | 149x100px
54 |
55 |
56 |
57 | { loading=lazy }
58 | 180x180px
59 |
60 |
61 |
62 | { loading=lazy }
63 | 400x400px
64 |
65 |
66 |
67 | { 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 | 
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 |
--------------------------------------------------------------------------------