├── CODEOWNERS ├── config ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── certmanager │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── certificate.yaml ├── webhook │ ├── kustomization.yaml │ ├── service.yaml │ ├── kustomizeconfig.yaml │ └── manifests.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── samples │ ├── kustomization.yaml │ ├── test_v1_object.yaml │ ├── styra_v1beta1_system.yaml │ ├── styra_v1alpha1_library.yaml │ └── config_v2alpha2_projectconfig.yaml ├── crd │ ├── patches │ │ ├── cainjection_in_styra_systems.yaml │ │ ├── cainjection_in_styra_libraries.yaml │ │ ├── cainjection_in_config_projectconfigs.yaml │ │ ├── webhook_in_styra_systems.yaml │ │ ├── webhook_in_styra_libraries.yaml │ │ └── webhook_in_config_projectconfigs.yaml │ ├── kustomizeconfig.yaml │ └── kustomization.yaml ├── rbac │ ├── service_account.yaml │ ├── auth_proxy_client_clusterrole.yaml │ ├── role_binding.yaml │ ├── auth_proxy_role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_service.yaml │ ├── styra_system_viewer_role.yaml │ ├── test_object_viewer_role.yaml │ ├── styra_library_viewer_role.yaml │ ├── kustomization.yaml │ ├── config_projectconfig_viewer_role.yaml │ ├── test_object_editor_role.yaml │ ├── styra_system_editor_role.yaml │ ├── styra_library_editor_role.yaml │ ├── config_projectconfig_editor_role.yaml │ ├── leader_election_role.yaml │ └── role.yaml └── default │ ├── manager_webhook_patch.yaml │ ├── manager_config_patch.yaml │ ├── webhookcainjection_mutating_patch.yaml │ ├── webhookcainjection_validating_patch.yaml │ ├── webhookcainjection_patch.yaml │ ├── manager_auth_proxy_patch.yaml │ └── config.yaml ├── docs ├── images │ ├── Styra │ │ └── system.png │ ├── controller-arch.dark.excalidraw.png │ ├── controller-arch.light.excalidraw.png │ ├── ocp-controller-arch.dark.excalidraw.png │ └── ocp-controller-arch.light.excalidraw.png ├── releasing.md ├── installation.md └── design.md ├── internal ├── template │ ├── placeholder.go │ ├── pkg.tpl │ ├── members.tpl │ └── type.tpl ├── controller │ └── styra │ │ ├── styra_suite_test.go │ │ ├── styra.go │ │ └── system_controller_test.go ├── config │ ├── config_suite_test.go │ ├── config_test.go │ └── config.go ├── labels │ ├── labels_suite_test.go │ ├── labels_test.go │ └── labels.go ├── predicate │ ├── suite_test.go │ ├── predicate.go │ └── predicate_test.go ├── k8sconv │ └── suite_test.go ├── webhook │ ├── suite_test.go │ ├── mocks │ │ └── client.go │ └── styra │ │ ├── v1alpha1 │ │ └── library_webhook_test.go │ │ └── v1beta1 │ │ └── webhook_suite_test.go ├── fields │ └── fields.go ├── finalizer │ └── finalizer.go ├── errors │ └── errors.go └── sentry │ └── sentry.go ├── scripts └── gen-api-docs │ ├── README.md │ ├── config.json │ └── gen-api-docs.sh ├── SECURITY.md ├── .mockery.yaml ├── .gitignore ├── hack └── boilerplate.go.txt ├── pkg ├── styra │ ├── styra.go │ ├── suite_test.go │ ├── policies.go │ ├── client_test.go │ ├── policies_test.go │ ├── invitations.go │ ├── opaconfig_test.go │ ├── secrets_test.go │ ├── invitations_test.go │ ├── workspace.go │ ├── opaconfig.go │ ├── users.go │ ├── secrets.go │ ├── workspace_test.go │ ├── users_test.go │ └── library.go ├── ptr │ ├── suite_test.go │ ├── ptr.go │ └── ptr_test.go ├── ocp │ ├── opaconfig.go │ ├── client.go │ └── mocks │ │ └── client_interface.go ├── s3 │ ├── client.go │ ├── interface.go │ ├── mocks │ │ └── client.go │ └── minio.go └── httperror │ └── httperror.go ├── api ├── test │ └── v1 │ │ ├── v1.go │ │ ├── object_types.go │ │ ├── groupversion_info.go │ │ └── zz_generated.deepcopy.go ├── styra │ ├── v1beta1 │ │ ├── doc.go │ │ ├── v1beta1_suite_test.go │ │ ├── groupversion_info.go │ │ └── system_types_test.go │ └── v1alpha1 │ │ ├── doc.go │ │ ├── groupversion_info.go │ │ └── library_types.go └── config │ └── v2alpha2 │ └── groupversion_info.go ├── cmd └── suite_test.go ├── .golangci.yml ├── tools.go ├── .github ├── dependabot.yml └── workflows │ ├── codeql.yaml │ ├── tags.yaml │ ├── pr.yaml │ └── trivy.yml ├── PROJECT ├── .goreleaser.yaml └── README.md /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Bankdata/team-styra 2 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /docs/images/Styra/system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bankdata/styra-controller/HEAD/docs/images/Styra/system.png -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /internal/template/placeholder.go: -------------------------------------------------------------------------------- 1 | // Package template is a placeholder file to make Go vendor this directory properly. 2 | package template 3 | -------------------------------------------------------------------------------- /docs/images/controller-arch.dark.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bankdata/styra-controller/HEAD/docs/images/controller-arch.dark.excalidraw.png -------------------------------------------------------------------------------- /docs/images/controller-arch.light.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bankdata/styra-controller/HEAD/docs/images/controller-arch.light.excalidraw.png -------------------------------------------------------------------------------- /docs/images/ocp-controller-arch.dark.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bankdata/styra-controller/HEAD/docs/images/ocp-controller-arch.dark.excalidraw.png -------------------------------------------------------------------------------- /docs/images/ocp-controller-arch.light.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bankdata/styra-controller/HEAD/docs/images/ocp-controller-arch.light.excalidraw.png -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: controller 8 | newTag: latest 9 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples of your project ## 2 | resources: 3 | - styra_v1beta1_system.yaml 4 | - test_v1_object.yaml 5 | - config_v2alpha2_projectconfig.yaml 6 | - styra_v1alpha1_library.yaml 7 | #+kubebuilder:scaffold:manifestskustomizesamples 8 | -------------------------------------------------------------------------------- /config/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 | -------------------------------------------------------------------------------- /internal/controller/styra/styra_suite_test.go: -------------------------------------------------------------------------------- 1 | package styra_test 2 | 3 | import ( 4 | "testing" 5 | 6 | ginkgo "github.com/onsi/ginkgo/v2" 7 | gomega "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestStyra(t *testing.T) { 11 | gomega.RegisterFailHandler(ginkgo.Fail) 12 | ginkgo.RunSpecs(t, "internal/controller/styra") 13 | } 14 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_styra_systems.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 7 | name: systems.styra.bankdata.dk 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_styra_libraries.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 7 | name: libraries.styra.bankdata.dk 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_config_projectconfigs.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 7 | name: projectconfigs.config.bankdata.dk 8 | -------------------------------------------------------------------------------- /scripts/gen-api-docs/README.md: -------------------------------------------------------------------------------- 1 | `gen-crd-api-reference-docs` is a binary that can be used to generate documentation for CRDs. Repository is https://github.com/ahmetb/gen-crd-api-reference-docs. 2 | 3 | Download the binary to `./bin` by running `make gen-crd-api-reference-docs`. 4 | 5 | There exists `make` targets for generating all documentation and a `make` target for each API. 6 | -------------------------------------------------------------------------------- /config/samples/test_v1_object.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: test.bankdata.dk/v1 2 | kind: Object 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: object 6 | app.kubernetes.io/instance: object-sample 7 | app.kubernetes.io/part-of: styra-controller 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: styra-controller 10 | name: object-sample 11 | spec: 12 | # TODO(user): Add fields here 13 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: serviceaccount 6 | app.kubernetes.io/instance: controller-manager-sa 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: styra-controller 9 | app.kubernetes.io/part-of: styra-controller 10 | app.kubernetes.io/managed-by: kustomize 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_styra_systems.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: systems.styra.bankdata.dk 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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The following versions will be supported with security fixes. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 0.3.0 < | :white_check_mark: | 10 | 11 | ## Reporting a vulnerability 12 | 13 | Please don't open an issue if you find a vulnerability in the project, instead 14 | use the github 15 | [security vulnerability report form](https://github.com/Bankdata/styra-controller/security/advisories/new). 16 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_styra_libraries.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: libraries.styra.bankdata.dk 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_config_projectconfigs.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: projectconfigs.config.bankdata.dk 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: metrics-reader 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: styra-controller 9 | app.kubernetes.io/part-of: styra-controller 10 | app.kubernetes.io/managed-by: kustomize 11 | name: metrics-reader 12 | rules: 13 | - nonResourceURLs: 14 | - "/metrics" 15 | verbs: 16 | - get 17 | -------------------------------------------------------------------------------- /.mockery.yaml: -------------------------------------------------------------------------------- 1 | dir: "{{ .InterfaceDir }}/mocks" 2 | filename: "{{ .InterfaceNameSnake }}.go" 3 | outpkg: "mocks" 4 | mockname: "{{ .InterfaceName }}" 5 | with-expecter: false 6 | disable-version-string: true 7 | packages: 8 | github.com/bankdata/styra-controller/pkg/styra: 9 | config: 10 | all: true 11 | github.com/bankdata/styra-controller/pkg/ocp: 12 | config: 13 | all: true 14 | github.com/bankdata/styra-controller/internal/webhook: 15 | config: 16 | all: true 17 | github.com/bankdata/styra-controller/pkg/s3: 18 | config: 19 | all: true 20 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: service 7 | app.kubernetes.io/instance: webhook-service 8 | app.kubernetes.io/component: webhook 9 | app.kubernetes.io/created-by: styra-controller 10 | app.kubernetes.io/part-of: styra-controller 11 | app.kubernetes.io/managed-by: kustomize 12 | name: webhook-service 13 | namespace: system 14 | spec: 15 | ports: 16 | - port: 443 17 | protocol: TCP 18 | targetPort: 9443 19 | selector: 20 | control-plane: controller-manager 21 | -------------------------------------------------------------------------------- /docs/releasing.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | Only maintainers with access to pushing tags are able to perform releases. If 4 | changes have been merged into the master branch but a release has not yet been 5 | scheduled, you can contact one of the maintainers to request and plan the 6 | release. 7 | 8 | ## Binaries and docker images 9 | 10 | In order to make a new release push a semver tag eg. `v0.1.0`. If you want to 11 | publish a prerelase, simply do a prerelease tag eg. `v0.2.0-rc.1`. 12 | 13 | This will run [goreleaser](https://goreleaser.com/) according to the 14 | configuration in `.goreleaser.yaml`. 15 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /.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 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Kubernetes Generated files - skip generated files, except for vendored files 19 | 20 | !vendor/**/zz_generated.* 21 | 22 | # editor and IDE paraphernalia 23 | .idea 24 | *.swp 25 | *.swo 26 | *~ 27 | 28 | # Local configuration files 29 | config/default/config_local.yaml 30 | config/samples/styra_v1beta1_system_local.yaml 31 | 32 | /dist -------------------------------------------------------------------------------- /config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/k8s-webhook-server/serving-certs 17 | name: cert 18 | readOnly: true 19 | volumes: 20 | - name: cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | args: 12 | - --config=/etc/styra-controller/config.yaml 13 | volumeMounts: 14 | - name: config 15 | mountPath: /etc/styra-controller 16 | - name: token 17 | mountPath: /etc/styra-controller-token 18 | volumes: 19 | - name: config 20 | secret: 21 | secretName: config 22 | - name: token 23 | secret: 24 | secretName: token -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: manager-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: styra-controller 9 | app.kubernetes.io/part-of: styra-controller 10 | app.kubernetes.io/managed-by: kustomize 11 | name: manager-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: manager-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_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: proxy-rolebinding 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: styra-controller 9 | app.kubernetes.io/part-of: styra-controller 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: proxy-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: rolebinding 6 | app.kubernetes.io/instance: leader-election-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: styra-controller 9 | app.kubernetes.io/part-of: styra-controller 10 | app.kubernetes.io/managed-by: kustomize 11 | name: leader-election-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: leader-election-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: proxy-role 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: styra-controller 9 | app.kubernetes.io/part-of: styra-controller 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-role 12 | rules: 13 | - apiGroups: 14 | - authentication.k8s.io 15 | resources: 16 | - tokenreviews 17 | verbs: 18 | - create 19 | - apiGroups: 20 | - authorization.k8s.io 21 | resources: 22 | - subjectaccessreviews 23 | verbs: 24 | - create 25 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: service 7 | app.kubernetes.io/instance: controller-manager-metrics-service 8 | app.kubernetes.io/component: kube-rbac-proxy 9 | app.kubernetes.io/created-by: styra-controller 10 | app.kubernetes.io/part-of: styra-controller 11 | app.kubernetes.io/managed-by: kustomize 12 | name: controller-manager-metrics-service 13 | namespace: system 14 | spec: 15 | ports: 16 | - name: https 17 | port: 8443 18 | protocol: TCP 19 | targetPort: https 20 | selector: 21 | control-plane: controller-manager 22 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Compatibility Table 2 | 3 | | Styra Controller | Styra DAS SaaS | Styra DAS Self-Hosted | 4 | |------------------|----------------|-----------------------| 5 | | v0.1.0 | 20230125 | 0.10.2 | 6 | | v0.29.0 | 20250814 | 0.17.3 | 7 | 8 | 9 | | OCP Controller | OPA Control Plane | 10 | |------------------|-------------------| 11 | | v0.30.0 | V0.0.1 | 12 | 13 | 14 | # Configuration of the ocp-controller 15 | The [configuration](https://github.com/Bankdata/styra-controller/blob/master/docs/configuration.md) document contains information about the configuration options for the OPA Control Plane Controller. 16 | -------------------------------------------------------------------------------- /internal/controller/styra/styra.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Bankdata. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package styra holds controllers for the styra API group. 18 | package styra 19 | -------------------------------------------------------------------------------- /pkg/styra/styra.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Bankdata. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package styra holds a client and helpers for interacting with the Styra 18 | // APIs. 19 | package styra 20 | -------------------------------------------------------------------------------- /config/samples/styra_v1beta1_system.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: styra.bankdata.dk/v1beta1 2 | kind: System 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: system 6 | app.kubernetes.io/instance: system-sample 7 | app.kubernetes.io/part-of: styra-controller 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: styra-controller 10 | styra-controller/control-plane: opa-control-plane 11 | name: system-sample 12 | spec: 13 | decisionMappings: 14 | - allowed: 15 | expected: 16 | boolean: true 17 | path: result.allowed 18 | name: api/authz/decision 19 | reason: 20 | path: result.reasons 21 | datasources: 22 | - path: "test" 23 | # TODO(user): Add fields here 24 | -------------------------------------------------------------------------------- /config/rbac/styra_system_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view systems. 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: system-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: styra-controller 10 | app.kubernetes.io/part-of: styra-controller 11 | app.kubernetes.io/managed-by: kustomize 12 | name: system-viewer-role 13 | rules: 14 | - apiGroups: 15 | - styra.bankdata.dk 16 | resources: 17 | - systems 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - styra.bankdata.dk 24 | resources: 25 | - systems/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/test_object_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view objects. 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: object-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: styra-controller 10 | app.kubernetes.io/part-of: styra-controller 11 | app.kubernetes.io/managed-by: kustomize 12 | name: object-viewer-role 13 | rules: 14 | - apiGroups: 15 | - test.bankdata.dk 16 | resources: 17 | - objects 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - test.bankdata.dk 24 | resources: 25 | - objects/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /scripts/gen-api-docs/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "hideMemberFields": [ 3 | "TypeMeta" 4 | ], 5 | "hideTypePatterns": [ 6 | "ParseError$", 7 | "List$" 8 | ], 9 | "externalPackages": [ 10 | { 11 | "typeMatchPrefix": "^k8s\\.io/apimachinery/pkg/apis/meta/v1\\.Duration$", 12 | "docsURLTemplate": "https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration" 13 | }, 14 | { 15 | "typeMatchPrefix": "^k8s\\.io/(api|apimachinery/pkg/apis)/", 16 | "docsURLTemplate": "https://v1-20.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#{{lower .TypeIdentifier}}-{{arrIndex .PackageSegments -1}}-{{arrIndex .PackageSegments -2}}" 17 | } 18 | ], 19 | "markdownDisabled": false 20 | } 21 | -------------------------------------------------------------------------------- /config/rbac/styra_library_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view libraries. 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: library-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: styra-controller 10 | app.kubernetes.io/part-of: styra-controller 11 | app.kubernetes.io/managed-by: kustomize 12 | name: library-viewer-role 13 | rules: 14 | - apiGroups: 15 | - styra.bankdata.dk 16 | resources: 17 | - libraries 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - styra.bankdata.dk 24 | resources: 25 | - libraries/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_mutating_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # CERTIFICATE_NAMESPACE and CERTIFICATE_NAME will be substituted by kustomize 3 | apiVersion: admissionregistration.k8s.io/v1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | labels: 7 | app.kubernetes.io/name: mutatingwebhookconfiguration 8 | app.kubernetes.io/instance: mutating-webhook-configuration 9 | app.kubernetes.io/component: webhook 10 | app.kubernetes.io/created-by: styra-controller 11 | app.kubernetes.io/part-of: styra-controller 12 | app.kubernetes.io/managed-by: kustomize 13 | name: mutating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 16 | -------------------------------------------------------------------------------- /api/test/v1/v1.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Bankdata. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1 contains API Schema definitions for the test v1 API group. The 18 | // types in this package are only used for tests. 19 | package v1 20 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_validating_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # CERTIFICATE_NAMESPACE and CERTIFICATE_NAME will be substituted by kustomize 3 | apiVersion: admissionregistration.k8s.io/v1 4 | kind: ValidatingWebhookConfiguration 5 | metadata: 6 | labels: 7 | app.kubernetes.io/name: validatingwebhookconfiguration 8 | app.kubernetes.io/instance: validating-webhook-configuration 9 | app.kubernetes.io/component: webhook 10 | app.kubernetes.io/created-by: styra-controller 11 | app.kubernetes.io/part-of: styra-controller 12 | app.kubernetes.io/managed-by: kustomize 13 | name: validating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 16 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /api/styra/v1beta1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Bankdata. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // +groupName=styra.bankdata.dk 18 | 19 | // Package v1beta1 contains API Schema definitions for the styra v1beta1 API 20 | // group. 21 | package v1beta1 22 | -------------------------------------------------------------------------------- /api/styra/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Bankdata. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // +groupName=styra.bankdata.dk 18 | 19 | // Package v1alpha1 contains API Schema definitions for the styra v1alpha1 API 20 | // group. 21 | package v1alpha1 22 | -------------------------------------------------------------------------------- /config/rbac/config_projectconfig_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view projectconfigs. 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: projectconfig-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: styra-controller 10 | app.kubernetes.io/part-of: styra-controller 11 | app.kubernetes.io/managed-by: kustomize 12 | name: projectconfig-viewer-role 13 | rules: 14 | - apiGroups: 15 | - config.bankdata.dk 16 | resources: 17 | - projectconfigs 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - config.bankdata.dk 24 | resources: 25 | - projectconfigs/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/test_object_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit objects. 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: object-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: styra-controller 10 | app.kubernetes.io/part-of: styra-controller 11 | app.kubernetes.io/managed-by: kustomize 12 | name: object-editor-role 13 | rules: 14 | - apiGroups: 15 | - test.bankdata.dk 16 | resources: 17 | - objects 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - test.bankdata.dk 28 | resources: 29 | - objects/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/rbac/styra_system_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit systems. 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: system-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: styra-controller 10 | app.kubernetes.io/part-of: styra-controller 11 | app.kubernetes.io/managed-by: kustomize 12 | name: system-editor-role 13 | rules: 14 | - apiGroups: 15 | - styra.bankdata.dk 16 | resources: 17 | - systems 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - styra.bankdata.dk 28 | resources: 29 | - systems/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/rbac/styra_library_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit libraries. 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: library-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: styra-controller 10 | app.kubernetes.io/part-of: styra-controller 11 | app.kubernetes.io/managed-by: kustomize 12 | name: library-editor-role 13 | rules: 14 | - apiGroups: 15 | - styra.bankdata.dk 16 | resources: 17 | - libraries 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - styra.bankdata.dk 28 | resources: 29 | - libraries/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/rbac/config_projectconfig_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit projectconfigs. 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: projectconfig-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: styra-controller 10 | app.kubernetes.io/part-of: styra-controller 11 | app.kubernetes.io/managed-by: kustomize 12 | name: projectconfig-editor-role 13 | rules: 14 | - apiGroups: 15 | - config.bankdata.dk 16 | resources: 17 | - projectconfigs 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - config.bankdata.dk 28 | resources: 29 | - projectconfigs/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/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 | -------------------------------------------------------------------------------- /config/samples/styra_v1alpha1_library.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: styra.bankdata.dk/v1alpha1 2 | kind: Library 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: library 6 | app.kubernetes.io/instance: library-sample 7 | app.kubernetes.io/part-of: styra-controller 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: styra-controller 10 | name: my-library 11 | spec: 12 | name: mylibrary 13 | description: my library 14 | sourceControl: 15 | libraryOrigin: 16 | url: https://github.com/Bankdata/styra-controller.git 17 | reference: refs/heads/master 18 | commit: f37cc9d87251921cbe49349235d9b5305c833769 19 | path: path 20 | datasources: 21 | - path: seconds/datasource 22 | description: this is the second datasource 23 | subjects: 24 | - kind: user 25 | name: user1@mail.dk 26 | - kind: group 27 | name: mygroup 28 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | app.kubernetes.io/name: servicemonitor 9 | app.kubernetes.io/instance: controller-manager-metrics-monitor 10 | app.kubernetes.io/component: metrics 11 | app.kubernetes.io/created-by: styra-controller 12 | app.kubernetes.io/part-of: styra-controller 13 | app.kubernetes.io/managed-by: kustomize 14 | name: controller-manager-metrics-monitor 15 | namespace: system 16 | spec: 17 | endpoints: 18 | - path: /metrics 19 | port: https 20 | scheme: https 21 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 22 | tlsConfig: 23 | insecureSkipVerify: true 24 | selector: 25 | matchLabels: 26 | control-plane: controller-manager 27 | -------------------------------------------------------------------------------- /cmd/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main_test 18 | 19 | import ( 20 | "testing" 21 | 22 | ginkgo "github.com/onsi/ginkgo/v2" 23 | gomega "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestMain(t *testing.T) { 27 | gomega.RegisterFailHandler(ginkgo.Fail) 28 | ginkgo.RunSpecs(t, "cmd") 29 | } 30 | -------------------------------------------------------------------------------- /pkg/ptr/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package ptr_test 18 | 19 | import ( 20 | "testing" 21 | 22 | ginkgo "github.com/onsi/ginkgo/v2" 23 | gomega "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestPtr(t *testing.T) { 27 | gomega.RegisterFailHandler(ginkgo.Fail) 28 | ginkgo.RunSpecs(t, "pkg/ptr") 29 | } 30 | -------------------------------------------------------------------------------- /pkg/styra/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package styra_test 18 | 19 | import ( 20 | "testing" 21 | 22 | ginkgo "github.com/onsi/ginkgo/v2" 23 | gomega "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestStyraClient(t *testing.T) { 27 | gomega.RegisterFailHandler(ginkgo.Fail) 28 | ginkgo.RunSpecs(t, "pkg/styra") 29 | } 30 | -------------------------------------------------------------------------------- /internal/config/config_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "testing" 21 | 22 | ginkgo "github.com/onsi/ginkgo/v2" 23 | gomega "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestConfig(t *testing.T) { 27 | gomega.RegisterFailHandler(ginkgo.Fail) 28 | ginkgo.RunSpecs(t, "internal/config") 29 | } 30 | -------------------------------------------------------------------------------- /api/styra/v1beta1/v1beta1_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | "testing" 21 | 22 | ginkgo "github.com/onsi/ginkgo/v2" 23 | gomega "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestAPIs(t *testing.T) { 27 | gomega.RegisterFailHandler(ginkgo.Fail) 28 | ginkgo.RunSpecs(t, "api/styra/v1beta1") 29 | } 30 | -------------------------------------------------------------------------------- /internal/labels/labels_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package labels_test 18 | 19 | import ( 20 | "testing" 21 | 22 | ginkgo "github.com/onsi/ginkgo/v2" 23 | gomega "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestLabels(t *testing.T) { 27 | gomega.RegisterFailHandler(ginkgo.Fail) 28 | ginkgo.RunSpecs(t, "internal/labels") 29 | } 30 | -------------------------------------------------------------------------------- /internal/predicate/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package predicate_test 18 | 19 | import ( 20 | "testing" 21 | 22 | ginkgo "github.com/onsi/ginkgo/v2" 23 | gomega "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestPtr(t *testing.T) { 27 | gomega.RegisterFailHandler(ginkgo.Fail) 28 | ginkgo.RunSpecs(t, "internal/predicate") 29 | } 30 | -------------------------------------------------------------------------------- /pkg/ocp/opaconfig.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package ocp 18 | 19 | // OPAConfig stores the information going into the ConfigMap for the OPA 20 | type OPAConfig struct { 21 | BundleResource string 22 | BundleService string 23 | ServiceURL string 24 | ServiceName string 25 | UniqueName string 26 | Namespace string 27 | } 28 | -------------------------------------------------------------------------------- /pkg/s3/client.go: -------------------------------------------------------------------------------- 1 | // Package s3 contains a client for interacting with S3 compatible object storage. 2 | package s3 3 | 4 | import ( 5 | "strings" 6 | 7 | configv2alpha2 "github.com/bankdata/styra-controller/api/config/v2alpha2" 8 | ) 9 | 10 | // NewClient creates a new S3Client for MinIO 11 | func NewClient(s3Handler configv2alpha2.S3Handler) (Client, error) { 12 | config := Config{ 13 | AccessKeyID: s3Handler.AccessKeyID, 14 | SecretAccessKey: s3Handler.SecretAccessKey, 15 | Region: s3Handler.Region, 16 | PathStyle: s3Handler.URL != "", // Use path style for custom endpoints 17 | } 18 | 19 | if s3Handler.URL != "" && strings.HasPrefix(s3Handler.URL, "https://") { 20 | config.Endpoint = strings.TrimPrefix(s3Handler.URL, "https://") 21 | config.UseSSL = true 22 | } else { 23 | config.Endpoint = strings.TrimPrefix(s3Handler.URL, "http://") 24 | config.UseSSL = false 25 | } 26 | 27 | return newMinioClient(config) 28 | } 29 | -------------------------------------------------------------------------------- /internal/k8sconv/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package k8sconv_test 18 | 19 | import ( 20 | "testing" 21 | 22 | ginkgo "github.com/onsi/ginkgo/v2" 23 | gomega "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestStyraClient(t *testing.T) { 27 | gomega.RegisterFailHandler(ginkgo.Fail) 28 | ginkgo.RunSpecs(t, "internal/k8sconv") 29 | } 30 | -------------------------------------------------------------------------------- /internal/webhook/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package webhook 18 | 19 | import ( 20 | "testing" 21 | 22 | ginkgo "github.com/onsi/ginkgo/v2" 23 | gomega "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestWebhookClient(t *testing.T) { 27 | 28 | gomega.RegisterFailHandler(ginkgo.Fail) 29 | ginkgo.RunSpecs(t, "internal/webhook") 30 | } 31 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: role 7 | app.kubernetes.io/instance: leader-election-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: styra-controller 10 | app.kubernetes.io/part-of: styra-controller 11 | app.kubernetes.io/managed-by: kustomize 12 | name: leader-election-role 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - configmaps 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - coordination.k8s.io 28 | resources: 29 | - leases 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | - create 35 | - update 36 | - patch 37 | - delete 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - events 42 | verbs: 43 | - create 44 | - patch 45 | -------------------------------------------------------------------------------- /pkg/ptr/ptr.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package ptr contains helpers for creating pointers to built-in types. 18 | package ptr 19 | 20 | // Bool creates a pointer to a bool. 21 | func Bool(b bool) *bool { 22 | return &b 23 | } 24 | 25 | // String creates a pointer to a string. 26 | func String(s string) *string { 27 | return &s 28 | } 29 | 30 | // Int creates a pointer to an int. 31 | func Int(i int) *int { 32 | return &i 33 | } 34 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | - secrets 12 | verbs: 13 | - create 14 | - delete 15 | - get 16 | - list 17 | - patch 18 | - update 19 | - watch 20 | - apiGroups: 21 | - "" 22 | resources: 23 | - events 24 | verbs: 25 | - create 26 | - patch 27 | - apiGroups: 28 | - apps 29 | resources: 30 | - statefulsets 31 | verbs: 32 | - get 33 | - list 34 | - patch 35 | - watch 36 | - apiGroups: 37 | - styra.bankdata.dk 38 | resources: 39 | - libraries 40 | - systems 41 | verbs: 42 | - create 43 | - delete 44 | - get 45 | - list 46 | - patch 47 | - update 48 | - watch 49 | - apiGroups: 50 | - styra.bankdata.dk 51 | resources: 52 | - libraries/finalizers 53 | - systems/finalizers 54 | verbs: 55 | - update 56 | - apiGroups: 57 | - styra.bankdata.dk 58 | resources: 59 | - libraries/status 60 | - systems/status 61 | verbs: 62 | - get 63 | - patch 64 | - update 65 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | run: 16 | timeout: 10m 17 | 18 | linters: 19 | enable: 20 | - misspell 21 | - revive 22 | - goimports 23 | - stylecheck 24 | - lll 25 | 26 | issues: 27 | exclude-use-default: false 28 | exclude: 29 | # The list of ids of default excludes to include or disable. 30 | # https://golangci-lint.run/usage/false-positives/#default-exclusions 31 | - EXC0001 32 | - EXC0011 33 | exclude-rules: 34 | - linters: 35 | - lll 36 | source: "^//\\+kubebuilder" 37 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | /* 5 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | _ "github.com/ahmetb/gen-crd-api-reference-docs" 24 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 25 | _ "github.com/goreleaser/goreleaser" 26 | _ "github.com/onsi/ginkgo/v2/ginkgo" 27 | _ "github.com/vektra/mockery/v2" 28 | _ "sigs.k8s.io/controller-runtime/tools/setup-envtest" 29 | _ "sigs.k8s.io/controller-tools/cmd/controller-gen" 30 | _ "sigs.k8s.io/kind" 31 | _ "sigs.k8s.io/kustomize/kustomize/v5" 32 | ) 33 | -------------------------------------------------------------------------------- /api/test/v1/object_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1 18 | 19 | // +kubebuilder:skip 20 | 21 | import ( 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | ) 24 | 25 | // Object is a very simple kubernetes object which doesn't have a spec or 26 | // status. It only has type and object metadata. This type is useful for 27 | // testing reconciliation predicates. 28 | // +kubebuilder:object:root=true 29 | type Object struct { 30 | metav1.TypeMeta `json:",inline"` 31 | metav1.ObjectMeta `json:"metadata,omitempty"` 32 | } 33 | 34 | func init() { 35 | SchemeBuilder.Register(&Object{}) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/s3/interface.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Client is an interface for clients interacting with S3 compatible object storage. 8 | type Client interface { 9 | UserExists(ctx context.Context, accessKey string) (bool, error) 10 | CreateSystemBundleUser(ctx context.Context, accessKey, bucketName string, uniqueName string) (string, error) 11 | SetNewUserSecretKey(ctx context.Context, accessKey string) (string, error) 12 | } 13 | 14 | // Config defines the configuration for a S3 client. 15 | type Config struct { 16 | Endpoint string 17 | AccessKeyID string 18 | SecretAccessKey string 19 | Region string 20 | UseSSL bool 21 | PathStyle bool // Required for MinIO and some S3-compatible storage 22 | } 23 | 24 | // Credentials represents S3 credentials. 25 | type Credentials struct { 26 | AccessKeyID string `json:"accessKeyID"` 27 | SecretAccessKey string `json:"secretAccessKey"` 28 | Region string `json:"region"` 29 | } 30 | 31 | // Constants for keys in secret generated for OPAs 32 | const ( 33 | AWSSecretNameKeyID = "AWS_ACCESS_KEY_ID" 34 | AWSSecretNameSecretKey = "AWS_SECRET_ACCESS_KEY" 35 | AWSSecretNameRegion = "AWS_REGION" 36 | ) 37 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | # This kustomization.yaml is not intended to be run by itself, 5 | # since it depends on service name and namespace that are out of this kustomize package. 6 | # It should be run by config/default 7 | resources: 8 | - bases/styra.bankdata.dk_systems.yaml 9 | - bases/styra.bankdata.dk_libraries.yaml 10 | #+kubebuilder:scaffold:crdkustomizeresource 11 | 12 | patches: 13 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 14 | # patches here are for enabling the conversion webhook for each CRD 15 | - path: patches/webhook_in_styra_systems.yaml 16 | - path: patches/webhook_in_styra_libraries.yaml 17 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 18 | 19 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 20 | # patches here are for enabling the CA injection for each CRD 21 | - path: patches/cainjection_in_styra_systems.yaml 22 | - path: patches/cainjection_in_styra_libraries.yaml 23 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 24 | 25 | # the following config is for teaching kustomize how to do kustomization for CRDs. 26 | configurations: 27 | - kustomizeconfig.yaml 28 | -------------------------------------------------------------------------------- /internal/template/pkg.tpl: -------------------------------------------------------------------------------- 1 | {{ define "packages" }} 2 | 3 | {{ with .packages}} 4 |

Packages:

5 | 12 | {{ end}} 13 | 14 | {{ range .packages }} 15 |

16 | {{- packageDisplayName . -}} 17 |

18 | 19 | {{ with (index .GoPackages 0 )}} 20 | {{ with .DocComments }} 21 |
22 | {{ safe (renderComments .) }} 23 |
24 | {{ end }} 25 | {{ end }} 26 | 27 | Resource Types: 28 | 37 | 38 | {{ range (visibleTypes (sortedTypes .Types))}} 39 | {{ template "type" . }} 40 | {{ end }} 41 |
42 | {{ end }} 43 | 44 |

45 | Generated with gen-crd-api-reference-docs 46 | {{ with .gitCommit }} on git commit {{ . }}{{end}}. 47 |

48 | 49 | {{ end }} 50 | -------------------------------------------------------------------------------- /pkg/styra/policies.go: -------------------------------------------------------------------------------- 1 | package styra 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | 9 | "github.com/bankdata/styra-controller/pkg/httperror" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // DeletePolicyResponse is the response type for calls to 14 | // the DELETE /v1/policies/{policy} endpoint in the Styra API. 15 | type DeletePolicyResponse struct { 16 | StatusCode int 17 | Body []byte 18 | } 19 | 20 | // DeletePolicy calls the DELETE /v1/policies/{policy} endpoint in the Styra API. 21 | func (c *Client) DeletePolicy(ctx context.Context, policyName string) (*DeletePolicyResponse, error) { 22 | res, err := c.request(ctx, http.MethodDelete, fmt.Sprintf("/v1/policies/%s", policyName), nil, nil) 23 | if err != nil { 24 | return nil, errors.Wrap(err, fmt.Sprintf("could not delete policy: %s", policyName)) 25 | } 26 | 27 | body, err := io.ReadAll(res.Body) 28 | if err != nil { 29 | return nil, errors.Wrap(err, "failed to read response body") 30 | } 31 | 32 | if res.StatusCode != http.StatusNotFound && res.StatusCode != http.StatusOK { 33 | err := httperror.NewHTTPError(res.StatusCode, string(body)) 34 | return nil, err 35 | } 36 | 37 | r := DeletePolicyResponse{ 38 | StatusCode: res.StatusCode, 39 | Body: body, 40 | } 41 | 42 | return &r, nil 43 | } 44 | -------------------------------------------------------------------------------- /api/test/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // +kubebuilder:object:generate=true 18 | // +groupName=test.bankdata.dk 19 | 20 | package v1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "test.bankdata.dk", Version: "v1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /internal/fields/fields.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package fields contains helpers for working with fields in the CRDs. These 18 | // are mostly used when setting up field indexers. 19 | package fields 20 | 21 | import "k8s.io/apimachinery/pkg/fields" 22 | 23 | const ( 24 | // SystemCredentialsSecretName is the path to the credential secret name. 25 | SystemCredentialsSecretName = ".spec.sourceControl.origin.credentialsSecretName" 26 | ) 27 | 28 | // SystemCredentialsSecretNameSelector returns a field selector for finding the 29 | // Systems referencing a secret. 30 | func SystemCredentialsSecretNameSelector(name string) fields.Selector { 31 | return fields.OneTermEqualSelector(SystemCredentialsSecretName, name) 32 | } 33 | -------------------------------------------------------------------------------- /internal/template/members.tpl: -------------------------------------------------------------------------------- 1 | {{ define "members" }} 2 | 3 | {{ range .Members }} 4 | {{ if not (hiddenMember .)}} 5 | 6 | 7 | {{ fieldName . }}
8 | 9 | {{ if linkForType .Type }} 10 | 11 | {{ typeDisplayName .Type }} 12 | 13 | {{ else }} 14 | {{ typeDisplayName .Type }} 15 | {{ end }} 16 | 17 | 18 | 19 | {{ if fieldEmbedded . }} 20 |

21 | (Members of {{ fieldName . }} are embedded into this type.) 22 |

23 | {{ end}} 24 | 25 | {{ if isOptionalMember .}} 26 | (Optional) 27 | {{ end }} 28 | 29 | {{ safe (renderComments .CommentLines) }} 30 | 31 | {{ if and (eq (.Type.Name.Name) "ObjectMeta") }} 32 | Refer to the Kubernetes API documentation for the fields of the 33 | metadata field. 34 | {{ end }} 35 | 36 | {{ if or (eq (fieldName .) "spec") }} 37 |
38 |
39 | 40 | {{ template "members" .Type }} 41 |
42 | {{ end }} 43 | 44 | 45 | {{ end }} 46 | {{ end }} 47 | 48 | {{ end }} 49 | -------------------------------------------------------------------------------- /api/styra/v1beta1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // +kubebuilder:object:generate=true 18 | // +groupName=styra.bankdata.dk 19 | 20 | package v1beta1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "styra.bankdata.dk", Version: "v1beta1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/styra/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // +kubebuilder:object:generate=true 18 | // +groupName=styra.bankdata.dk 19 | 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "styra.bankdata.dk", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # CERTIFICATE_NAMESPACE and CERTIFICATE_NAME will be substituted by kustomize 3 | apiVersion: admissionregistration.k8s.io/v1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | labels: 7 | app.kubernetes.io/name: mutatingwebhookconfiguration 8 | app.kubernetes.io/instance: mutating-webhook-configuration 9 | app.kubernetes.io/component: webhook 10 | app.kubernetes.io/created-by: styra-controller 11 | app.kubernetes.io/part-of: styra-controller 12 | app.kubernetes.io/managed-by: kustomize 13 | name: mutating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 16 | --- 17 | apiVersion: admissionregistration.k8s.io/v1 18 | kind: ValidatingWebhookConfiguration 19 | metadata: 20 | labels: 21 | app.kubernetes.io/name: validatingwebhookconfiguration 22 | app.kubernetes.io/instance: validating-webhook-configuration 23 | app.kubernetes.io/component: webhook 24 | app.kubernetes.io/created-by: styra-controller 25 | app.kubernetes.io/part-of: styra-controller 26 | app.kubernetes.io/managed-by: kustomize 27 | name: validating-webhook-configuration 28 | annotations: 29 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # To get started with Dependabot version updates, you'll need to specify which 16 | # package ecosystems to update and where the package manifests are located. 17 | # Please see the documentation for all configuration options: 18 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 19 | 20 | version: 2 21 | updates: 22 | - package-ecosystem: "gomod" # See documentation for possible values 23 | directory: "/" # Location of package manifests 24 | schedule: 25 | interval: "daily" 26 | commit-message: 27 | prefix: ":arrow_up: " 28 | groups: 29 | k8s-controller-dependencies: 30 | patterns: 31 | - "k8s.io/*" 32 | 33 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: "Code Scanning - Action" 16 | 17 | on: 18 | push: 19 | branches: master 20 | schedule: 21 | - cron: '0 4 * * *' 22 | 23 | jobs: 24 | CodeQL-Build: 25 | runs-on: ubuntu-latest 26 | 27 | permissions: 28 | # required for all workflows 29 | security-events: write 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | 35 | - uses: actions/setup-go@v4 36 | with: 37 | go-version: '>=1.21.3' 38 | 39 | - name: Initialize CodeQL 40 | uses: github/codeql-action/init@v2 41 | 42 | - run: | 43 | make build 44 | 45 | - name: Perform CodeQL Analysis 46 | uses: github/codeql-action/analyze@v2 -------------------------------------------------------------------------------- /api/config/v2alpha2/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v2alpha2 contains API Schema definitions for the config v2alpha2 API group 18 | // +kubebuilder:object:generate=true 19 | // +kubebuilder:skip 20 | // +groupName=config.bankdata.dk 21 | package v2alpha2 22 | 23 | import ( 24 | "k8s.io/apimachinery/pkg/runtime/schema" 25 | "sigs.k8s.io/controller-runtime/pkg/scheme" 26 | ) 27 | 28 | var ( 29 | // GroupVersion is group version used to register these objects 30 | GroupVersion = schema.GroupVersion{Group: "config.bankdata.dk", Version: "v2alpha2"} 31 | 32 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 33 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 34 | 35 | // AddToScheme adds the types in this group-version to the given scheme. 36 | AddToScheme = SchemeBuilder.AddToScheme 37 | ) 38 | -------------------------------------------------------------------------------- /internal/predicate/predicate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package predicate contains predicates used by the controllers. 18 | package predicate 19 | 20 | import ( 21 | "github.com/pkg/errors" 22 | "sigs.k8s.io/controller-runtime/pkg/predicate" 23 | 24 | "github.com/bankdata/styra-controller/internal/labels" 25 | ) 26 | 27 | // ControllerClass creates a predicate which ensures that we only reconcile 28 | // resources that match the controller class label selector 29 | // labels.ControllerClassLabelSelector. 30 | func ControllerClass(class string) (predicate.Predicate, error) { 31 | labelSelector := labels.ControllerClassLabelSelector(class) 32 | predicate, err := predicate.LabelSelectorPredicate(labelSelector) 33 | if err != nil { 34 | return nil, errors.Wrap(err, "could not create LabelSelectorPredicate") 35 | } 36 | return predicate, nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/finalizer/finalizer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package finalizer contains helpers for working with the controller finalizer. 18 | package finalizer 19 | 20 | import ( 21 | "sigs.k8s.io/controller-runtime/pkg/client" 22 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 23 | ) 24 | 25 | const name = "styra.bankdata.dk/finalizer" 26 | 27 | // IsSet returns true if an object has the "styra.bankdata.dk/finalizer" finalizer. 28 | func IsSet(o client.Object) bool { 29 | return controllerutil.ContainsFinalizer(o, name) 30 | } 31 | 32 | // Add adds the "styra.bankdata.dk/finalizer" finalizer to an object. 33 | func Add(o client.Object) { 34 | controllerutil.AddFinalizer(o, name) 35 | } 36 | 37 | // Remove removes the "styra.bankdata.dk/finalizer" finalizer from an object. 38 | func Remove(o client.Object) { 39 | controllerutil.RemoveFinalizer(o, name) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/styra/client_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package styra_test 18 | 19 | import ( 20 | "net/http" 21 | 22 | "github.com/bankdata/styra-controller/pkg/styra" 23 | "github.com/patrickmn/go-cache" 24 | ) 25 | 26 | type roundTripFunc func(req *http.Request) *http.Response 27 | 28 | func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { 29 | return f(req), nil 30 | } 31 | 32 | func newTestClient(f roundTripFunc) styra.ClientInterface { 33 | return &styra.Client{ 34 | URL: "http://test.com", 35 | HTTPClient: http.Client{ 36 | Transport: roundTripFunc(f), 37 | }, 38 | } 39 | } 40 | 41 | func newTestClientWithCache(f roundTripFunc, cache *cache.Cache) styra.ClientInterface { 42 | return &styra.Client{ 43 | URL: "http://test.com", 44 | HTTPClient: http.Client{ 45 | Transport: roundTripFunc(f), 46 | }, 47 | Cache: cache, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /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: bankdata.dk 6 | layout: 7 | - go.kubebuilder.io/v4 8 | multigroup: true 9 | projectName: styra-controller 10 | repo: github.com/bankdata/styra-controller 11 | resources: 12 | - api: 13 | crdVersion: v1 14 | namespaced: true 15 | controller: true 16 | domain: bankdata.dk 17 | group: styra 18 | kind: System 19 | path: github.com/bankdata/styra-controller/api/styra/v1beta1 20 | version: v1beta1 21 | webhooks: 22 | defaulting: true 23 | validation: true 24 | webhookVersion: v1 25 | - api: 26 | crdVersion: v1 27 | namespaced: true 28 | domain: bankdata.dk 29 | group: test 30 | kind: Object 31 | path: github.com/bankdata/styra-controller/api/test/v1 32 | version: v1 33 | - api: 34 | crdVersion: v1 35 | domain: bankdata.dk 36 | group: config 37 | kind: ProjectConfig 38 | path: github.com/bankdata/styra-controller/api/config/v2alpha2 39 | version: v2alpha2 40 | - api: 41 | crdVersion: v1 42 | namespaced: true 43 | controller: true 44 | domain: bankdata.dk 45 | group: styra 46 | kind: Library 47 | path: github.com/bankdata/styra-controller/api/styra/v1alpha1 48 | version: v1alpha1 49 | webhooks: 50 | defaulting: true 51 | validation: true 52 | webhookVersion: v1 53 | version: "3" 54 | -------------------------------------------------------------------------------- /pkg/ptr/ptr_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package ptr_test 18 | 19 | import ( 20 | ginkgo "github.com/onsi/ginkgo/v2" 21 | gomega "github.com/onsi/gomega" 22 | 23 | "github.com/bankdata/styra-controller/pkg/ptr" 24 | ) 25 | 26 | var _ = ginkgo.Describe("Bool", func() { 27 | ginkgo.It("should return a pointer to the boolean", func() { 28 | gomega.Expect(*ptr.Bool(true)).To(gomega.BeTrue()) 29 | gomega.Expect(*ptr.Bool(false)).To(gomega.BeFalse()) 30 | }) 31 | }) 32 | 33 | var _ = ginkgo.Describe("String", func() { 34 | ginkgo.It("should return a pointer to the string", func() { 35 | gomega.Expect(*ptr.String("")).To(gomega.Equal("")) 36 | gomega.Expect(*ptr.String("test")).To(gomega.Equal("test")) 37 | }) 38 | }) 39 | 40 | var _ = ginkgo.Describe("Int", func() { 41 | ginkgo.It("should return a pointer to the int", func() { 42 | gomega.Expect(*ptr.Int(0)).To(gomega.Equal(0)) 43 | gomega.Expect(*ptr.Int(42)).To(gomega.Equal(42)) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /.github/workflows/tags.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Tags 16 | 17 | on: 18 | push: 19 | tags: 20 | - 'v[0-9]+.[0-9]+.[0-9]+' 21 | - 'v[0-9]+.[0-9]+.[0-9]+-rc.*' 22 | 23 | jobs: 24 | release: 25 | name: Release 26 | runs-on: ubuntu-latest 27 | permissions: 28 | contents: write 29 | packages: write 30 | steps: 31 | - uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 0 34 | - run: git fetch --force --tags 35 | - uses: docker/setup-qemu-action@v2 36 | - uses: docker/setup-buildx-action@v2 37 | - uses: docker/login-action@v2 38 | with: 39 | registry: ghcr.io 40 | username: ${{ github.actor }} 41 | password: ${{ secrets.GITHUB_TOKEN }} 42 | - uses: actions/setup-go@v4 43 | with: 44 | go-version: '>=1.21.3' 45 | - name: release 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | run: make release 49 | -------------------------------------------------------------------------------- /config/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: certificate 9 | app.kubernetes.io/instance: serving-cert 10 | app.kubernetes.io/component: certificate 11 | app.kubernetes.io/created-by: styra-controller 12 | app.kubernetes.io/part-of: styra-controller 13 | app.kubernetes.io/managed-by: kustomize 14 | name: selfsigned-issuer 15 | namespace: system 16 | spec: 17 | selfSigned: {} 18 | --- 19 | apiVersion: cert-manager.io/v1 20 | kind: Certificate 21 | metadata: 22 | labels: 23 | app.kubernetes.io/name: certificate 24 | app.kubernetes.io/instance: serving-cert 25 | app.kubernetes.io/component: certificate 26 | app.kubernetes.io/created-by: styra-controller 27 | app.kubernetes.io/part-of: styra-controller 28 | app.kubernetes.io/managed-by: kustomize 29 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 30 | namespace: system 31 | spec: 32 | # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize 33 | dnsNames: 34 | - SERVICE_NAME.SERVICE_NAMESPACE.svc 35 | - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local 36 | issuerRef: 37 | kind: Issuer 38 | name: selfsigned-issuer 39 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 40 | -------------------------------------------------------------------------------- /api/test/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1 22 | 23 | import ( 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | ) 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *Object) DeepCopyInto(out *Object) { 29 | *out = *in 30 | out.TypeMeta = in.TypeMeta 31 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 32 | } 33 | 34 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Object. 35 | func (in *Object) DeepCopy() *Object { 36 | if in == nil { 37 | return nil 38 | } 39 | out := new(Object) 40 | in.DeepCopyInto(out) 41 | return out 42 | } 43 | 44 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 45 | func (in *Object) DeepCopyObject() runtime.Object { 46 | if c := in.DeepCopy(); c != nil { 47 | return c 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | affinity: 12 | nodeAffinity: 13 | requiredDuringSchedulingIgnoredDuringExecution: 14 | nodeSelectorTerms: 15 | - matchExpressions: 16 | - key: kubernetes.io/arch 17 | operator: In 18 | values: 19 | - amd64 20 | - arm64 21 | - ppc64le 22 | - s390x 23 | - key: kubernetes.io/os 24 | operator: In 25 | values: 26 | - linux 27 | containers: 28 | - name: kube-rbac-proxy 29 | securityContext: 30 | allowPrivilegeEscalation: false 31 | capabilities: 32 | drop: 33 | - "ALL" 34 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 35 | args: 36 | - "--secure-listen-address=0.0.0.0:8443" 37 | - "--upstream=http://127.0.0.1:8080/" 38 | - "--logtostderr=true" 39 | - "--v=0" 40 | ports: 41 | - containerPort: 8443 42 | protocol: TCP 43 | name: https 44 | resources: 45 | limits: 46 | cpu: 500m 47 | memory: 128Mi 48 | requests: 49 | cpu: 5m 50 | memory: 64Mi 51 | -------------------------------------------------------------------------------- /scripts/gen-api-docs/gen-api-docs.sh: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | #!/usr/bin/env bash 16 | 17 | gen_styra_v1alpha1 () { 18 | echo "generating docs for api/styra/v1alpha1" 19 | ./bin/gen-crd-api-reference-docs \ 20 | -config "scripts/gen-api-docs/config.json" \ 21 | -api-dir "github.com/bankdata/styra-controller/api/styra/v1alpha1" \ 22 | -template-dir "internal/template" \ 23 | -out-file ./docs/apis/styra/v1alpha1.md 24 | } 25 | 26 | 27 | gen_styra_v1beta1 () { 28 | echo "generating docs for api/styra/v1beta1" 29 | ./bin/gen-crd-api-reference-docs \ 30 | -config "scripts/gen-api-docs/config.json" \ 31 | -api-dir "github.com/bankdata/styra-controller/api/styra/v1beta1" \ 32 | -template-dir "internal/template" \ 33 | -out-file ./docs/apis/styra/v1beta1.md 34 | } 35 | 36 | case $1 in 37 | styra-v1alpha1) 38 | gen_styra_v1alpha1 39 | ;; 40 | styra-v1beta1) 41 | gen_styra_v1beta1 42 | ;; 43 | all) 44 | gen_styra_v1alpha1 45 | gen_styra_v1beta1 46 | ;; 47 | *) 48 | echo "Usage: gen-api-docs.sh styra-v1alpha1|styra-v1beta1|all" 49 | ;; 50 | esac 51 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Pull Request 16 | 17 | on: 18 | pull_request: 19 | branches: 20 | - 'master' 21 | 22 | jobs: 23 | build: 24 | name: Build 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: actions/setup-go@v4 29 | with: 30 | go-version: '>=1.21.3' 31 | - name: build 32 | run: make build 33 | testing: 34 | name: Run tests 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: actions/cache@v3 39 | with: 40 | path: | 41 | ~/.cache/go-build 42 | ~/go/pkg/mod 43 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 44 | restore-keys: | 45 | ${{ runner.os }}-go- 46 | - uses: actions/cache@v3 47 | with: 48 | path: ./bin/ 49 | key: ${{ runner.os }}-binaries-${{ hashFiles('**/go.sum') }} 50 | - uses: actions/setup-go@v4 51 | with: 52 | go-version: '>=1.21.3' 53 | - name: run tests 54 | run: make test 55 | 56 | -------------------------------------------------------------------------------- /pkg/httperror/httperror.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package httperror defines functionality for handling HTTP errors 18 | package httperror 19 | 20 | import ( 21 | "encoding/json" 22 | "fmt" 23 | 24 | "github.com/pkg/errors" 25 | ) 26 | 27 | // HTTPError represents an error that occurred when interacting with the Styra 28 | // API. 29 | type HTTPError struct { 30 | StatusCode int 31 | Body string 32 | } 33 | 34 | // Error implements the error interface. 35 | func (httpError *HTTPError) Error() string { 36 | return fmt.Sprintf("unexpected statuscode: %d, body: %s", httpError.StatusCode, httpError.Body) 37 | } 38 | 39 | // NewHTTPError creates a new HTTPError based on the statuscode and body from a 40 | // failed http call. 41 | func NewHTTPError(statuscode int, body string) error { 42 | httpError := &HTTPError{ 43 | StatusCode: statuscode, 44 | } 45 | 46 | if isValidJSON(body) { 47 | httpError.Body = body 48 | } else { 49 | httpError.Body = "invalid JSON response" 50 | } 51 | 52 | return errors.WithStack(httpError) 53 | } 54 | 55 | func isValidJSON(data string) bool { 56 | var out interface{} 57 | return json.Unmarshal([]byte(data), &out) == nil 58 | } 59 | -------------------------------------------------------------------------------- /config/default/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.bankdata.dk/v2alpha2 2 | kind: ProjectConfig 3 | #controllerClass: 4 | deletionProtectionDefault: false 5 | #disableCRDWebhooks: 6 | readOnly: true 7 | enableMigrations: false 8 | enableDeltaBundlesDefault: false # This does affect the thingy 9 | #gitCredentials: 10 | logLevel: 0 11 | # leaderElection: 12 | # leaseDuration: "60s" 13 | # renewDeadline: "30s" 14 | # retryPeriod: "5s" 15 | notificationWebhooks: {} 16 | # systemDatasourceChanged: google.com 17 | # libraryDatasourceChanged: test.dk 18 | #sentry: 19 | #sso: 20 | styra: 21 | address: https://styra-url.example.com 22 | token: styra-token 23 | # tokenSecretPath: /etc/styra-controller-token/styra_token 24 | 25 | opaControlPlaneConfig: 26 | address: https://ocp-host/ocp 27 | token: ocp-token 28 | gitCredentials: 29 | - id: git-credential-id 30 | repoPrefix: https://github.com 31 | bundleObjectStorage: 32 | s3: 33 | bucket: ocp 34 | region: us-east-1 35 | url: https://minio-host 36 | ocpConfigSecretName: minio # Name of secret in ocp config 37 | defaultRequirements: 38 | - library1 39 | 40 | # used by controller to create users and policies 41 | userCredentialHandler: 42 | s3: 43 | bucket: ocp 44 | url: https://minio-host 45 | region: us-east-1 #default use for minio 46 | accessKeyID: minio-access-key 47 | secretAccessKey: minio-secret-key 48 | 49 | #systemPrefix: 50 | #systemSuffix: 51 | systemUserRoles: 52 | - SystemOwner 53 | - SystemMetadataManager 54 | 55 | #datasourceIgnorePatterns: 56 | # - "^systems/[a-z0-9]+/dontdeleteme/.*" 57 | # - "^libraries/[a-z0-9_]+/deletemenot/.*" 58 | 59 | #decisionsExporter: 60 | 61 | #activityExporter: 62 | 63 | podRestart: 64 | slpRestart: 65 | enabled: true 66 | deploymentType: StatefulSet 67 | 68 | 69 | #opa: 70 | # decision_logs: 71 | # request_context: 72 | # http: 73 | # headers: 74 | # - "Accept" 75 | -------------------------------------------------------------------------------- /pkg/styra/policies_test.go: -------------------------------------------------------------------------------- 1 | package styra_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/bankdata/styra-controller/pkg/httperror" 11 | ginkgo "github.com/onsi/ginkgo/v2" 12 | gomega "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = ginkgo.Describe("DeletePolicy", func() { 16 | 17 | type test struct { 18 | policyName string 19 | responseCode int 20 | responseBody string 21 | expectedBody []byte 22 | expectStyraErr bool 23 | } 24 | 25 | ginkgo.DescribeTable("DeletePolicy", func(test test) { 26 | c := newTestClient(func(r *http.Request) *http.Response { 27 | bs, err := io.ReadAll(r.Body) 28 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 29 | gomega.Expect(bs).To(gomega.Equal([]byte(""))) 30 | gomega.Expect(r.Method).To(gomega.Equal(http.MethodDelete)) 31 | gomega.Expect(r.URL.String()).To(gomega.Equal("http://test.com/v1/policies/" + test.policyName)) 32 | 33 | return &http.Response{ 34 | Header: make(http.Header), 35 | StatusCode: test.responseCode, 36 | Body: io.NopCloser(bytes.NewBufferString(test.responseBody)), 37 | } 38 | }) 39 | 40 | res, err := c.DeletePolicy(context.Background(), test.policyName) 41 | if test.expectStyraErr { 42 | gomega.Expect(res).To(gomega.BeNil()) 43 | target := &httperror.HTTPError{} 44 | gomega.Expect(errors.As(err, &target)).To(gomega.BeTrue()) 45 | } else { 46 | gomega.Expect(err).ToNot(gomega.HaveOccurred()) 47 | gomega.Expect(res.StatusCode).To(gomega.Equal(test.responseCode)) 48 | gomega.Expect(res.Body).To(gomega.Equal(test.expectedBody)) 49 | } 50 | }, 51 | 52 | ginkgo.Entry("something", test{ 53 | policyName: "policyname", 54 | responseCode: http.StatusOK, 55 | responseBody: `expected response from styra api`, 56 | expectedBody: []byte(`expected response from styra api`)}, 57 | ), 58 | 59 | ginkgo.Entry("styra http error", test{ 60 | policyName: "policyname", 61 | responseCode: http.StatusInternalServerError, 62 | expectStyraErr: true, 63 | }), 64 | ) 65 | }) 66 | -------------------------------------------------------------------------------- /.github/workflows/trivy.yml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Trivy scan master 16 | 17 | on: 18 | schedule: 19 | - cron: '0 5 * * *' 20 | 21 | env: 22 | REGISTRY: ghcr.io 23 | 24 | jobs: 25 | docker: 26 | name: Trivy scan 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: actions/cache@v3 31 | with: 32 | path: | 33 | ~/.cache/go-build 34 | ~/go/pkg/mod 35 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 36 | restore-keys: | 37 | ${{ runner.os }}-go- 38 | - uses: actions/cache@v3 39 | with: 40 | path: ./bin/ 41 | key: ${{ runner.os }}-binaries-${{ hashFiles('**/go.sum') }} 42 | - uses: actions/setup-go@v4 43 | with: 44 | go-version: '>=1.21.3' 45 | - name: build 46 | run: make docker-build 47 | - uses: aquasecurity/trivy-action@master 48 | with: 49 | scan-type: 'image' 50 | image-ref: 'controller:latest' 51 | ignore-unfixed: true 52 | severity: 'CRITICAL,HIGH,MEDIUM,LOW' 53 | exit-code: '1' 54 | # format: 'table' 55 | format: 'sarif' 56 | output: 'trivy-results-image.sarif' 57 | - name: Upload Trivy scan image results to GitHub Security tab 58 | uses: github/codeql-action/upload-sarif@v2 59 | with: 60 | sarif_file: 'trivy-results-image.sarif' 61 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | ginkgo "github.com/onsi/ginkgo/v2" 21 | gomega "github.com/onsi/gomega" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "k8s.io/apimachinery/pkg/runtime" 24 | 25 | "github.com/bankdata/styra-controller/api/config/v2alpha2" 26 | ) 27 | 28 | var _ = ginkgo.DescribeTable("deserialize", 29 | func(data []byte, expected *v2alpha2.ProjectConfig, shouldErr bool) { 30 | scheme := runtime.NewScheme() 31 | err := v2alpha2.AddToScheme(scheme) 32 | gomega.Ω(err).ShouldNot(gomega.HaveOccurred()) 33 | actual, err := deserialize(data, scheme) 34 | if shouldErr { 35 | gomega.Ω(err).Should(gomega.HaveOccurred()) 36 | } else { 37 | gomega.Ω(err).ShouldNot(gomega.HaveOccurred()) 38 | } 39 | gomega.Ω(actual).Should(gomega.Equal(expected)) 40 | }, 41 | 42 | ginkgo.Entry("errors on unexpected api group", 43 | []byte(` 44 | apiVersion: myconfig.bankdata.dk/v1 45 | kind: ProjectConfig 46 | styra: 47 | token: my-token 48 | `), 49 | nil, 50 | true, 51 | ), 52 | 53 | ginkgo.Entry("can deserialize v2alpha2", 54 | []byte(` 55 | apiVersion: config.bankdata.dk/v2alpha2 56 | kind: ProjectConfig 57 | styra: 58 | token: my-token 59 | `), 60 | &v2alpha2.ProjectConfig{ 61 | TypeMeta: metav1.TypeMeta{ 62 | Kind: "ProjectConfig", 63 | APIVersion: v2alpha2.GroupVersion.Identifier(), 64 | }, 65 | Styra: v2alpha2.StyraConfig{ 66 | Token: "my-token", 67 | }, 68 | }, 69 | false, 70 | ), 71 | ) 72 | -------------------------------------------------------------------------------- /internal/template/type.tpl: -------------------------------------------------------------------------------- 1 | {{ define "type" }} 2 | 3 |

4 | {{- .Name.Name }} 5 | {{ if eq .Kind "Alias" }}({{.Underlying}} alias){{ end -}} 6 |

7 | {{ with (typeReferences .) }} 8 |

9 | (Appears on: 10 | {{- $prev := "" -}} 11 | {{- range . -}} 12 | {{- if $prev -}}, {{ end -}} 13 | {{- $prev = . -}} 14 | {{ typeDisplayName . }} 15 | {{- end -}} 16 | ) 17 |

18 | {{ end }} 19 | 20 |
21 | {{ safe (renderComments .CommentLines) }} 22 |
23 | 24 | {{ with (constantsOfType .) }} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {{- range . -}} 34 | 35 | {{- /* 36 | renderComments implicitly creates a

element, so we 37 | add one to the display name as well to make the contents 38 | of the two cells align evenly. 39 | */ -}} 40 |

41 | 42 | 43 | {{- end -}} 44 | 45 |
ValueDescription

{{ typeDisplayName . }}

{{ safe (renderComments .CommentLines) }}
46 | {{ end }} 47 | 48 | {{ if .Members }} 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {{ if isExportedType . }} 58 | 59 | 62 | 67 | 68 | 69 | 73 | 74 | 75 | {{ end }} 76 | {{ template "members" .}} 77 | 78 |
FieldDescription
60 | apiVersion
61 | string
63 | 64 | {{apiGroup .}} 65 | 66 |
70 | kind
71 | string 72 |
{{.Name.Name}}
79 | {{ end }} 80 | 81 | {{ end }} 82 | -------------------------------------------------------------------------------- /config/webhook/manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: MutatingWebhookConfiguration 4 | metadata: 5 | name: mutating-webhook-configuration 6 | webhooks: 7 | - admissionReviewVersions: 8 | - v1 9 | clientConfig: 10 | service: 11 | name: webhook-service 12 | namespace: system 13 | path: /mutate-styra-bankdata-dk-v1alpha1-library 14 | failurePolicy: Fail 15 | name: mlibrary-v1alpha1.kb.io 16 | rules: 17 | - apiGroups: 18 | - styra.bankdata.dk 19 | apiVersions: 20 | - v1alpha1 21 | operations: 22 | - CREATE 23 | - UPDATE 24 | resources: 25 | - libraries 26 | sideEffects: None 27 | - admissionReviewVersions: 28 | - v1 29 | clientConfig: 30 | service: 31 | name: webhook-service 32 | namespace: system 33 | path: /mutate-styra-bankdata-dk-v1beta1-system 34 | failurePolicy: Fail 35 | name: msystem-v1beta1.kb.io 36 | rules: 37 | - apiGroups: 38 | - styra.bankdata.dk 39 | apiVersions: 40 | - v1beta1 41 | operations: 42 | - CREATE 43 | - UPDATE 44 | resources: 45 | - systems 46 | sideEffects: None 47 | --- 48 | apiVersion: admissionregistration.k8s.io/v1 49 | kind: ValidatingWebhookConfiguration 50 | metadata: 51 | name: validating-webhook-configuration 52 | webhooks: 53 | - admissionReviewVersions: 54 | - v1 55 | clientConfig: 56 | service: 57 | name: webhook-service 58 | namespace: system 59 | path: /validate-styra-bankdata-dk-v1alpha1-library 60 | failurePolicy: Fail 61 | name: vlibrary-v1alpha1.kb.io 62 | rules: 63 | - apiGroups: 64 | - styra.bankdata.dk 65 | apiVersions: 66 | - v1alpha1 67 | operations: 68 | - CREATE 69 | - UPDATE 70 | resources: 71 | - libraries 72 | sideEffects: None 73 | - admissionReviewVersions: 74 | - v1 75 | clientConfig: 76 | service: 77 | name: webhook-service 78 | namespace: system 79 | path: /validate-styra-bankdata-dk-v1beta1-system 80 | failurePolicy: Fail 81 | name: vsystem-v1beta1.kb.io 82 | rules: 83 | - apiGroups: 84 | - styra.bankdata.dk 85 | apiVersions: 86 | - v1beta1 87 | operations: 88 | - CREATE 89 | - UPDATE 90 | resources: 91 | - systems 92 | sideEffects: None 93 | -------------------------------------------------------------------------------- /pkg/styra/invitations.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package styra 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "io" 23 | "net/http" 24 | "strconv" 25 | 26 | "github.com/bankdata/styra-controller/pkg/httperror" 27 | "github.com/pkg/errors" 28 | ) 29 | 30 | const ( 31 | endpointV1Invitations = "/v1/invitations" 32 | ) 33 | 34 | // CreateInvitationResponse is the response type for calls to the 35 | // POST /v1/invitations endpoint in the Styra API. 36 | type CreateInvitationResponse struct { 37 | StatusCode int 38 | Body []byte 39 | } 40 | 41 | // CreateInvitationRequest is the request body for the 42 | // POST /v1/invitations endpoint in the Styra API. 43 | type CreateInvitationRequest struct { 44 | UserID string `json:"user_id"` 45 | } 46 | 47 | // CreateInvitation calls the POST /v1/invitations endpoint in the Styra API. 48 | func (c *Client) CreateInvitation(ctx context.Context, email bool, name string) (*CreateInvitationResponse, error) { 49 | createInvitationData := CreateInvitationRequest{ 50 | UserID: name, 51 | } 52 | 53 | res, err := c.request( 54 | ctx, 55 | http.MethodPost, 56 | fmt.Sprintf("%s?email=%s", endpointV1Invitations, strconv.FormatBool(email)), 57 | createInvitationData, 58 | nil, 59 | ) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | body, err := io.ReadAll(res.Body) 65 | if err != nil { 66 | return nil, errors.Wrap(err, "could not read body") 67 | } 68 | 69 | if res.StatusCode != http.StatusOK { 70 | err := httperror.NewHTTPError(res.StatusCode, string(body)) 71 | return nil, err 72 | } 73 | 74 | r := CreateInvitationResponse{ 75 | StatusCode: res.StatusCode, 76 | Body: body, 77 | } 78 | 79 | return &r, nil 80 | } 81 | -------------------------------------------------------------------------------- /internal/predicate/predicate_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package predicate_test 18 | 19 | import ( 20 | ginkgo "github.com/onsi/ginkgo/v2" 21 | gomega "github.com/onsi/gomega" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "sigs.k8s.io/controller-runtime/pkg/client" 24 | "sigs.k8s.io/controller-runtime/pkg/event" 25 | 26 | testv1 "github.com/bankdata/styra-controller/api/test/v1" 27 | "github.com/bankdata/styra-controller/internal/predicate" 28 | ) 29 | 30 | var _ = ginkgo.DescribeTable("ControllerClass", 31 | func(class string, obj client.Object, expected bool) { 32 | p, err := predicate.ControllerClass(class) 33 | gomega.Ω(err).NotTo(gomega.HaveOccurred()) 34 | gomega.Ω(p.Create(event.CreateEvent{Object: obj})).To(gomega.Equal(expected)) 35 | }, 36 | 37 | ginkgo.Entry("empty class. no label.", "", &testv1.Object{}, true), 38 | 39 | ginkgo.Entry("empty class. label is set.", "", &testv1.Object{ 40 | ObjectMeta: metav1.ObjectMeta{ 41 | Labels: map[string]string{"styra-controller/class": "test"}, 42 | }, 43 | }, false), 44 | 45 | ginkgo.Entry("empty class. label is empty.", "", &testv1.Object{ 46 | ObjectMeta: metav1.ObjectMeta{ 47 | Labels: map[string]string{"styra-controller/class": ""}, 48 | }, 49 | }, false), 50 | 51 | ginkgo.Entry("class set. no label.", "test", &testv1.Object{}, false), 52 | 53 | ginkgo.Entry("class set. label mismatch", "test", &testv1.Object{ 54 | ObjectMeta: metav1.ObjectMeta{ 55 | Labels: map[string]string{"styra-controller/class": "tset"}, 56 | }, 57 | }, false), 58 | 59 | ginkgo.Entry("class set. label match.", "test", &testv1.Object{ 60 | ObjectMeta: metav1.ObjectMeta{ 61 | Labels: map[string]string{"styra-controller/class": "test"}, 62 | }, 63 | }, true), 64 | ) 65 | -------------------------------------------------------------------------------- /internal/errors/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package errors contains errors. 18 | package errors 19 | 20 | import ( 21 | "github.com/pkg/errors" 22 | 23 | "github.com/bankdata/styra-controller/api/styra/v1beta1" 24 | ) 25 | 26 | type stackTracer interface { 27 | StackTrace() errors.StackTrace 28 | } 29 | 30 | // ReconcilerErr defines an error that occurs during reconciliation. 31 | type ReconcilerErr struct { 32 | err error 33 | Event string 34 | ConditionType string 35 | } 36 | 37 | // New returns a new RenconcilerErr. 38 | func New(msg string) *ReconcilerErr { 39 | return Wrap(errors.New(msg), "") 40 | } 41 | 42 | // Wrap wraps an error as a RenconcilerErr. 43 | func Wrap(err error, msg string) *ReconcilerErr { 44 | return &ReconcilerErr{ 45 | err: errors.Wrap(err, msg), 46 | } 47 | } 48 | 49 | // WithEvent adds event metadata to the ReconcilerErr. 50 | func (err *ReconcilerErr) WithEvent(event v1beta1.EventType) *ReconcilerErr { 51 | err.Event = string(event) 52 | return err 53 | } 54 | 55 | // WithSystemCondition adds condition metadata to the ReconcilerErr. 56 | func (err *ReconcilerErr) WithSystemCondition(contype v1beta1.ConditionType) *ReconcilerErr { 57 | err.ConditionType = string(contype) 58 | return err 59 | } 60 | 61 | // Error implements the error interface. 62 | func (err *ReconcilerErr) Error() string { 63 | return err.err.Error() 64 | } 65 | 66 | // Cause returns the cause of the error. 67 | func (err *ReconcilerErr) Cause() error { 68 | return err.err 69 | } 70 | 71 | // Unwrap is the same as `Cause()` 72 | func (err *ReconcilerErr) Unwrap() error { 73 | return err.Cause() 74 | } 75 | 76 | // StackTrace implements the stackTracer interface. 77 | func (err *ReconcilerErr) StackTrace() errors.StackTrace { 78 | var st stackTracer 79 | if errors.As(err.err, &st) { 80 | return st.StackTrace() 81 | } 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /internal/sentry/sentry.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package sentry contains a reconciler middleware which sends errors to 18 | // Sentry. 19 | package sentry 20 | 21 | import ( 22 | "context" 23 | "errors" 24 | "strings" 25 | 26 | "github.com/bankdata/styra-controller/pkg/httperror" 27 | "github.com/getsentry/sentry-go" 28 | ctrl "sigs.k8s.io/controller-runtime" 29 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 30 | ) 31 | 32 | type sentryReconciler struct { 33 | next reconcile.Reconciler 34 | } 35 | 36 | // Reconcile implements reconciler.Reconcile for the sentry middleware. 37 | func (r *sentryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 38 | result, err := r.next.Reconcile(ctx, req) 39 | 40 | if sentry.CurrentHub().Client() != nil { 41 | if err != nil { 42 | hub := sentry.CurrentHub().Clone() 43 | var styraerror *httperror.HTTPError 44 | if errors.As(err, &styraerror) { 45 | hub.ConfigureScope(func(scope *sentry.Scope) { 46 | scope.SetContext("Styra Client", map[string]interface{}{ 47 | "body": styraerror.Body, 48 | "statuscode": styraerror.StatusCode, 49 | }) 50 | }) 51 | } 52 | 53 | hub.ConfigureScope(func(scope *sentry.Scope) { 54 | scope.SetTags(map[string]string{ 55 | "namespace": req.Namespace, 56 | "name": req.Name, 57 | }) 58 | }) 59 | if !isUserError(err.Error()) { 60 | hub.CaptureException(err) 61 | } 62 | } 63 | } 64 | return result, err 65 | } 66 | 67 | // Decorate applies the sentry middleware to the given reconcile.Reconciler. 68 | func Decorate(r reconcile.Reconciler) reconcile.Reconciler { 69 | return &sentryReconciler{next: r} 70 | } 71 | 72 | func isUserError(msg string) bool { 73 | uniqueGitConfig := "the combination of url, branch, commit-sha and path must be unique across all git repos" 74 | couldNotFindCredentialsSecret := "Could not find credentials Secret" 75 | 76 | return strings.Contains(msg, uniqueGitConfig) || 77 | strings.Contains(msg, couldNotFindCredentialsSecret) 78 | } 79 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | builds: 16 | - main: cmd/main.go 17 | env: 18 | - CGO_ENABLED=0 19 | goos: 20 | - linux 21 | goarch: 22 | - amd64 23 | - arm64 24 | 25 | changelog: 26 | sort: asc 27 | filters: 28 | exclude: [] 29 | 30 | archives: 31 | - format: tar.gz 32 | # this name template makes the OS and Arch compatible with the results of uname. 33 | name_template: >- 34 | {{ .ProjectName }}_ 35 | {{- .Os }}_ 36 | {{- if eq .Arch "amd64" }}x86_64 37 | {{- else }}{{ .Arch }}{{ end }} 38 | {{- if .Arm }}v{{ .Arm }}{{ end }} 39 | format_overrides: 40 | - goos: windows 41 | format: zip 42 | 43 | checksum: 44 | name_template: 'checksums.txt' 45 | 46 | dockers: 47 | - image_templates: 48 | - "ghcr.io/bankdata/styra-controller:{{ .Version }}-amd64" 49 | use: buildx 50 | goarch: amd64 51 | dockerfile: build/package/Dockerfile 52 | build_flag_templates: 53 | - "--platform=linux/amd64" 54 | - image_templates: 55 | - "ghcr.io/bankdata/styra-controller:{{ .Version }}-arm64" 56 | use: buildx 57 | goarch: arm64 58 | dockerfile: build/package/Dockerfile 59 | build_flag_templates: 60 | - "--platform=linux/arm64" 61 | 62 | docker_manifests: 63 | - name_template: "ghcr.io/bankdata/styra-controller:latest" 64 | image_templates: 65 | - "ghcr.io/bankdata/styra-controller:{{ .Version }}-amd64" 66 | - "ghcr.io/bankdata/styra-controller:{{ .Version }}-arm64" 67 | - name_template: "ghcr.io/bankdata/styra-controller:{{ .Major }}" 68 | image_templates: 69 | - "ghcr.io/bankdata/styra-controller:{{ .Version }}-amd64" 70 | - "ghcr.io/bankdata/styra-controller:{{ .Version }}-arm64" 71 | - name_template: "ghcr.io/bankdata/styra-controller:{{ .Major }}.{{ .Minor }}" 72 | image_templates: 73 | - "ghcr.io/bankdata/styra-controller:{{ .Version }}-amd64" 74 | - "ghcr.io/bankdata/styra-controller:{{ .Version }}-arm64" 75 | - name_template: "ghcr.io/bankdata/styra-controller:{{ .Version }}" 76 | image_templates: 77 | - "ghcr.io/bankdata/styra-controller:{{ .Version }}-amd64" 78 | - "ghcr.io/bankdata/styra-controller:{{ .Version }}-arm64" 79 | 80 | release: 81 | github: 82 | owner: Bankdata 83 | name: styra-controller 84 | footer: | 85 | ## Docker Image 86 | - `ghcr.io/bankdata/styra-controller:{{ .Version }}` 87 | -------------------------------------------------------------------------------- /pkg/styra/opaconfig_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package styra_test 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "errors" 23 | "io" 24 | "net/http" 25 | 26 | ginkgo "github.com/onsi/ginkgo/v2" 27 | gomega "github.com/onsi/gomega" 28 | 29 | "github.com/bankdata/styra-controller/pkg/httperror" 30 | "github.com/bankdata/styra-controller/pkg/styra" 31 | ) 32 | 33 | var _ = ginkgo.Describe("GetOPAConfig", func() { 34 | 35 | type test struct { 36 | responseBody string 37 | responseCode int 38 | expectedOPAConf styra.OPAConfig 39 | expectStyraErr bool 40 | } 41 | 42 | ginkgo.DescribeTable("GetOPAConfig", func(test test) { 43 | c := newTestClient(func(r *http.Request) *http.Response { 44 | gomega.Expect(r.URL.String()).To(gomega.Equal("http://test.com/v1/systems/test_id/assets/opa-config")) 45 | gomega.Expect(r.Method).To(gomega.Equal(http.MethodGet)) 46 | return &http.Response{ 47 | Header: make(http.Header), 48 | StatusCode: test.responseCode, 49 | Body: io.NopCloser(bytes.NewBufferString(test.responseBody)), 50 | } 51 | }) 52 | 53 | opaconf, err := c.GetOPAConfig(context.Background(), "test_id") 54 | if test.expectStyraErr { 55 | gomega.Expect(opaconf).To(gomega.Equal(styra.OPAConfig{})) 56 | target := &httperror.HTTPError{} 57 | gomega.Expect(errors.As(err, &target)).To(gomega.BeTrue()) 58 | } else { 59 | gomega.Expect(err).ToNot(gomega.HaveOccurred()) 60 | gomega.Expect(opaconf).To(gomega.Equal(test.expectedOPAConf)) 61 | } 62 | }, 63 | 64 | ginkgo.Entry("success", test{ 65 | responseBody: ` 66 | discovery: 67 | name: discovery-123 68 | prefix: prefix-123 69 | service: service-123 70 | labels: 71 | system-id: system-123 72 | system-type: custom-123 73 | services: 74 | - credentials: 75 | bearer: 76 | token: opa-token-123 77 | url: styra-url-123 78 | - credentials: 79 | bearer: 80 | token: opa-token-1234 81 | url: styra-url-1234`, 82 | expectedOPAConf: styra.OPAConfig{ 83 | HostURL: "styra-url-123", 84 | Token: "opa-token-123", 85 | SystemID: "system-123", 86 | SystemType: "custom-123", 87 | }, 88 | responseCode: http.StatusOK, 89 | }), 90 | ginkgo.Entry("styra http error", test{ 91 | responseCode: http.StatusInternalServerError, 92 | expectStyraErr: true, 93 | }), 94 | ) 95 | }) 96 | -------------------------------------------------------------------------------- /internal/labels/labels_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package labels_test 18 | 19 | import ( 20 | ginkgo "github.com/onsi/ginkgo/v2" 21 | gomega "github.com/onsi/gomega" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "sigs.k8s.io/controller-runtime/pkg/client" 24 | 25 | testv1 "github.com/bankdata/styra-controller/api/test/v1" 26 | "github.com/bankdata/styra-controller/internal/labels" 27 | ) 28 | 29 | var _ = ginkgo.Describe("SetManagedBy", func() { 30 | ginkgo.It("should set the managed-by label", func() { 31 | o := testv1.Object{} 32 | labels.SetManagedBy(&o) 33 | gomega.Ω(o.Labels["app.kubernetes.io/managed-by"]).To(gomega.Equal("styra-controller")) 34 | }) 35 | }) 36 | 37 | var _ = ginkgo.DescribeTable("HasManagedBy", 38 | func(o client.Object, expected bool) { 39 | gomega.Ω(labels.HasManagedBy(o)).To(gomega.Equal(expected)) 40 | }, 41 | ginkgo.Entry( 42 | "should return false if labels is nil", 43 | &testv1.Object{}, 44 | false, 45 | ), 46 | ginkgo.Entry( 47 | "should return false if label is set to something unexpected", 48 | &testv1.Object{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{ 49 | "app.kubernetes.io/managed-by": "something-unexpected", 50 | }}}, 51 | false, 52 | ), 53 | ginkgo.Entry( 54 | "should return true if label is set as expected", 55 | &testv1.Object{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{ 56 | "app.kubernetes.io/managed-by": "styra-controller", 57 | }}}, 58 | true, 59 | ), 60 | ) 61 | 62 | var _ = ginkgo.DescribeTable("ControllerClassMatches", 63 | func(o client.Object, class string, expected bool) { 64 | gomega.Ω(labels.ControllerClassMatches(o, class)).To(gomega.Equal(expected)) 65 | }, 66 | ginkgo.Entry( 67 | "should return false if labels is nil and class is non-empty", 68 | &testv1.Object{}, 69 | "test", 70 | false, 71 | ), 72 | ginkgo.Entry( 73 | "should return true if labels is nil and class empty", 74 | &testv1.Object{}, 75 | "", 76 | true, 77 | ), 78 | ginkgo.Entry( 79 | "should return true if label is missing but class is empty", 80 | &testv1.Object{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}}, 81 | "", 82 | true, 83 | ), 84 | ginkgo.Entry( 85 | "should return true if label value matches", 86 | &testv1.Object{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{ 87 | "styra-controller/class": "test", 88 | }}}, 89 | "test", 90 | true, 91 | ), 92 | ) 93 | -------------------------------------------------------------------------------- /pkg/styra/secrets_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package styra_test 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "encoding/json" 23 | "errors" 24 | "io" 25 | "net/http" 26 | 27 | ginkgo "github.com/onsi/ginkgo/v2" 28 | gomega "github.com/onsi/gomega" 29 | 30 | "github.com/bankdata/styra-controller/pkg/httperror" 31 | "github.com/bankdata/styra-controller/pkg/styra" 32 | ) 33 | 34 | var _ = ginkgo.Describe("CreateUpdateSecret", func() { 35 | type test struct { 36 | secretID string 37 | responseCode int 38 | responseBody string 39 | createUpdateSecretsRequest *styra.CreateUpdateSecretsRequest 40 | expectStyraErr bool 41 | } 42 | 43 | ginkgo.DescribeTable("CreateUpdateSecret", func(test test) { 44 | c := newTestClient(func(r *http.Request) *http.Response { 45 | bs, err := io.ReadAll(r.Body) 46 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 47 | var b bytes.Buffer 48 | gomega.Expect(json.NewEncoder(&b).Encode(test.createUpdateSecretsRequest)).To(gomega.Succeed()) 49 | gomega.Expect(bs).To(gomega.Equal((b.Bytes()))) 50 | gomega.Expect(r.Method).To(gomega.Equal(http.MethodPut)) 51 | gomega.Expect(r.URL.String()).To(gomega.Equal("http://test.com/v1/secrets/" + test.secretID)) 52 | 53 | return &http.Response{ 54 | Header: make(http.Header), 55 | StatusCode: test.responseCode, 56 | Body: io.NopCloser(bytes.NewBufferString(test.responseBody)), 57 | } 58 | }) 59 | 60 | res, err := c.CreateUpdateSecret(context.Background(), test.secretID, test.createUpdateSecretsRequest) 61 | if test.expectStyraErr { 62 | gomega.Expect(res).To(gomega.BeNil()) 63 | target := &httperror.HTTPError{} 64 | gomega.Expect(errors.As(err, &target)).To(gomega.BeTrue()) 65 | } else { 66 | gomega.Expect(err).ToNot(gomega.HaveOccurred()) 67 | gomega.Expect(res.StatusCode).To(gomega.Equal(test.responseCode)) 68 | } 69 | 70 | }, 71 | ginkgo.Entry("something", test{ 72 | secretID: "name", 73 | responseCode: http.StatusOK, 74 | responseBody: `{"test"}`, 75 | createUpdateSecretsRequest: &styra.CreateUpdateSecretsRequest{ 76 | Description: "description", 77 | Name: "name", 78 | Secret: "secret", 79 | }, 80 | }), 81 | 82 | ginkgo.Entry("styra http error", test{ 83 | responseCode: http.StatusInternalServerError, 84 | expectStyraErr: true, 85 | }), 86 | ) 87 | }) 88 | -------------------------------------------------------------------------------- /internal/webhook/mocks/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | logr "github.com/go-logr/logr" 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // Client is an autogenerated mock type for the Client type 13 | type Client struct { 14 | mock.Mock 15 | } 16 | 17 | // LibraryDatasourceChanged provides a mock function with given fields: _a0, _a1, _a2 18 | func (_m *Client) LibraryDatasourceChanged(_a0 context.Context, _a1 logr.Logger, _a2 string) error { 19 | ret := _m.Called(_a0, _a1, _a2) 20 | 21 | if len(ret) == 0 { 22 | panic("no return value specified for LibraryDatasourceChanged") 23 | } 24 | 25 | var r0 error 26 | if rf, ok := ret.Get(0).(func(context.Context, logr.Logger, string) error); ok { 27 | r0 = rf(_a0, _a1, _a2) 28 | } else { 29 | r0 = ret.Error(0) 30 | } 31 | 32 | return r0 33 | } 34 | 35 | // LibraryDatasourceChangedOCP provides a mock function with given fields: _a0, _a1, _a2 36 | func (_m *Client) LibraryDatasourceChangedOCP(_a0 context.Context, _a1 logr.Logger, _a2 string) error { 37 | ret := _m.Called(_a0, _a1, _a2) 38 | 39 | if len(ret) == 0 { 40 | panic("no return value specified for LibraryDatasourceChangedOCP") 41 | } 42 | 43 | var r0 error 44 | if rf, ok := ret.Get(0).(func(context.Context, logr.Logger, string) error); ok { 45 | r0 = rf(_a0, _a1, _a2) 46 | } else { 47 | r0 = ret.Error(0) 48 | } 49 | 50 | return r0 51 | } 52 | 53 | // SystemDatasourceChanged provides a mock function with given fields: _a0, _a1, _a2, _a3 54 | func (_m *Client) SystemDatasourceChanged(_a0 context.Context, _a1 logr.Logger, _a2 string, _a3 string) error { 55 | ret := _m.Called(_a0, _a1, _a2, _a3) 56 | 57 | if len(ret) == 0 { 58 | panic("no return value specified for SystemDatasourceChanged") 59 | } 60 | 61 | var r0 error 62 | if rf, ok := ret.Get(0).(func(context.Context, logr.Logger, string, string) error); ok { 63 | r0 = rf(_a0, _a1, _a2, _a3) 64 | } else { 65 | r0 = ret.Error(0) 66 | } 67 | 68 | return r0 69 | } 70 | 71 | // SystemDatasourceChangedOCP provides a mock function with given fields: _a0, _a1, _a2 72 | func (_m *Client) SystemDatasourceChangedOCP(_a0 context.Context, _a1 logr.Logger, _a2 string) error { 73 | ret := _m.Called(_a0, _a1, _a2) 74 | 75 | if len(ret) == 0 { 76 | panic("no return value specified for SystemDatasourceChangedOCP") 77 | } 78 | 79 | var r0 error 80 | if rf, ok := ret.Get(0).(func(context.Context, logr.Logger, string) error); ok { 81 | r0 = rf(_a0, _a1, _a2) 82 | } else { 83 | r0 = ret.Error(0) 84 | } 85 | 86 | return r0 87 | } 88 | 89 | // NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 90 | // The first argument is typically a *testing.T value. 91 | func NewClient(t interface { 92 | mock.TestingT 93 | Cleanup(func()) 94 | }) *Client { 95 | mock := &Client{} 96 | mock.Mock.Test(t) 97 | 98 | t.Cleanup(func() { mock.AssertExpectations(t) }) 99 | 100 | return mock 101 | } 102 | -------------------------------------------------------------------------------- /pkg/styra/invitations_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package styra_test 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "encoding/json" 23 | "errors" 24 | "io" 25 | "net/http" 26 | "strconv" 27 | 28 | ginkgo "github.com/onsi/ginkgo/v2" 29 | gomega "github.com/onsi/gomega" 30 | 31 | "github.com/bankdata/styra-controller/pkg/httperror" 32 | "github.com/bankdata/styra-controller/pkg/styra" 33 | ) 34 | 35 | var _ = ginkgo.Describe("CreateInvitation", func() { 36 | 37 | type test struct { 38 | email bool 39 | name string 40 | responseCode int 41 | responseBody string 42 | createInvitationRequest *styra.CreateInvitationRequest 43 | expectStyraErr bool 44 | } 45 | 46 | ginkgo.DescribeTable("CreateInvitation", func(test test) { 47 | c := newTestClient(func(r *http.Request) *http.Response { 48 | bs, err := io.ReadAll(r.Body) 49 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 50 | var b bytes.Buffer 51 | gomega.Expect(json.NewEncoder(&b).Encode(test.createInvitationRequest)).To(gomega.Succeed()) 52 | gomega.Expect(bs).To(gomega.Equal(b.Bytes())) 53 | gomega.Expect(r.Method).To(gomega.Equal(http.MethodPost)) 54 | gomega.Expect(r.URL.String()).To(gomega.Equal("http://test.com/v1/invitations?email=" + 55 | strconv.FormatBool(test.email))) 56 | 57 | return &http.Response{ 58 | Header: make(http.Header), 59 | StatusCode: test.responseCode, 60 | Body: io.NopCloser(bytes.NewBufferString(test.responseBody)), 61 | } 62 | }) 63 | 64 | res, err := c.CreateInvitation(context.Background(), test.email, test.name) 65 | if test.expectStyraErr { 66 | gomega.Expect(res).To(gomega.BeNil()) 67 | target := &httperror.HTTPError{} 68 | gomega.Expect(errors.As(err, &target)).To(gomega.BeTrue()) 69 | } else { 70 | gomega.Expect(err).ToNot(gomega.HaveOccurred()) 71 | gomega.Expect(res.StatusCode).To(gomega.Equal(test.responseCode)) 72 | } 73 | }, 74 | 75 | ginkgo.Entry("something", test{ 76 | name: "name", 77 | responseCode: http.StatusOK, 78 | responseBody: `{ 79 | "request_id": "id", 80 | "result": { 81 | "url": "url" 82 | } 83 | }`, 84 | createInvitationRequest: &styra.CreateInvitationRequest{ 85 | UserID: "name", 86 | }, 87 | }), 88 | 89 | ginkgo.Entry("styra http error", test{ 90 | name: "name", 91 | createInvitationRequest: &styra.CreateInvitationRequest{ 92 | UserID: "name", 93 | }, 94 | responseCode: http.StatusInternalServerError, 95 | expectStyraErr: true, 96 | }), 97 | ) 98 | }) 99 | -------------------------------------------------------------------------------- /internal/labels/labels.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package labels contains helpers for working with labels. 18 | package labels 19 | 20 | import ( 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/apimachinery/pkg/labels" 23 | "sigs.k8s.io/controller-runtime/pkg/client" 24 | ) 25 | 26 | // Constants for labels configured on System resources. 27 | const ( 28 | labelControllerClass = "styra-controller/class" 29 | labelManagedBy = "app.kubernetes.io/managed-by" 30 | labelValueManagedBy = "styra-controller" 31 | LabelControlPlane = "styra-controller/control-plane" 32 | LabelValueControlPlaneStyra = "styra" 33 | LabelValueControlPlaneOCP = "opa-control-plane" 34 | ) 35 | 36 | // ControllerClassLabelSelector creates a metav1.LabelSelector which selects 37 | // objects that has the "styra-controller/class" label with the value `class`. 38 | func ControllerClassLabelSelector(class string) metav1.LabelSelector { 39 | var selector metav1.LabelSelector 40 | if class != "" { 41 | selector = metav1.LabelSelector{ 42 | MatchLabels: map[string]string{ 43 | labelControllerClass: class, 44 | }, 45 | } 46 | } else { 47 | selector = metav1.LabelSelector{ 48 | MatchExpressions: []metav1.LabelSelectorRequirement{{ 49 | Key: labelControllerClass, 50 | Operator: metav1.LabelSelectorOpDoesNotExist, 51 | }}, 52 | } 53 | } 54 | return selector 55 | } 56 | 57 | // ControllerClassLabelSelectorAsSelector creates a labels.Selecter which 58 | // selects objects that has the "styra-controller/class" label with the value `class`. 59 | func ControllerClassLabelSelectorAsSelector(class string) (labels.Selector, error) { 60 | ls := ControllerClassLabelSelector(class) 61 | return metav1.LabelSelectorAsSelector(&ls) 62 | } 63 | 64 | // SetManagedBy sets the `app.kubernetes.io/managed-by` label to 65 | // styra-controller. 66 | func SetManagedBy(o client.Object) { 67 | labels := o.GetLabels() 68 | if labels == nil { 69 | labels = map[string]string{} 70 | } 71 | labels[labelManagedBy] = labelValueManagedBy 72 | o.SetLabels(labels) 73 | } 74 | 75 | // HasManagedBy checks if the object has the label `app.kubernetes.io/managed-by` 76 | // set to styra-controller 77 | func HasManagedBy(o client.Object) bool { 78 | managedBy, ok := o.GetLabels()[labelManagedBy] 79 | return ok && managedBy == labelValueManagedBy 80 | } 81 | 82 | // ControllerClassMatches checks if the object has the `styra-controller/class` label 83 | // with the value `class`. 84 | func ControllerClassMatches(o client.Object, class string) bool { 85 | labels := o.GetLabels() 86 | if labels == nil { 87 | return class == "" 88 | } 89 | return labels[labelControllerClass] == class 90 | } 91 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # CustomResourceDefinition Design 2 | 3 | This document describes the design of the custom resource definitions that the 4 | ocp-controller manages. 5 | 6 | The custom resources managed by the ocp-controller are: 7 | 8 | * `System` 9 | * `Library` 10 | 11 | ## System 12 | 13 | The `System` custom resource definition (CRD) declaratively defines a desired 14 | bundle in OPA Control Plane (Before a System in Styra DAS). It provides options for configuring the name of the bundle, requirements/datasources, decision mappings(deprecated, only used in Styra DAS), git settings, and access control as a list of users and/or SSO claims (deprecated, only used in Styra DAS). 15 | 16 | ```yaml 17 | apiVersion: styra.bankdata.dk/v1beta1 18 | kind: System 19 | metadata: 20 | name: example-system 21 | labels: 22 | app: example-system 23 | spec: 24 | datasources: 25 | - path: datasources/example 26 | decisionMappings: 27 | - allowed: 28 | expected: 29 | boolean: true 30 | path: result.allowed 31 | columns: 32 | - key: extra 33 | path: input.extra 34 | name: path/to/example/rule 35 | reason: 36 | path: result.reasons 37 | deletionProtection: true 38 | enableDeltaBundles: true 39 | localPlane: 40 | name: styra-local-plane-example 41 | sourceControl: 42 | origin: 43 | commit: commitSHA 44 | path: path/to/policy/in/git/repo 45 | url: 'git-repo-url' 46 | subjects: 47 | - name: user@user.com 48 | - kind: group 49 | name: my-group 50 | ``` 51 | 52 | The git credentials which OPA Control Plane will need for fetching policy are specified 53 | by referencing to a credential ID in the controller config `opaControlPlane.gitCredentials.id` and `opaControlPlane.gitCredentials.repoPrefix`. 54 | [controller configuration documentation](configuration.md#default-git-credentials). 55 | 56 | ## Library 57 | 58 | The `Library` custom resource definition (CRD) declaratively defines a desired library in OPA Control Plane (Before Styra DAS). It provides options for configuring the name of the library, a description of it, permissions, git settings, and datasources. Note, a library is just a source in OPA Control Plane. 59 | 60 | ```yaml 61 | apiVersion: styra.bankdata.dk/v1alpha1 62 | kind: Library 63 | metadata: 64 | name: my-library 65 | spec: 66 | name: mylibrary 67 | description: my library 68 | sourceControl: 69 | libraryOrigin: 70 | url: https://github.com/Bankdata/styra-controller.git 71 | reference: refs/heads/master 72 | commit: f37cc9d87251921cbe49349235d9b5305c833769 73 | path: rego/path 74 | datasources: 75 | - path: seconds/datasource 76 | description: this is the second datasource 77 | subjects: 78 | - kind: user 79 | name: user1@mail.dk 80 | - kind: group 81 | name: mygroup 82 | ``` 83 | 84 | The content of the library is what is found in the folder `/libraries/`. 85 | There is therefore a tight coupling between the library name and the path to the library in the git repository. The library name is also used as the name of the library in OPA Control Plane. 86 | With the above example, the content of the library would be the files found at 87 | `https://github.com/Bankdata/styra-controller/tree/master/rego/path/libraries/mylibrary` together with the datasource. -------------------------------------------------------------------------------- /pkg/ocp/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package ocp provides functionality for interacting with the OCP API. 18 | package ocp 19 | 20 | import ( 21 | "bytes" 22 | "context" 23 | "encoding/json" 24 | "fmt" 25 | "net/http" 26 | "time" 27 | 28 | "github.com/patrickmn/go-cache" 29 | "github.com/pkg/errors" 30 | ) 31 | 32 | // ClientInterface defines the interface for the OCP client. 33 | type ClientInterface interface { 34 | GetSource(ctx context.Context, id string) (*GetSourceResponse, error) 35 | PutSource(ctx context.Context, id string, request *PutSourceRequest) (*PutSourceResponse, error) 36 | DeleteSource(ctx context.Context, id string) error 37 | PutBundle(ctx context.Context, bundle *PutBundleRequest) error 38 | DeleteBundle(ctx context.Context, name string) error 39 | } 40 | 41 | // Client is a client for the OCP APIs. 42 | type Client struct { 43 | HTTPClient http.Client 44 | URL string 45 | token string 46 | Cache *cache.Cache 47 | } 48 | 49 | // New creates a new OCP ClientInterface. 50 | func New(url string, token string) ClientInterface { 51 | c := cache.New(6*time.Hour, 10*time.Minute) 52 | 53 | return &Client{ 54 | URL: url, 55 | HTTPClient: http.Client{}, 56 | token: token, 57 | Cache: c, 58 | } 59 | } 60 | 61 | // InvalidateCache invalidates the entire cache 62 | func (c *Client) InvalidateCache() { 63 | c.Cache.Flush() 64 | } 65 | 66 | func (c *Client) newRequest( 67 | ctx context.Context, 68 | method string, 69 | endpoint string, 70 | body interface{}, 71 | headers map[string]string, 72 | ) (*http.Request, error) { 73 | u := fmt.Sprintf("%s%s", c.URL, endpoint) 74 | 75 | var b bytes.Buffer 76 | if body != nil { 77 | if err := json.NewEncoder(&b).Encode(body); err != nil { 78 | return nil, errors.Wrap(err, "could not encode body") 79 | } 80 | } 81 | 82 | r, err := http.NewRequestWithContext(ctx, method, u, &b) 83 | if err != nil { 84 | return nil, errors.Wrap(err, "could not create request") 85 | } 86 | 87 | r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) 88 | r.Header.Set("Content-Type", "application/json") 89 | 90 | for k, v := range headers { 91 | r.Header.Set(k, v) 92 | } 93 | 94 | return r, nil 95 | } 96 | 97 | func (c *Client) request( 98 | ctx context.Context, 99 | method string, 100 | endpoint string, 101 | body interface{}, 102 | headers map[string]string, 103 | ) (*http.Response, error) { 104 | req, err := c.newRequest(ctx, method, endpoint, body, headers) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | res, err := c.HTTPClient.Do(req) 110 | if err != nil { 111 | return nil, errors.Wrap(err, "could not send request") 112 | } 113 | 114 | return res, nil 115 | } 116 | -------------------------------------------------------------------------------- /pkg/styra/workspace.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package styra 18 | 19 | import ( 20 | "context" 21 | "io" 22 | "net/http" 23 | 24 | "github.com/bankdata/styra-controller/pkg/httperror" 25 | "github.com/pkg/errors" 26 | ) 27 | 28 | const ( 29 | endpointV1Workspace = "/v1/workspace" 30 | ) 31 | 32 | // UpdateWorkspaceRequest is the request type for calls to the PUT /v1/workspace endpoint 33 | // in the Styra API. 34 | type UpdateWorkspaceRequest struct { 35 | DecisionsExporter *ExporterConfig `json:"decisions_exporter,omitempty"` 36 | ActivityExporter *ExporterConfig `json:"activity_exporter,omitempty"` 37 | } 38 | 39 | // UpdateWorkspaceResponse is the response type for calls to the PUT /v1/workspace endpoint 40 | // in the Styra API. 41 | type UpdateWorkspaceResponse struct { 42 | StatusCode int 43 | Body []byte 44 | } 45 | 46 | // ExporterConfig is the configuration for the decision and activity exporter in the Styra API. 47 | type ExporterConfig struct { 48 | Interval string `json:"interval,omitempty"` 49 | Kafka *KafkaConfig `json:"kafka,omitempty"` 50 | } 51 | 52 | // KafkaConfig is the configuration for the Kafka exporter in the Styra API. 53 | type KafkaConfig struct { 54 | Authentication string `json:"authentication"` 55 | Brokers []string `json:"brokers"` 56 | RequredAcks string `json:"required_acks"` 57 | Topic string `json:"topic"` 58 | TLS *KafkaTLS `json:"tls"` 59 | } 60 | 61 | // KafkaTLS is the TLS configuration for the Kafka exporter in the Styra API. 62 | type KafkaTLS struct { 63 | ClientCert string `json:"client_cert"` 64 | RootCA string `json:"rootca"` 65 | InsecureSkipVerify bool `json:"insecure_skip_verify"` 66 | } 67 | 68 | // UpdateWorkspace calls the PATCH /v1/workspace endpoint in the Styra API. 69 | func (c *Client) UpdateWorkspace( 70 | ctx context.Context, 71 | request *UpdateWorkspaceRequest, 72 | ) (*UpdateWorkspaceResponse, error) { 73 | return c.UpdateWorkspaceRaw(ctx, request) 74 | } 75 | 76 | // UpdateWorkspaceRaw calls the PATCH /v1/workspace endpoint in the Styra API. 77 | func (c *Client) UpdateWorkspaceRaw( 78 | ctx context.Context, 79 | request interface{}, 80 | ) (*UpdateWorkspaceResponse, error) { 81 | res, err := c.request(ctx, http.MethodPatch, endpointV1Workspace, request, nil) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | body, err := io.ReadAll(res.Body) 87 | if err != nil { 88 | return nil, errors.Wrap(err, "could not read body") 89 | } 90 | 91 | if res.StatusCode != http.StatusOK { 92 | err := httperror.NewHTTPError(res.StatusCode, string(body)) 93 | return nil, err 94 | } 95 | 96 | r := UpdateWorkspaceResponse{ 97 | StatusCode: res.StatusCode, 98 | Body: body, 99 | } 100 | 101 | return &r, nil 102 | } 103 | -------------------------------------------------------------------------------- /internal/webhook/styra/v1alpha1/library_webhook_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | ginkgo "github.com/onsi/ginkgo/v2" 21 | gomega "github.com/onsi/gomega" 22 | 23 | styrav1alpha1 "github.com/bankdata/styra-controller/api/styra/v1alpha1" 24 | // TODO (user): Add any additional imports if needed 25 | ) 26 | 27 | var _ = ginkgo.Describe("Library Webhook", func() { 28 | var ( 29 | obj *styrav1alpha1.Library 30 | oldObj *styrav1alpha1.Library 31 | validator LibraryCustomValidator 32 | defaulter LibraryCustomDefaulter 33 | ) 34 | 35 | ginkgo.BeforeEach(func() { 36 | obj = &styrav1alpha1.Library{} 37 | oldObj = &styrav1alpha1.Library{} 38 | validator = LibraryCustomValidator{} 39 | gomega.Expect(validator).NotTo(gomega.BeNil(), "Expected validator to be initialized") 40 | defaulter = LibraryCustomDefaulter{} 41 | gomega.Expect(defaulter).NotTo(gomega.BeNil(), "Expected defaulter to be initialized") 42 | gomega.Expect(oldObj).NotTo(gomega.BeNil(), "Expected oldObj to be initialized") 43 | gomega.Expect(obj).NotTo(gomega.BeNil(), "Expected obj to be initialized") 44 | // TODO (user): Add any setup logic common to all tests 45 | }) 46 | 47 | ginkgo.AfterEach(func() { 48 | // TODO (user): Add any teardown logic common to all tests 49 | }) 50 | 51 | ginkgo.Context("When creating Library under Defaulting Webhook", func() { 52 | // TODO (user): Add logic for defaulting webhooks 53 | // Example: 54 | // It("Should apply defaults when a required field is empty", func() { 55 | // By("simulating a scenario where defaults should be applied") 56 | // obj.SomeFieldWithDefault = "" 57 | // By("calling the Default method to apply defaults") 58 | // defaulter.Default(ctx, obj) 59 | // By("checking that the default values are set") 60 | // Expect(obj.SomeFieldWithDefault).To(Equal("default_value")) 61 | // }) 62 | }) 63 | 64 | ginkgo.Context("When creating or updating Library under Validating Webhook", func() { 65 | // TODO (user): Add logic for validating webhooks 66 | // Example: 67 | // It("Should deny creation if a required field is missing", func() { 68 | // By("simulating an invalid creation scenario") 69 | // obj.SomeRequiredField = "" 70 | // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) 71 | // }) 72 | // 73 | // It("Should admit creation if all required fields are present", func() { 74 | // By("simulating an invalid creation scenario") 75 | // obj.SomeRequiredField = "valid_value" 76 | // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) 77 | // }) 78 | // 79 | // It("Should validate updates correctly", func() { 80 | // By("simulating a valid update scenario") 81 | // oldObj.SomeRequiredField = "updated_value" 82 | // obj.SomeRequiredField = "updated_value" 83 | // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) 84 | // }) 85 | }) 86 | 87 | }) 88 | -------------------------------------------------------------------------------- /pkg/s3/mocks/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // Client is an autogenerated mock type for the Client type 12 | type Client struct { 13 | mock.Mock 14 | } 15 | 16 | // CreateSystemBundleUser provides a mock function with given fields: ctx, accessKey, bucketName, uniqueName 17 | func (_m *Client) CreateSystemBundleUser(ctx context.Context, accessKey string, bucketName string, uniqueName string) (string, error) { 18 | ret := _m.Called(ctx, accessKey, bucketName, uniqueName) 19 | 20 | if len(ret) == 0 { 21 | panic("no return value specified for CreateSystemBundleUser") 22 | } 23 | 24 | var r0 string 25 | var r1 error 26 | if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (string, error)); ok { 27 | return rf(ctx, accessKey, bucketName, uniqueName) 28 | } 29 | if rf, ok := ret.Get(0).(func(context.Context, string, string, string) string); ok { 30 | r0 = rf(ctx, accessKey, bucketName, uniqueName) 31 | } else { 32 | r0 = ret.Get(0).(string) 33 | } 34 | 35 | if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok { 36 | r1 = rf(ctx, accessKey, bucketName, uniqueName) 37 | } else { 38 | r1 = ret.Error(1) 39 | } 40 | 41 | return r0, r1 42 | } 43 | 44 | // SetNewUserSecretKey provides a mock function with given fields: ctx, accessKey 45 | func (_m *Client) SetNewUserSecretKey(ctx context.Context, accessKey string) (string, error) { 46 | ret := _m.Called(ctx, accessKey) 47 | 48 | if len(ret) == 0 { 49 | panic("no return value specified for SetNewUserSecretKey") 50 | } 51 | 52 | var r0 string 53 | var r1 error 54 | if rf, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok { 55 | return rf(ctx, accessKey) 56 | } 57 | if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { 58 | r0 = rf(ctx, accessKey) 59 | } else { 60 | r0 = ret.Get(0).(string) 61 | } 62 | 63 | if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { 64 | r1 = rf(ctx, accessKey) 65 | } else { 66 | r1 = ret.Error(1) 67 | } 68 | 69 | return r0, r1 70 | } 71 | 72 | // UserExists provides a mock function with given fields: ctx, accessKey 73 | func (_m *Client) UserExists(ctx context.Context, accessKey string) (bool, error) { 74 | ret := _m.Called(ctx, accessKey) 75 | 76 | if len(ret) == 0 { 77 | panic("no return value specified for UserExists") 78 | } 79 | 80 | var r0 bool 81 | var r1 error 82 | if rf, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok { 83 | return rf(ctx, accessKey) 84 | } 85 | if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { 86 | r0 = rf(ctx, accessKey) 87 | } else { 88 | r0 = ret.Get(0).(bool) 89 | } 90 | 91 | if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { 92 | r1 = rf(ctx, accessKey) 93 | } else { 94 | r1 = ret.Error(1) 95 | } 96 | 97 | return r0, r1 98 | } 99 | 100 | // NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 101 | // The first argument is typically a *testing.T value. 102 | func NewClient(t interface { 103 | mock.TestingT 104 | Cleanup(func()) 105 | }) *Client { 106 | mock := &Client{} 107 | mock.Mock.Test(t) 108 | 109 | t.Cleanup(func() { mock.AssertExpectations(t) }) 110 | 111 | return mock 112 | } 113 | -------------------------------------------------------------------------------- /pkg/styra/opaconfig.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package styra 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "io" 23 | "net/http" 24 | 25 | "github.com/bankdata/styra-controller/pkg/httperror" 26 | "github.com/pkg/errors" 27 | "gopkg.in/yaml.v2" 28 | ) 29 | 30 | // OPAConfig stores the information retrieved from calling the GET 31 | // /v1/systems/{systemId}/assets/opa-config endpoint in the Styra API. 32 | type OPAConfig struct { 33 | HostURL string 34 | Token string 35 | SystemID string 36 | SystemType string 37 | } 38 | 39 | type getOPAConfigResponse struct { 40 | Discovery getOPAConfigDiscovery `yaml:"discovery"` 41 | Labels getOPAConfigLabels `yaml:"labels"` 42 | Services []getOPAConfigService `yaml:"services"` 43 | } 44 | 45 | type getOPAConfigDiscovery struct { 46 | Name string `yaml:"name"` 47 | Prefix string `yaml:"prefix"` 48 | Service string `yaml:"service"` 49 | } 50 | 51 | type getOPAConfigLabels struct { 52 | SystemID string `yaml:"system-id"` 53 | SystemType string `yaml:"system-type"` 54 | } 55 | 56 | type getOPAConfigService struct { 57 | Credentials getOPAConfigServiceCredentials `yaml:"credentials"` 58 | URL string `yaml:"url"` 59 | } 60 | 61 | type getOPAConfigServiceCredentials struct { 62 | Bearer getOPAConfigServiceBearerCredentials `yaml:"bearer"` 63 | } 64 | 65 | type getOPAConfigServiceBearerCredentials struct { 66 | Token string `yaml:"token"` 67 | } 68 | 69 | // GetOPAConfig calls the GET /v1/systems/{systemId}/assets/opa-config endpoint 70 | // in the Styra API. 71 | func (c *Client) GetOPAConfig(ctx context.Context, systemID string) (OPAConfig, error) { 72 | res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/v1/systems/%s/assets/opa-config", systemID), nil, nil) 73 | if err != nil { 74 | return OPAConfig{}, errors.Wrap(err, "could not get opaconf file") 75 | } 76 | 77 | if res.StatusCode != http.StatusOK { 78 | body, err := io.ReadAll(res.Body) 79 | if err != nil { 80 | return OPAConfig{}, errors.Wrap(err, "could not read body") 81 | } 82 | 83 | err = httperror.NewHTTPError(res.StatusCode, string(body)) 84 | return OPAConfig{}, err 85 | } 86 | 87 | var getOPAConfigResponse getOPAConfigResponse 88 | if err := yaml.NewDecoder(res.Body).Decode(&getOPAConfigResponse); err != nil { 89 | return OPAConfig{}, errors.Wrap(err, "could not decode opa-config asset response") 90 | } 91 | 92 | if getOPAConfigResponse.Services == nil { 93 | return OPAConfig{}, errors.Errorf("No services in opa config") 94 | } 95 | 96 | opaConfig := OPAConfig{ 97 | HostURL: getOPAConfigResponse.Services[0].URL, 98 | Token: getOPAConfigResponse.Services[0].Credentials.Bearer.Token, 99 | SystemID: getOPAConfigResponse.Labels.SystemID, 100 | SystemType: getOPAConfigResponse.Labels.SystemType, 101 | } 102 | 103 | return opaConfig, nil 104 | } 105 | -------------------------------------------------------------------------------- /pkg/styra/users.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package styra 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | "fmt" 23 | "io" 24 | "net/http" 25 | 26 | "github.com/bankdata/styra-controller/pkg/httperror" 27 | "github.com/pkg/errors" 28 | ) 29 | 30 | const ( 31 | endpointV1Users = "/v1/users" 32 | ) 33 | 34 | // GetUserResponse is the response type for calls to the GET /v1/users/{userId} endpoint 35 | // in the Styra API. 36 | type GetUserResponse struct { 37 | StatusCode int 38 | Body []byte 39 | } 40 | 41 | // GetUsersResponse is the response type for calls to the GET /v1/users endpoint 42 | // in the Styra API. 43 | type GetUsersResponse struct { 44 | Users []User 45 | } 46 | 47 | // Struct to unmarshal the JSON response from the GET /v1/users endpoint 48 | type getUsersJSONResponse struct { 49 | Result []User `json:"result"` 50 | } 51 | 52 | // User is the struct for a user in the Styra API. 53 | type User struct { 54 | Enabled bool `json:"enabled"` 55 | ID string `json:"id"` 56 | } 57 | 58 | // GetUsers calls the GET /v1/users endpoint in the Styra API. 59 | func (c *Client) GetUsers(ctx context.Context) (*GetUsersResponse, bool, error) { 60 | const cacheKey = "allUsersResponse" 61 | 62 | // Check if the response is in the cache 63 | if cachedResponse, found := c.Cache.Get(cacheKey); found { 64 | return cachedResponse.(*GetUsersResponse), true, nil 65 | } 66 | 67 | res, err := c.GetUserEndpoint(ctx, endpointV1Users) 68 | if err != nil { 69 | return nil, false, err 70 | } 71 | 72 | var js getUsersJSONResponse 73 | if err := json.Unmarshal(res.Body, &js); err != nil { 74 | return nil, false, errors.Wrap(err, "could not unmarshal body: ") 75 | } 76 | 77 | r := GetUsersResponse{ 78 | Users: js.Result, 79 | } 80 | 81 | // Cache the response 82 | c.Cache.Set(cacheKey, &r, 0) 83 | 84 | return &r, false, nil 85 | } 86 | 87 | // GetUser calls the GET /v1/users/{userId} endpoint in the Styra API. 88 | func (c *Client) GetUser(ctx context.Context, name string) (*GetUserResponse, error) { 89 | return c.GetUserEndpoint(ctx, fmt.Sprintf("%s/%s", endpointV1Users, name)) 90 | } 91 | 92 | // GetUserEndpoint is a helper function to call the Styra API. 93 | func (c *Client) GetUserEndpoint(ctx context.Context, endpoint string) (*GetUserResponse, error) { 94 | res, err := c.request(ctx, http.MethodGet, endpoint, nil, nil) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | body, err := io.ReadAll(res.Body) 100 | if err != nil { 101 | return nil, errors.Wrap(err, "could not read body") 102 | } 103 | 104 | if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNotFound { 105 | err := httperror.NewHTTPError(res.StatusCode, string(body)) 106 | return nil, err 107 | } 108 | 109 | r := GetUserResponse{ 110 | StatusCode: res.StatusCode, 111 | Body: body, 112 | } 113 | 114 | return &r, nil 115 | } 116 | -------------------------------------------------------------------------------- /pkg/styra/secrets.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package styra 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "io" 23 | "net/http" 24 | 25 | "github.com/bankdata/styra-controller/pkg/httperror" 26 | "github.com/pkg/errors" 27 | ) 28 | 29 | const ( 30 | endpointV1Secrets = "/v1/secrets" 31 | ) 32 | 33 | // DeleteSecretResponse is the response type for calls to the 34 | // DELETE /v1/secrets/{secretId} endpoint in the Styra API. 35 | type DeleteSecretResponse struct { 36 | StatusCode int 37 | Body []byte 38 | } 39 | 40 | // CreateUpdateSecretResponse is the response type for calls to the 41 | // PUT /v1/secrets/{secretId} endpoint in the Styra API. 42 | type CreateUpdateSecretResponse struct { 43 | StatusCode int 44 | Body []byte 45 | } 46 | 47 | // CreateUpdateSecretsRequest is the response body for the 48 | // PUT /v1/secrets/{secretId} endpoint in the Styra API. 49 | type CreateUpdateSecretsRequest struct { 50 | Description string `json:"description"` 51 | Name string `json:"name"` 52 | Secret string `json:"secret"` 53 | } 54 | 55 | // CreateUpdateSecret calls the PUT /v1/secrets/{secretId} endpoint in the 56 | // Styra API. 57 | func (c *Client) CreateUpdateSecret( 58 | ctx context.Context, 59 | secretID string, 60 | createUpdateSecretsRequest *CreateUpdateSecretsRequest, 61 | ) (*CreateUpdateSecretResponse, error) { 62 | res, err := c.request( 63 | ctx, 64 | http.MethodPut, 65 | fmt.Sprintf("%s/%s", endpointV1Secrets, secretID), 66 | createUpdateSecretsRequest, 67 | nil, 68 | ) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | body, err := io.ReadAll(res.Body) 74 | if err != nil { 75 | return nil, errors.Wrap(err, "could not read body") 76 | } 77 | 78 | if res.StatusCode != http.StatusOK { 79 | err := httperror.NewHTTPError(res.StatusCode, string(body)) 80 | return nil, err 81 | } 82 | 83 | r := CreateUpdateSecretResponse{ 84 | StatusCode: res.StatusCode, 85 | Body: body, 86 | } 87 | 88 | return &r, nil 89 | } 90 | 91 | // DeleteSecret calls the DELETE /v1/secrets/{secretId} endpoint in the 92 | // Styra API. 93 | func (c *Client) DeleteSecret( 94 | ctx context.Context, 95 | secretID string, 96 | ) (*DeleteSecretResponse, error) { 97 | res, err := c.request( 98 | ctx, 99 | http.MethodDelete, 100 | fmt.Sprintf("%s/%s", endpointV1Secrets, secretID), 101 | nil, 102 | nil, 103 | ) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | body, err := io.ReadAll(res.Body) 109 | if err != nil { 110 | return nil, errors.Wrap(err, "could not read body") 111 | } 112 | 113 | if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNotFound { 114 | err := httperror.NewHTTPError(res.StatusCode, string(body)) 115 | return nil, err 116 | } 117 | 118 | r := DeleteSecretResponse{ 119 | StatusCode: res.StatusCode, 120 | Body: body, 121 | } 122 | 123 | return &r, nil 124 | } 125 | -------------------------------------------------------------------------------- /config/samples/config_v2alpha2_projectconfig.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.bankdata.dk/v2alpha2 2 | kind: ProjectConfig 3 | 4 | # controllerClass sets a controller class for this controller. This allows the 5 | # provided CRDs to target a specific controller. This is useful when running 6 | # multiple controllers in the same cluster. 7 | controllerClass: "" 8 | 9 | # deletionProtectionDefault sets the default to use with regards to deletion 10 | # protection if it is not set on the resource. 11 | deletionProtectionDefault: false 12 | 13 | # disableCRDWebhooks disables the CRD webhooks on the controller. If running 14 | # multiple controllers in the same cluster, only one will need to have it's 15 | # webhooks enabled. 16 | disableCRDWebhooks: false 17 | 18 | # enableMigrations enables the system migration annotation. This should be kept 19 | # disabled unless migrations need to be done. 20 | enableMigrations: false 21 | 22 | # enableDeltaBundlesDefault sets the default to use with regards to delta 23 | enableDeltaBundlesDefault: false 24 | 25 | # gitCredentials holds a list of git credential configurations. The repoPrefix 26 | # of the git credential will be matched angainst repository URL in order to 27 | # determine which credential to use. The git credential with the longest 28 | # matching repoPrefix will be selected. 29 | gitCredentials: [] 30 | # - user: my-git-user 31 | # password: my-git-password 32 | # repoPrefix: https://github.com/my-org 33 | 34 | # leaderElection contains configuration for the controller-runtime leader 35 | # election. 36 | #leaderElection: 37 | # leaseDuration: 15s 38 | # renewDeadLine: 10s 39 | # retryPeriod: 2s 40 | 41 | # logLevel sets the logging level of the controller. A higher number gives more 42 | # verbosity. A number higher than 0 should only be used for debugging purposes. 43 | logLevel: 0 44 | 45 | # notificationWebhook contains configuration for how to call the notification 46 | # webhook. 47 | #notificationWebhook: 48 | # address: "" 49 | 50 | # sentry contains configuration for how errors should be reported to sentry. 51 | #sentry: 52 | # debug: false 53 | # dsn: "" 54 | # environment: "" 55 | # httpsProxy: "" 56 | 57 | # sso contains configuration for how to use SSO tokens for determining what 58 | # groups a user belongs to. This can be used to grant members of a certain 59 | # group access to systems. 60 | #sso: 61 | # identityProvider: "" 62 | # jwtGroupsClaim: "" 63 | 64 | # styra contains configuration for connecting to the Styra DAS apis 65 | styra: 66 | address: "" 67 | token: "" 68 | 69 | # systemPrefix is a prefix for all the systems that the controller creates 70 | # in Styra DAS. This is useful in order to be able to identify what 71 | # controller created a system in a shared Styra DAS instance. 72 | systemPrefix: "" 73 | 74 | # systemSuffix is a suffix for all the systems that the controller creates 75 | # in Styra DAS. This is useful in order to be able to identify what 76 | # controller created a system in a shared Styra DAS instance. 77 | systemSuffix: "" 78 | 79 | # systemUserRoles is a list of Styra DAS system level roles which the subjects of 80 | # a system will be granted. 81 | systemUserRoles: [] 82 | # - SystemViewer 83 | # - SystemInstall 84 | 85 | readOnly: true 86 | 87 | # opa contains default configuration for the opa configmap holding the opa config generated by the styra-controller 88 | # decision_logs: https://www.openpolicyagent.org/docs/latest/configuration/#decision-logs 89 | # request_context.http.headers: list of strings that will be added to the decision_logs 90 | #opa: 91 | # decision_logs: 92 | # request_context: 93 | # http: 94 | # headers: 95 | # - "Accept" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/bankdata/styra-controller)](https://goreportcard.com/report/github.com/bankdata/styra-controller) 2 | [![Go Reference](https://pkg.go.dev/badge/github.com/bankdata/styra-controller.svg)](https://pkg.go.dev/github.com/bankdata/styra-controller) 3 | [![Release](https://img.shields.io/github/release/bankdata/styra-controller.svg?style=flat-square)](https://github.com/bankdata/styra-controller/releases/latest) 4 | [![Gitmoji](https://img.shields.io/badge/gitmoji-%20😜%20😍-FFDD67.svg?style=flat-square)](https://gitmoji.dev) 5 | 6 | # ocp-controller 7 | 8 | ocp-controller is a Kubernetes controller first designed to automate configuration of Styra DAS, later rewritten to configure [OPA Control Plane](https://github.com/open-policy-agent/opa-control-plane). 9 | With the use of 10 | [CustomResourceDefinitions](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/), 11 | ocp-controller enables sources and bundles to be configured, without 12 | a manual process. By doing this we can guarantee that no changes are done to OPA Control Plane manually, which makes change management and compliance easier. 13 | 14 | In order to ease configuration of OPA [OPA](https://github.com/open-policy-agent/opa), the controller automatically creates ConfigMaps and Secrets which contain the configuration and connection details for these components. The controller creates credentials for each unique system/bundle in s3. 15 | 16 | ## Architectural overview 17 | 18 | ocp-controller sits in a Kubernetes cluster and ensures that sources and 19 | bundles are created in OPA Control Plane. It then creates ConfigMaps and Secrets 20 | with relevant configuration and connection details. 21 | 22 | 23 | 24 | diagram over the controller architecture 25 | 26 | 27 | ## CustomResourceDefinitions 28 | 29 | A core feature of the ocp-controller is to monitor the Kubernetes API 30 | server for changes to specific objects and ensure that the current OPA Control Plane 31 | resources match these objects. The controller acts on the following custom 32 | resource definitions (CRDs). 33 | 34 | - `System`, which defines a OPA Control Plane source configuration and its bundle. 35 | - `Library`, which defines a Library resource in OPA Control Plane. 36 | 37 | For more information about these resources, see the 38 | [design document](docs/design.md) or the full [api reference](docs/apis). 39 | 40 | ## Installation 41 | 42 | For a guide on how to install ocp-controller, see 43 | [the installation instructions](docs/installation.md). 44 | 45 | ## Limitations 46 | 47 | The ocp-controller is in late 2025 refactored to accommodate the needs we had in Bankdata, while migrating from Styra DAS to OPA Control Plane. This means that the feature set currently has some limitations. The following is a few of the most important ones. 48 | 49 | - Only supports OCP ObjectStorage: AmazonS3 (at first only MinIO is supported) 50 | - Stacks are currently unsupported 51 | 52 | These limitations merely reflect the current state, and we might change them and add new features when the need for them arises. If you want to help removing any of these limitations, feel free to open an issue or submit a pull request. 53 | 54 | ## Contributing 55 | 56 | For a guide on how to contribute to the ocp-controller project as well as how 57 | to deploy the ocp-controller for testing purposes see 58 | [CONTRIBUTING.md](CONTRIBUTING.md). 59 | 60 | ## Security 61 | 62 | For more information about the security policy of the project see [SECURITY.md](SECURITY.md) 63 | -------------------------------------------------------------------------------- /pkg/styra/workspace_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package styra_test 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "encoding/json" 23 | "errors" 24 | "io" 25 | "net/http" 26 | 27 | ginkgo "github.com/onsi/ginkgo/v2" 28 | gomega "github.com/onsi/gomega" 29 | 30 | "github.com/bankdata/styra-controller/pkg/httperror" 31 | "github.com/bankdata/styra-controller/pkg/styra" 32 | ) 33 | 34 | var _ = ginkgo.Describe("UpdateWorkspace", func() { 35 | 36 | type test struct { 37 | request *styra.UpdateWorkspaceRequest 38 | responseCode int 39 | responseBody string 40 | expectStyraErr bool 41 | } 42 | 43 | ginkgo.DescribeTable("UpdateWorkspace", func(test test) { 44 | c := newTestClient(func(r *http.Request) *http.Response { 45 | bs, err := io.ReadAll(r.Body) 46 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 47 | 48 | var b bytes.Buffer 49 | gomega.Expect(json.NewEncoder(&b).Encode(test.request)).To(gomega.Succeed()) 50 | gomega.Expect(bs).To(gomega.Equal(b.Bytes())) 51 | 52 | gomega.Expect(r.Method).To(gomega.Equal(http.MethodPatch)) 53 | gomega.Expect(r.URL.String()).To(gomega.Equal("http://test.com/v1/workspace")) 54 | 55 | return &http.Response{ 56 | Header: make(http.Header), 57 | StatusCode: test.responseCode, 58 | Body: io.NopCloser(bytes.NewBufferString(test.responseBody)), 59 | } 60 | }) 61 | 62 | res, err := c.UpdateWorkspace(context.Background(), test.request) 63 | if test.expectStyraErr { 64 | gomega.Expect(res).To(gomega.BeNil()) 65 | target := &httperror.HTTPError{} 66 | gomega.Expect(errors.As(err, &target)).To(gomega.BeTrue()) 67 | } else { 68 | gomega.Expect(err).ToNot(gomega.HaveOccurred()) 69 | gomega.Expect(res.StatusCode).To(gomega.Equal(test.responseCode)) 70 | } 71 | }, 72 | 73 | ginkgo.Entry("update workspace DecisionsExporter", test{ 74 | request: &styra.UpdateWorkspaceRequest{ 75 | DecisionsExporter: &styra.ExporterConfig{ 76 | Interval: "1m", 77 | Kafka: &styra.KafkaConfig{ 78 | Authentication: "auth", 79 | Brokers: []string{"broker"}, 80 | RequredAcks: "acks", 81 | Topic: "topic", 82 | TLS: &styra.KafkaTLS{ 83 | ClientCert: "clientcert", 84 | RootCA: "rootca", 85 | }, 86 | }, 87 | }, 88 | }, 89 | responseCode: http.StatusOK, 90 | responseBody: `{ 91 | "request_id": "id" 92 | }`, 93 | }), 94 | 95 | ginkgo.Entry("update workspace ActivityExporter", test{ 96 | request: &styra.UpdateWorkspaceRequest{ 97 | ActivityExporter: &styra.ExporterConfig{ 98 | Interval: "1m", 99 | Kafka: &styra.KafkaConfig{ 100 | Authentication: "auth", 101 | Brokers: []string{"broker"}, 102 | RequredAcks: "acks", 103 | Topic: "topic", 104 | TLS: &styra.KafkaTLS{ 105 | ClientCert: "clientcert", 106 | RootCA: "rootca", 107 | InsecureSkipVerify: true, 108 | }, 109 | }, 110 | }, 111 | }, 112 | responseCode: http.StatusOK, 113 | responseBody: `{ 114 | "request_id": "id" 115 | }`, 116 | }), 117 | ginkgo.Entry("styra http error", test{ 118 | responseCode: http.StatusInternalServerError, 119 | expectStyraErr: true, 120 | }), 121 | ) 122 | }) 123 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: namespace 7 | app.kubernetes.io/instance: system 8 | app.kubernetes.io/component: manager 9 | app.kubernetes.io/created-by: styra-controller 10 | app.kubernetes.io/part-of: styra-controller 11 | app.kubernetes.io/managed-by: kustomize 12 | name: system 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: controller-manager 18 | namespace: system 19 | labels: 20 | control-plane: controller-manager 21 | app.kubernetes.io/name: deployment 22 | app.kubernetes.io/instance: controller-manager 23 | app.kubernetes.io/component: manager 24 | app.kubernetes.io/created-by: styra-controller 25 | app.kubernetes.io/part-of: styra-controller 26 | app.kubernetes.io/managed-by: kustomize 27 | spec: 28 | selector: 29 | matchLabels: 30 | control-plane: controller-manager 31 | replicas: 1 32 | template: 33 | metadata: 34 | annotations: 35 | kubectl.kubernetes.io/default-container: manager 36 | labels: 37 | control-plane: controller-manager 38 | spec: 39 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 40 | # according to the platforms which are supported by your solution. 41 | # It is considered best practice to support multiple architectures. You can 42 | # build your manager image using the makefile target docker-buildx. 43 | # affinity: 44 | # nodeAffinity: 45 | # requiredDuringSchedulingIgnoredDuringExecution: 46 | # nodeSelectorTerms: 47 | # - matchExpressions: 48 | # - key: kubernetes.io/arch 49 | # operator: In 50 | # values: 51 | # - amd64 52 | # - arm64 53 | # - ppc64le 54 | # - s390x 55 | # - key: kubernetes.io/os 56 | # operator: In 57 | # values: 58 | # - linux 59 | securityContext: 60 | runAsNonRoot: true 61 | # TODO(user): For common cases that do not require escalating privileges 62 | # it is recommended to ensure that all your Pods/Containers are restrictive. 63 | # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 64 | # Please uncomment the following code if your project does NOT have to work on old Kubernetes 65 | # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). 66 | # seccompProfile: 67 | # type: RuntimeDefault 68 | containers: 69 | - command: 70 | - /styra-controller 71 | image: controller:latest 72 | imagePullPolicy: IfNotPresent 73 | name: manager 74 | securityContext: 75 | allowPrivilegeEscalation: false 76 | capabilities: 77 | drop: 78 | - "ALL" 79 | livenessProbe: 80 | httpGet: 81 | path: /healthz 82 | port: 8081 83 | initialDelaySeconds: 15 84 | periodSeconds: 20 85 | readinessProbe: 86 | httpGet: 87 | path: /readyz 88 | port: 8081 89 | initialDelaySeconds: 5 90 | periodSeconds: 10 91 | # TODO(user): Configure the resources accordingly based on the project requirements. 92 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 93 | resources: 94 | limits: 95 | cpu: 500m 96 | memory: 128Mi 97 | requests: 98 | cpu: 10m 99 | memory: 64Mi 100 | serviceAccountName: controller-manager 101 | terminationGracePeriodSeconds: 10 102 | -------------------------------------------------------------------------------- /internal/controller/styra/system_controller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package styra 18 | 19 | import ( 20 | ginkgo "github.com/onsi/ginkgo/v2" 21 | gomega "github.com/onsi/gomega" 22 | 23 | configv2alpha2 "github.com/bankdata/styra-controller/api/config/v2alpha2" 24 | styrav1beta1 "github.com/bankdata/styra-controller/api/styra/v1beta1" 25 | "github.com/bankdata/styra-controller/pkg/styra" 26 | ) 27 | 28 | var _ = ginkgo.DescribeTable("createRolebindingSubjects", 29 | func(subjects []styrav1beta1.Subject, expectedSubject []*styra.Subject) { 30 | gomega.Ω(createRolebindingSubjects(subjects, &configv2alpha2.SSOConfig{ 31 | IdentityProvider: "BDAD", 32 | JWTGroupsClaim: "groups", 33 | })).To(gomega.Equal(expectedSubject)) 34 | }, 35 | 36 | ginkgo.Entry("returns same adgroup", 37 | []styrav1beta1.Subject{{Kind: "group", Name: "ADTEST"}}, 38 | []*styra.Subject{{ 39 | Kind: "claim", 40 | ClaimConfig: &styra.ClaimConfig{ 41 | IdentityProvider: "BDAD", 42 | Key: "groups", 43 | Value: "ADTEST", 44 | }, 45 | }}, 46 | ), 47 | 48 | ginkgo.Entry("defaults empty kind value to user", 49 | []styrav1beta1.Subject{ 50 | {Kind: "user", Name: "test1@test.dk"}, 51 | {Name: "test2@test.dk"}, 52 | {Kind: "group", Name: "ADTEST"}, 53 | }, 54 | []*styra.Subject{ 55 | { 56 | Kind: "user", 57 | ID: "test1@test.dk", 58 | }, { 59 | Kind: "user", 60 | ID: "test2@test.dk", 61 | }, { 62 | Kind: "claim", 63 | ClaimConfig: &styra.ClaimConfig{ 64 | IdentityProvider: "BDAD", 65 | Key: "groups", 66 | Value: "ADTEST", 67 | }, 68 | }, 69 | }, 70 | ), 71 | 72 | ginkgo.Entry("does not return duplicates adgroups", 73 | []styrav1beta1.Subject{ 74 | {Kind: "group", Name: "ADTEST"}, 75 | {Kind: "group", Name: "ADTEST1"}, 76 | {Kind: "group", Name: "ADTEST1"}, 77 | }, 78 | []*styra.Subject{ 79 | { 80 | Kind: "claim", 81 | ClaimConfig: &styra.ClaimConfig{ 82 | IdentityProvider: "BDAD", 83 | Key: "groups", 84 | Value: "ADTEST", 85 | }, 86 | }, { 87 | Kind: "claim", 88 | ClaimConfig: &styra.ClaimConfig{ 89 | IdentityProvider: "BDAD", 90 | Key: "groups", 91 | Value: "ADTEST1", 92 | }, 93 | }, 94 | }, 95 | ), 96 | 97 | ginkgo.Entry("does not return duplicates users and groups", 98 | []styrav1beta1.Subject{ 99 | {Kind: "user", Name: "test@test.dk"}, 100 | {Kind: "user", Name: "test@test.dk"}, 101 | {Kind: "group", Name: "ADTEST"}, 102 | {Kind: "group", Name: "ADTEST"}, 103 | }, 104 | []*styra.Subject{ 105 | { 106 | Kind: "user", 107 | ID: "test@test.dk", 108 | }, { 109 | Kind: "claim", 110 | ClaimConfig: &styra.ClaimConfig{ 111 | IdentityProvider: "BDAD", 112 | Key: "groups", 113 | Value: "ADTEST", 114 | }, 115 | }, 116 | }, 117 | ), 118 | ) 119 | 120 | // test the isURLValid method 121 | var _ = ginkgo.DescribeTable("isURLValid", 122 | func(url string, expected bool) { 123 | gomega.Ω(isURLValid(url)).To(gomega.Equal(expected)) 124 | }, 125 | ginkgo.Entry("valid url", "", true), 126 | ginkgo.Entry("valid url", "https://www.github.com/test/repo.git", true), 127 | ginkgo.Entry("valid url", "https://www.github.com/test/repo", true), 128 | ginkgo.Entry("invalid url", "https://www.github.com/[test]/repo", false), 129 | ginkgo.Entry("invalid url", "https://www.github.com/[test]/repo.git", false), 130 | ginkgo.Entry("invalid url", "www.google.com", false), 131 | ginkgo.Entry("invalid url", "google.com", false), 132 | ginkgo.Entry("invalid url", "google", false), 133 | ) 134 | -------------------------------------------------------------------------------- /pkg/s3/minio.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "fmt" 8 | "strings" 9 | 10 | madmin "github.com/minio/madmin-go/v3" 11 | miniocred "github.com/minio/minio-go/v7/pkg/credentials" 12 | ) 13 | 14 | type minioClient struct { 15 | config Config 16 | adminClient *madmin.AdminClient 17 | } 18 | 19 | func newMinioClient(cfg Config) (Client, error) { 20 | // Create MinIO admin client 21 | adminClient, err := madmin.NewWithOptions(cfg.Endpoint, &madmin.Options{ 22 | Creds: miniocred.NewStaticV4(cfg.AccessKeyID, cfg.SecretAccessKey, ""), 23 | Secure: cfg.UseSSL, 24 | }) 25 | if err != nil { 26 | return nil, fmt.Errorf("failed to create MinIO admin client: %w", err) 27 | } 28 | 29 | return &minioClient{ 30 | adminClient: adminClient, 31 | config: cfg, 32 | }, nil 33 | } 34 | 35 | // UserExists checks if a user exists in MinIO 36 | func (c *minioClient) UserExists(ctx context.Context, accessKey string) (bool, error) { 37 | _, err := c.adminClient.GetUserInfo(ctx, accessKey) 38 | if err != nil { 39 | if strings.Contains(err.Error(), "The specified user does not exist") { 40 | return false, nil 41 | } 42 | return false, fmt.Errorf("failed to get user info for %s: %w", accessKey, err) 43 | } 44 | 45 | return true, nil 46 | } 47 | 48 | // CreateSystemBundleUser creates a user in MinIO with read-only access to a specific bucketPath 49 | func (c *minioClient) CreateSystemBundleUser( 50 | ctx context.Context, 51 | accessKey string, 52 | bucketName string, 53 | uniqueName string) (string, error) { 54 | secretKey, err := generateBase64Secret(16) 55 | if err != nil { 56 | return "", fmt.Errorf("failed to generate secret key: %w", err) 57 | } 58 | 59 | err = c.adminClient.AddUser(ctx, accessKey, secretKey) 60 | if err != nil { 61 | return "", fmt.Errorf("failed to create user %s: %w", accessKey, err) 62 | } 63 | 64 | // Create a read-only policy for the specific bucket 65 | policyName := fmt.Sprintf("readonly-%s-%s", bucketName, uniqueName) 66 | policyDocument := fmt.Sprintf(`{ 67 | "Version": "2012-10-17", 68 | "Statement": [ 69 | { 70 | "Effect": "Allow", 71 | "Action": [ 72 | "s3:GetObject" 73 | ], 74 | "Resource": [ 75 | "arn:aws:s3:::%s/bundles/%s/*" 76 | ] 77 | }, 78 | { 79 | "Effect": "Allow", 80 | "Action": [ 81 | "s3:ListBucket" 82 | ], 83 | "Resource": [ 84 | "arn:aws:s3:::%s" 85 | ], 86 | "Condition": { 87 | "StringLike": { 88 | "s3:prefix": [ 89 | "bundles/%s/*" 90 | ] 91 | } 92 | } 93 | } 94 | ] 95 | }`, bucketName, uniqueName, bucketName, uniqueName) 96 | 97 | err = c.adminClient.AddCannedPolicy(ctx, policyName, []byte(policyDocument)) 98 | if err != nil { 99 | return "", fmt.Errorf("failed to create policy %s: %w", policyName, err) 100 | } 101 | 102 | _, err = c.adminClient.AttachPolicy(ctx, madmin.PolicyAssociationReq{ 103 | Policies: []string{policyName}, 104 | User: accessKey, 105 | }) 106 | if err != nil { 107 | return "", fmt.Errorf("failed to attach policy %s to user %s: %w", policyName, accessKey, err) 108 | } 109 | 110 | return secretKey, nil 111 | } 112 | 113 | func (c *minioClient) SetNewUserSecretKey(ctx context.Context, accessKey string) (string, error) { 114 | // Update existing user with new secret key 115 | newSecretKey, err := generateBase64Secret(16) 116 | if err != nil { 117 | return "", fmt.Errorf("failed to generate new secret key: %w", err) 118 | } 119 | 120 | err = c.adminClient.SetUser(ctx, accessKey, newSecretKey, madmin.AccountEnabled) 121 | if err != nil { 122 | return "", fmt.Errorf("failed to update user %s secret key: %w", accessKey, err) 123 | } 124 | return newSecretKey, nil 125 | } 126 | 127 | // Helper function to generate base64 encoded secret 128 | func generateBase64Secret(length int) (string, error) { 129 | bytes := make([]byte, length) 130 | _, err := rand.Read(bytes) 131 | if err != nil { 132 | return "", err 133 | } 134 | return base64.URLEncoding.EncodeToString(bytes), nil 135 | } 136 | -------------------------------------------------------------------------------- /pkg/ocp/mocks/client_interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | ocp "github.com/bankdata/styra-controller/pkg/ocp" 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // ClientInterface is an autogenerated mock type for the ClientInterface type 13 | type ClientInterface struct { 14 | mock.Mock 15 | } 16 | 17 | // DeleteBundle provides a mock function with given fields: ctx, name 18 | func (_m *ClientInterface) DeleteBundle(ctx context.Context, name string) error { 19 | ret := _m.Called(ctx, name) 20 | 21 | if len(ret) == 0 { 22 | panic("no return value specified for DeleteBundle") 23 | } 24 | 25 | var r0 error 26 | if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { 27 | r0 = rf(ctx, name) 28 | } else { 29 | r0 = ret.Error(0) 30 | } 31 | 32 | return r0 33 | } 34 | 35 | // DeleteSource provides a mock function with given fields: ctx, id 36 | func (_m *ClientInterface) DeleteSource(ctx context.Context, id string) error { 37 | ret := _m.Called(ctx, id) 38 | 39 | if len(ret) == 0 { 40 | panic("no return value specified for DeleteSource") 41 | } 42 | 43 | var r0 error 44 | if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { 45 | r0 = rf(ctx, id) 46 | } else { 47 | r0 = ret.Error(0) 48 | } 49 | 50 | return r0 51 | } 52 | 53 | // GetSource provides a mock function with given fields: ctx, id 54 | func (_m *ClientInterface) GetSource(ctx context.Context, id string) (*ocp.GetSourceResponse, error) { 55 | ret := _m.Called(ctx, id) 56 | 57 | if len(ret) == 0 { 58 | panic("no return value specified for GetSource") 59 | } 60 | 61 | var r0 *ocp.GetSourceResponse 62 | var r1 error 63 | if rf, ok := ret.Get(0).(func(context.Context, string) (*ocp.GetSourceResponse, error)); ok { 64 | return rf(ctx, id) 65 | } 66 | if rf, ok := ret.Get(0).(func(context.Context, string) *ocp.GetSourceResponse); ok { 67 | r0 = rf(ctx, id) 68 | } else { 69 | if ret.Get(0) != nil { 70 | r0 = ret.Get(0).(*ocp.GetSourceResponse) 71 | } 72 | } 73 | 74 | if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { 75 | r1 = rf(ctx, id) 76 | } else { 77 | r1 = ret.Error(1) 78 | } 79 | 80 | return r0, r1 81 | } 82 | 83 | // PutBundle provides a mock function with given fields: ctx, bundle 84 | func (_m *ClientInterface) PutBundle(ctx context.Context, bundle *ocp.PutBundleRequest) error { 85 | ret := _m.Called(ctx, bundle) 86 | 87 | if len(ret) == 0 { 88 | panic("no return value specified for PutBundle") 89 | } 90 | 91 | var r0 error 92 | if rf, ok := ret.Get(0).(func(context.Context, *ocp.PutBundleRequest) error); ok { 93 | r0 = rf(ctx, bundle) 94 | } else { 95 | r0 = ret.Error(0) 96 | } 97 | 98 | return r0 99 | } 100 | 101 | // PutSource provides a mock function with given fields: ctx, id, request 102 | func (_m *ClientInterface) PutSource(ctx context.Context, id string, request *ocp.PutSourceRequest) (*ocp.PutSourceResponse, error) { 103 | ret := _m.Called(ctx, id, request) 104 | 105 | if len(ret) == 0 { 106 | panic("no return value specified for PutSource") 107 | } 108 | 109 | var r0 *ocp.PutSourceResponse 110 | var r1 error 111 | if rf, ok := ret.Get(0).(func(context.Context, string, *ocp.PutSourceRequest) (*ocp.PutSourceResponse, error)); ok { 112 | return rf(ctx, id, request) 113 | } 114 | if rf, ok := ret.Get(0).(func(context.Context, string, *ocp.PutSourceRequest) *ocp.PutSourceResponse); ok { 115 | r0 = rf(ctx, id, request) 116 | } else { 117 | if ret.Get(0) != nil { 118 | r0 = ret.Get(0).(*ocp.PutSourceResponse) 119 | } 120 | } 121 | 122 | if rf, ok := ret.Get(1).(func(context.Context, string, *ocp.PutSourceRequest) error); ok { 123 | r1 = rf(ctx, id, request) 124 | } else { 125 | r1 = ret.Error(1) 126 | } 127 | 128 | return r0, r1 129 | } 130 | 131 | // NewClientInterface creates a new instance of ClientInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 132 | // The first argument is typically a *testing.T value. 133 | func NewClientInterface(t interface { 134 | mock.TestingT 135 | Cleanup(func()) 136 | }) *ClientInterface { 137 | mock := &ClientInterface{} 138 | mock.Mock.Test(t) 139 | 140 | t.Cleanup(func() { mock.AssertExpectations(t) }) 141 | 142 | return mock 143 | } 144 | -------------------------------------------------------------------------------- /pkg/styra/users_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package styra_test 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "errors" 23 | "io" 24 | "net/http" 25 | "time" 26 | 27 | "github.com/bankdata/styra-controller/pkg/httperror" 28 | ginkgo "github.com/onsi/ginkgo/v2" 29 | gomega "github.com/onsi/gomega" 30 | "github.com/patrickmn/go-cache" 31 | ) 32 | 33 | var _ = ginkgo.Describe("GetUser", func() { 34 | 35 | type test struct { 36 | name string 37 | responseCode int 38 | responseBody string 39 | expectStyraErr bool 40 | } 41 | 42 | ginkgo.DescribeTable("GetUser", func(test test) { 43 | c := newTestClient(func(r *http.Request) *http.Response { 44 | bs, err := io.ReadAll(r.Body) 45 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 46 | gomega.Expect(bs).To(gomega.Equal([]byte(""))) 47 | gomega.Expect(r.Method).To(gomega.Equal(http.MethodGet)) 48 | gomega.Expect(r.URL.String()).To(gomega.Equal("http://test.com/v1/users/" + test.name)) 49 | 50 | return &http.Response{ 51 | Header: make(http.Header), 52 | StatusCode: test.responseCode, 53 | Body: io.NopCloser(bytes.NewBufferString(test.responseBody)), 54 | } 55 | }) 56 | 57 | res, err := c.GetUser(context.Background(), test.name) 58 | if test.expectStyraErr { 59 | gomega.Expect(res).To(gomega.BeNil()) 60 | target := &httperror.HTTPError{} 61 | gomega.Expect(errors.As(err, &target)).To(gomega.BeTrue()) 62 | } else { 63 | gomega.Expect(err).ToNot(gomega.HaveOccurred()) 64 | gomega.Expect(res.StatusCode).To(gomega.Equal(test.responseCode)) 65 | } 66 | }, 67 | 68 | ginkgo.Entry("something", test{ 69 | name: "name", 70 | responseCode: http.StatusOK, 71 | responseBody: `{ 72 | "request_id": "id", 73 | "result": { 74 | "enabled": false, 75 | "id": "name" 76 | } 77 | }`, 78 | }), 79 | 80 | ginkgo.Entry("styra http error", test{ 81 | responseCode: http.StatusInternalServerError, 82 | expectStyraErr: true, 83 | }), 84 | ) 85 | }) 86 | 87 | var _ = ginkgo.Describe("GetUsers", func() { 88 | type test struct { 89 | responseCode int 90 | responseBody string 91 | expectStyraErr bool 92 | } 93 | 94 | ginkgo.DescribeTable("GetUsers", 95 | func(test test) { 96 | c := newTestClientWithCache(func(r *http.Request) *http.Response { 97 | bs, err := io.ReadAll(r.Body) 98 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 99 | gomega.Expect(bs).To(gomega.Equal([]byte(""))) 100 | gomega.Expect(r.Method).To(gomega.Equal(http.MethodGet)) 101 | gomega.Expect(r.URL.String()).To(gomega.Equal("http://test.com/v1/users")) 102 | 103 | return &http.Response{ 104 | Header: make(http.Header), 105 | StatusCode: test.responseCode, 106 | Body: io.NopCloser(bytes.NewBufferString(test.responseBody)), 107 | } 108 | }, cache.New(1*time.Hour, 10*time.Minute)) 109 | 110 | // Call GetUsers 111 | res, _, err := c.GetUsers(context.Background()) 112 | if test.expectStyraErr { 113 | gomega.Expect(res).To(gomega.BeNil()) 114 | target := &httperror.HTTPError{} 115 | gomega.Expect(errors.As(err, &target)).To(gomega.BeTrue()) 116 | } else { 117 | gomega.Expect(err).ToNot(gomega.HaveOccurred()) 118 | gomega.Expect(res.Users).ToNot(gomega.BeNil()) 119 | gomega.Expect(res.Users[0].ID).To(gomega.Equal("user1")) 120 | gomega.Expect(res.Users[0].Enabled).To(gomega.BeTrue()) 121 | gomega.Expect(res.Users[1].ID).To(gomega.Equal("user2")) 122 | gomega.Expect(res.Users[1].Enabled).To(gomega.BeFalse()) 123 | } 124 | }, 125 | 126 | ginkgo.Entry("successful response", test{ 127 | responseCode: http.StatusOK, 128 | responseBody: `{ 129 | "result": [ 130 | {"enabled": true, "id": "user1"}, 131 | {"enabled": false, "id": "user2"} 132 | ] 133 | }`, 134 | }), 135 | 136 | ginkgo.Entry("styra http error", test{ 137 | responseCode: http.StatusInternalServerError, 138 | expectStyraErr: true, 139 | }), 140 | ) 141 | }) 142 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package config provides utilities for reading configfiles 18 | package config 19 | 20 | import ( 21 | "os" 22 | "regexp" 23 | 24 | "github.com/bankdata/styra-controller/api/config/v2alpha2" 25 | "github.com/pkg/errors" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/apimachinery/pkg/runtime/serializer" 28 | "sigs.k8s.io/controller-runtime/pkg/manager" 29 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 30 | "sigs.k8s.io/controller-runtime/pkg/webhook" 31 | ) 32 | 33 | const ( 34 | healthProbeBindAddress = ":8081" 35 | metricsBindAddress = ":8080" 36 | leaderElectionID = "5d272013.bankdata.dk" 37 | webhookPort = 9443 38 | ) 39 | 40 | // Load loads controller configuration from the given file using the types 41 | // registered in the scheme. 42 | func Load(file string, scheme *runtime.Scheme) (*v2alpha2.ProjectConfig, error) { 43 | bs, err := os.ReadFile(file) 44 | if err != nil { 45 | return nil, errors.Wrap(err, "could not read config file") 46 | } 47 | return deserialize(bs, scheme) 48 | } 49 | 50 | // OptionsFromConfig creates a manager.Options based on a configuration file 51 | func OptionsFromConfig(cfg *v2alpha2.ProjectConfig, scheme *runtime.Scheme) manager.Options { 52 | o := manager.Options{ 53 | Scheme: scheme, 54 | HealthProbeBindAddress: healthProbeBindAddress, 55 | WebhookServer: webhook.NewServer(webhook.Options{Port: webhookPort}), 56 | Metrics: metricsserver.Options{ 57 | BindAddress: metricsBindAddress, 58 | }, 59 | } 60 | 61 | if cfg.LeaderElection != nil { 62 | o.LeaderElection = true 63 | o.LeaseDuration = &cfg.LeaderElection.LeaseDuration.Duration 64 | o.RenewDeadline = &cfg.LeaderElection.RenewDeadline.Duration 65 | o.RetryPeriod = &cfg.LeaderElection.RetryPeriod.Duration 66 | o.LeaderElectionID = leaderElectionID 67 | } 68 | 69 | return o 70 | } 71 | 72 | // TokenFromConfig returns the Styra DAS api token directly from "styra.token" 73 | // in the config or using the "styra.tokenSecretPath" to retrieve it fra a secret 74 | func TokenFromConfig(cfg *v2alpha2.ProjectConfig) (string, error) { 75 | if cfg.Styra.Token != "" { 76 | return cfg.Styra.Token, nil 77 | } 78 | 79 | if cfg.Styra.TokenSecretPath != "" { 80 | styraURLBytes, err := os.ReadFile(cfg.Styra.TokenSecretPath) 81 | if err != nil { 82 | return "", errors.Wrapf(err, "Could not ready Styra token from TokenSecretPath: %s", cfg.Styra.TokenSecretPath) 83 | } 84 | return string(styraURLBytes), nil 85 | } 86 | 87 | return "", errors.New("No token or tokenSecretPath defined in the config") 88 | } 89 | 90 | func deserialize(data []byte, scheme *runtime.Scheme) (*v2alpha2.ProjectConfig, error) { 91 | decoder := serializer.NewCodecFactory(scheme).UniversalDeserializer() 92 | _, gvk, err := decoder.Decode(data, nil, nil) 93 | if err != nil { 94 | return nil, errors.Wrap(err, "could not decode config") 95 | } 96 | 97 | if gvk.Group != v2alpha2.GroupVersion.Group { 98 | return nil, errors.New("unsupported api group") 99 | } 100 | 101 | if gvk.Kind != "ProjectConfig" { 102 | return nil, errors.New("unsupported api kind") 103 | } 104 | 105 | cfg := &v2alpha2.ProjectConfig{} 106 | 107 | switch gvk.Version { 108 | case v2alpha2.GroupVersion.Version: 109 | if _, _, err := decoder.Decode(data, nil, cfg); err != nil { 110 | return nil, errors.Wrap(err, "could not decode into kind") 111 | } 112 | default: 113 | return nil, errors.New("unsupported api version") 114 | } 115 | 116 | return cfg, nil 117 | } 118 | 119 | // MatchesIgnorePattern matches a specified ignore pattern, and excludes matches from being deleted 120 | func MatchesIgnorePattern(ignorePatterns []string, id string) (bool, error) { 121 | for _, patternString := range ignorePatterns { 122 | matches, err := regexp.MatchString(patternString, id) 123 | if err != nil { 124 | return false, errors.Wrapf(err, "could not compile regex pattern: %s", patternString) 125 | } 126 | if matches { 127 | return true, nil 128 | } 129 | } 130 | return false, nil 131 | } 132 | -------------------------------------------------------------------------------- /pkg/styra/library.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package styra 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | "fmt" 23 | "io" 24 | "net/http" 25 | 26 | "github.com/bankdata/styra-controller/pkg/httperror" 27 | "github.com/pkg/errors" 28 | ) 29 | 30 | const ( 31 | endpointV1Libraries = "/v1/libraries" 32 | ) 33 | 34 | type getLibraryJSONResponse struct { 35 | Result *LibraryEntityExpanded `json:"result"` 36 | } 37 | 38 | // GetLibraryResponse is the response type for calls to the 39 | // GET /v1/libraries/{id} endpoint in the Styra API. 40 | type GetLibraryResponse struct { 41 | Statuscode int 42 | Body []byte 43 | LibraryEntityExpanded *LibraryEntityExpanded 44 | } 45 | 46 | // LibraryEntityExpanded is the type that defines of a Library 47 | type LibraryEntityExpanded struct { 48 | DataSources []LibraryDatasourceConfig `json:"datasources"` 49 | Description string `json:"description"` 50 | ID string `json:"id"` 51 | ReadOnly bool `json:"read_only"` 52 | SourceControl *LibrarySourceControlConfig `json:"source_control"` 53 | } 54 | 55 | // LibraryDatasourceConfig defines metadata of a datasource 56 | type LibraryDatasourceConfig struct { 57 | Category string `json:"category"` 58 | ID string `json:"id"` 59 | } 60 | 61 | // LibrarySourceControlConfig is a struct from styra where we only use a single field 62 | // but kept for clarity when comparing to the API 63 | type LibrarySourceControlConfig struct { 64 | LibraryOrigin *LibraryGitRepoConfig `json:"library_origin"` 65 | } 66 | 67 | // LibraryGitRepoConfig defines the Git configurations a library can be defined by 68 | type LibraryGitRepoConfig struct { 69 | Commit string `json:"commit"` 70 | Credentials string `json:"credentials"` 71 | Path string `json:"path"` 72 | Reference string `json:"reference"` 73 | URL string `json:"url"` 74 | } 75 | 76 | // UpsertLibraryRequest is the request body for the 77 | // PUT /v1/libraries/{id} endpoint in the Styra API. 78 | type UpsertLibraryRequest struct { 79 | Description string `json:"description"` 80 | ReadOnly bool `json:"read_only"` 81 | SourceControl *LibrarySourceControlConfig `json:"source_control"` 82 | } 83 | 84 | // UpsertLibraryResponse is the response body for the 85 | // PUT /v1/libraries/{id} endpoint in the Styra API. 86 | type UpsertLibraryResponse struct { 87 | StatusCode int 88 | Body []byte 89 | } 90 | 91 | // GetLibrary calls the GET /v1/libraries/{id} endpoint in the 92 | // Styra API. 93 | func (c *Client) GetLibrary(ctx context.Context, id string) (*GetLibraryResponse, error) { 94 | res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("%s/%s", endpointV1Libraries, id), nil, nil) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | body, err := io.ReadAll(res.Body) 100 | if err != nil { 101 | return nil, errors.Wrap(err, "could not read body") 102 | } 103 | 104 | if res.StatusCode != http.StatusOK { 105 | err := httperror.NewHTTPError(res.StatusCode, string(body)) 106 | return nil, err 107 | } 108 | 109 | var jsonRes getLibraryJSONResponse 110 | if err := json.Unmarshal(body, &jsonRes); err != nil { 111 | return nil, errors.Wrap(err, "could not unmarshal body") 112 | } 113 | 114 | return &GetLibraryResponse{ 115 | Statuscode: res.StatusCode, 116 | Body: body, 117 | LibraryEntityExpanded: jsonRes.Result, 118 | }, nil 119 | } 120 | 121 | // UpsertLibrary calls the PUT /v1/libraries/{id} endpoint in the 122 | // Styra API. 123 | func (c *Client) UpsertLibrary(ctx context.Context, id string, request *UpsertLibraryRequest, 124 | ) (*UpsertLibraryResponse, error) { 125 | res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("%s/%s", endpointV1Libraries, id), request, nil) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | body, err := io.ReadAll(res.Body) 131 | if err != nil { 132 | return nil, errors.Wrap(err, "could not read body") 133 | } 134 | 135 | if res.StatusCode != http.StatusOK { 136 | err := httperror.NewHTTPError(res.StatusCode, string(body)) 137 | return nil, err 138 | } 139 | 140 | resp := UpsertLibraryResponse{ 141 | StatusCode: res.StatusCode, 142 | Body: body, 143 | } 144 | 145 | return &resp, nil 146 | } 147 | -------------------------------------------------------------------------------- /api/styra/v1beta1/system_types_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | "time" 21 | 22 | ginkgo "github.com/onsi/ginkgo/v2" 23 | gomega "github.com/onsi/gomega" 24 | 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | 27 | "github.com/bankdata/styra-controller/pkg/ptr" 28 | ) 29 | 30 | var _ = ginkgo.Describe("Expected", func() { 31 | ginkgo.Describe("Value", func() { 32 | ginkgo.It("returns true by default", func() { 33 | gomega.Expect(Expected{}.Value()).To(gomega.BeTrue()) 34 | }) 35 | 36 | ginkgo.It("returns true in invalid configurations", func() { 37 | gomega.Expect(Expected{ 38 | Boolean: ptr.Bool(false), 39 | String: ptr.String("test"), 40 | Integer: ptr.Int(42), 41 | }.Value()).To(gomega.BeTrue()) 42 | }) 43 | 44 | ginkgo.It("returns the value set", func() { 45 | gomega.Expect(Expected{Boolean: ptr.Bool(true)}.Value()).To(gomega.BeTrue()) 46 | gomega.Expect(Expected{Boolean: ptr.Bool(false)}.Value()).To(gomega.BeFalse()) 47 | gomega.Expect(Expected{String: ptr.String("")}.Value()).To(gomega.Equal("")) 48 | gomega.Expect(Expected{String: ptr.String("test")}.Value()).To(gomega.Equal("test")) 49 | gomega.Expect(Expected{Integer: ptr.Int(0)}.Value()).To(gomega.Equal(0)) 50 | gomega.Expect(Expected{Integer: ptr.Int(42)}.Value()).To(gomega.Equal(42)) 51 | }) 52 | }) 53 | }) 54 | 55 | var _ = ginkgo.Describe("System", func() { 56 | 57 | ginkgo.DescribeTable("SetCondition", 58 | func( 59 | conditions []Condition, 60 | conditionType ConditionType, 61 | status metav1.ConditionStatus, 62 | expectedConditions []Condition, 63 | ) { 64 | ss := System{ 65 | Status: SystemStatus{ 66 | Conditions: conditions, 67 | }, 68 | } 69 | ss.setCondition(func() time.Time { 70 | return time.Time{} 71 | }, conditionType, status) 72 | 73 | gomega.Ω(ss.Status.Conditions).To(gomega.Equal(expectedConditions)) 74 | }, 75 | 76 | ginkgo.Entry("Add first condition", nil, 77 | ConditionTypeCreatedInStyra, metav1.ConditionTrue, 78 | []Condition{ 79 | { 80 | Type: ConditionTypeCreatedInStyra, 81 | Status: metav1.ConditionTrue, 82 | }, 83 | }, 84 | ), 85 | 86 | ginkgo.Entry("Add new condition", 87 | []Condition{ 88 | { 89 | Type: ConditionTypeCreatedInStyra, 90 | Status: metav1.ConditionTrue, 91 | }, 92 | }, 93 | ConditionTypeGitCredentialsUpdated, metav1.ConditionFalse, 94 | []Condition{ 95 | { 96 | Type: ConditionTypeCreatedInStyra, 97 | Status: metav1.ConditionTrue, 98 | }, 99 | { 100 | Type: ConditionTypeGitCredentialsUpdated, 101 | Status: metav1.ConditionFalse, 102 | }, 103 | }, 104 | ), 105 | 106 | ginkgo.Entry("Update status on existing condition", 107 | []Condition{ 108 | { 109 | Type: ConditionTypeCreatedInStyra, 110 | Status: metav1.ConditionTrue, 111 | }, 112 | { 113 | Type: ConditionTypeGitCredentialsUpdated, 114 | Status: metav1.ConditionFalse, 115 | }, 116 | }, 117 | ConditionTypeGitCredentialsUpdated, metav1.ConditionTrue, 118 | []Condition{ 119 | { 120 | Type: ConditionTypeCreatedInStyra, 121 | Status: metav1.ConditionTrue, 122 | }, 123 | { 124 | Type: ConditionTypeGitCredentialsUpdated, 125 | Status: metav1.ConditionTrue, 126 | }, 127 | }, 128 | ), 129 | ) 130 | 131 | ginkgo.DescribeTable("DisplayName", 132 | func(system *System, prefix, suffix, expected string) { 133 | gomega.Expect(system.DisplayName(prefix, suffix)).To(gomega.Equal(expected)) 134 | }, 135 | 136 | ginkgo.Entry("only namespace and name", &System{ObjectMeta: metav1.ObjectMeta{ 137 | Namespace: "namespace", 138 | Name: "name", 139 | }}, "", "", "namespace/name"), 140 | 141 | ginkgo.Entry("with prefix", &System{ObjectMeta: metav1.ObjectMeta{ 142 | Namespace: "namespace", 143 | Name: "name", 144 | }}, "test", "", "test/namespace/name"), 145 | 146 | ginkgo.Entry("also with suffix", &System{ObjectMeta: metav1.ObjectMeta{ 147 | Namespace: "namespace", 148 | Name: "name", 149 | }}, "test", "cluster1", "test/namespace/name/cluster1"), 150 | ) 151 | 152 | ginkgo.Describe("GitSecretID", func() { 153 | ginkgo.It("creates the git secret ID", func() { 154 | s := &System{Status: SystemStatus{ID: "testid"}} 155 | gomega.Expect(s.GitSecretID()).To(gomega.Equal("systems/testid/git")) 156 | }) 157 | }) 158 | }) 159 | -------------------------------------------------------------------------------- /api/styra/v1alpha1/library_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // LibrarySpec defines the desired state of Library 24 | type LibrarySpec struct { 25 | 26 | // Name is the name the Library will have in Styra DAS 27 | Name string `json:"name"` 28 | 29 | // Description is the description of the Library 30 | Description string `json:"description"` 31 | 32 | // Subjects is the list of subjects which should have access to the system. 33 | Subjects []LibrarySubject `json:"subjects,omitempty"` 34 | 35 | // SourceControl is the sourcecontrol configuration for the Library 36 | SourceControl *SourceControl `json:"sourceControl,omitempty"` 37 | 38 | // Datasources is the list of datasources in the Library 39 | Datasources []LibraryDatasource `json:"datasources,omitempty"` 40 | } 41 | 42 | // LibraryDatasource contains metadata of a datasource, stored in a library 43 | type LibraryDatasource struct { 44 | // Path is the path within the system where the datasource should reside. 45 | Path string `json:"path"` 46 | 47 | // Description is a description of the datasource 48 | Description string `json:"description,omitempty"` 49 | } 50 | 51 | // SourceControl is a struct from styra where we only use a single field 52 | // but kept for clarity when comparing to the API 53 | type SourceControl struct { 54 | LibraryOrigin *GitRepo `json:"libraryOrigin"` 55 | } 56 | 57 | // LibrarySubjectKind represents a kind of a subject. 58 | type LibrarySubjectKind string 59 | 60 | const ( 61 | // LibrarySubjectKindUser is the subject kind user. 62 | LibrarySubjectKindUser LibrarySubjectKind = "user" 63 | 64 | // LibrarySubjectKindGroup is the subject kind group. 65 | LibrarySubjectKindGroup LibrarySubjectKind = "group" 66 | ) 67 | 68 | // LibrarySubject represents a subject which has been granted access to the Library. 69 | // The subject is assigned to the LibraryViewer role. 70 | type LibrarySubject struct { 71 | // Kind is the LibrarySubjectKind of the subject. 72 | //+kubebuilder:validation:Enum=user;group 73 | Kind LibrarySubjectKind `json:"kind,omitempty"` 74 | 75 | // Name is the name of the subject. The meaning of this field depends on the 76 | // SubjectKind. 77 | Name string `json:"name"` 78 | } 79 | 80 | // IsUser returns whether or not the kind of the subject is a user. 81 | func (subject LibrarySubject) IsUser() bool { 82 | return subject.Kind == LibrarySubjectKindUser || subject.Kind == "" 83 | } 84 | 85 | // GitRepo defines the Git configurations a library can be defined by 86 | type GitRepo struct { 87 | // Path is the path in the git repo where the policies are located. 88 | Path string `json:"path,omitempty"` 89 | 90 | // Reference is used to point to a tag or branch. This will be ignored if 91 | // `Commit` is specified. 92 | Reference string `json:"reference,omitempty"` 93 | 94 | // Commit is used to point to a specific commit SHA. This takes precedence 95 | // over `Reference` if both are specified. 96 | Commit string `json:"commit,omitempty"` 97 | 98 | // URL is the URL of the git repo. 99 | URL string `json:"url"` 100 | } 101 | 102 | // LibrarySecretRef defines how to access a k8s secret for the library. 103 | type LibrarySecretRef struct { 104 | // Namespace is the namespace where the secret resides. 105 | Namespace string `json:"namespace"` 106 | // Name is the name of the secret. 107 | Name string `json:"name"` 108 | } 109 | 110 | // LibraryStatus defines the observed state of Library 111 | type LibraryStatus struct { 112 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 113 | // Important: Run "make" to regenerate code after modifying this file 114 | } 115 | 116 | //+kubebuilder:object:root=true 117 | //+kubebuilder:subresource:status 118 | //+kubebuilder:resource:scope=Cluster 119 | 120 | // Library is the Schema for the libraries API 121 | type Library struct { 122 | metav1.TypeMeta `json:",inline"` 123 | metav1.ObjectMeta `json:"metadata,omitempty"` 124 | 125 | Spec LibrarySpec `json:"spec,omitempty"` 126 | Status LibraryStatus `json:"status,omitempty"` 127 | } 128 | 129 | //+kubebuilder:object:root=true 130 | 131 | // LibraryList contains a list of Library 132 | type LibraryList struct { 133 | metav1.TypeMeta `json:",inline"` 134 | metav1.ListMeta `json:"metadata,omitempty"` 135 | Items []Library `json:"items"` 136 | } 137 | 138 | func init() { 139 | SchemeBuilder.Register(&Library{}, &LibraryList{}) 140 | } 141 | -------------------------------------------------------------------------------- /internal/webhook/styra/v1beta1/webhook_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2025 Bankdata (bankdata@bankdata.dk) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | "context" 21 | "crypto/tls" 22 | "fmt" 23 | "net" 24 | "path/filepath" 25 | "testing" 26 | "time" 27 | 28 | ginkgo "github.com/onsi/ginkgo/v2" 29 | gomega "github.com/onsi/gomega" 30 | 31 | "k8s.io/apimachinery/pkg/runtime" 32 | ctrl "sigs.k8s.io/controller-runtime" 33 | "sigs.k8s.io/controller-runtime/pkg/client" 34 | "sigs.k8s.io/controller-runtime/pkg/envtest" 35 | logf "sigs.k8s.io/controller-runtime/pkg/log" 36 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 37 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 38 | "sigs.k8s.io/controller-runtime/pkg/webhook" 39 | 40 | //+kubebuilder:scaffold:imports 41 | "github.com/bankdata/styra-controller/api/styra/v1alpha1" 42 | "github.com/bankdata/styra-controller/api/styra/v1beta1" 43 | ) 44 | 45 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 46 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 47 | 48 | var ( 49 | k8sClient client.Client 50 | testEnv *envtest.Environment 51 | ctx context.Context 52 | cancel context.CancelFunc 53 | ) 54 | 55 | func TestAPIs(t *testing.T) { 56 | gomega.RegisterFailHandler(ginkgo.Fail) 57 | 58 | ginkgo.RunSpecs(t, "test/integration/webhook") 59 | } 60 | 61 | var _ = ginkgo.BeforeSuite(func() { 62 | logf.SetLogger(zap.New(zap.WriteTo(ginkgo.GinkgoWriter), zap.UseDevMode(true))) 63 | 64 | if !ginkgo.Label("integration").MatchesLabelFilter(ginkgo.GinkgoLabelFilter()) { 65 | return 66 | } 67 | 68 | scheme := runtime.NewScheme() 69 | err := v1alpha1.AddToScheme(scheme) 70 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 71 | err = v1beta1.AddToScheme(scheme) 72 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 73 | 74 | //+kubebuilder:scaffold:scheme 75 | 76 | ginkgo.By("bootstrapping test environment") 77 | testEnv = &envtest.Environment{ 78 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "config", "crd", "bases")}, 79 | ErrorIfCRDPathMissing: false, 80 | WebhookInstallOptions: envtest.WebhookInstallOptions{ 81 | Paths: []string{filepath.Join("..", "..", "..", "..", "config", "webhook")}, 82 | }, 83 | CRDInstallOptions: envtest.CRDInstallOptions{ 84 | Scheme: scheme, 85 | }, 86 | } 87 | 88 | cfg, err := testEnv.Start() 89 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 90 | gomega.Expect(cfg).NotTo(gomega.BeNil()) 91 | 92 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) 93 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 94 | gomega.Expect(k8sClient).NotTo(gomega.BeNil()) 95 | 96 | // start webhook server using Manager 97 | webhookInstallOptions := &testEnv.WebhookInstallOptions 98 | mgr, err := ctrl.NewManager(cfg, ctrl.Options{ 99 | Scheme: scheme, 100 | WebhookServer: webhook.NewServer(webhook.Options{ 101 | Host: webhookInstallOptions.LocalServingHost, 102 | Port: webhookInstallOptions.LocalServingPort, 103 | CertDir: webhookInstallOptions.LocalServingCertDir, 104 | }), 105 | LeaderElection: false, 106 | Metrics: metricsserver.Options{ 107 | BindAddress: "0", 108 | }, 109 | }) 110 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 111 | 112 | err = SetupSystemWebhookWithManager(mgr) 113 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 114 | 115 | //+kubebuilder:scaffold:webhook 116 | 117 | ctx, cancel = context.WithCancel(context.Background()) 118 | 119 | go func() { 120 | defer ginkgo.GinkgoRecover() 121 | err = mgr.Start(ctx) 122 | if err != nil { 123 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 124 | } 125 | }() 126 | 127 | // wait for the webhook server to get ready 128 | dialer := &net.Dialer{Timeout: time.Second} 129 | addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) 130 | gomega.Eventually(func() error { 131 | conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) 132 | if err != nil { 133 | return err 134 | } 135 | err = conn.Close() 136 | if err != nil { 137 | return err 138 | } 139 | return nil 140 | }).Should(gomega.Succeed()) 141 | 142 | }) 143 | 144 | var _ = ginkgo.AfterSuite(func() { 145 | if testing.Short() { 146 | return 147 | } 148 | 149 | if cancel != nil { 150 | cancel() 151 | } 152 | 153 | ginkgo.By("tearing down the test environment") 154 | if testEnv != nil { 155 | err := testEnv.Stop() 156 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 157 | } 158 | }) 159 | --------------------------------------------------------------------------------