├── 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 |
29 | {{- range (visibleTypes (sortedTypes .Types)) -}}
30 | {{ if isExportedType . -}}
31 |
32 | {{ typeDisplayName . }}
33 |
34 | {{- end }}
35 | {{- end -}}
36 |
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 | Value
29 | Description
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 |
{{ typeDisplayName . }}
41 | {{ safe (renderComments .CommentLines) }}
42 |
43 | {{- end -}}
44 |
45 |
46 | {{ end }}
47 |
48 | {{ if .Members }}
49 |
50 |
51 |
52 | Field
53 | Description
54 |
55 |
56 |
57 | {{ if isExportedType . }}
58 |
59 |
60 | apiVersion
61 | string
62 |
63 |
64 | {{apiGroup .}}
65 |
66 |
67 |
68 |
69 |
70 | kind
71 | string
72 |
73 | {{.Name.Name}}
74 |
75 | {{ end }}
76 | {{ template "members" .}}
77 |
78 |
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 | [](https://goreportcard.com/report/github.com/bankdata/styra-controller)
2 | [](https://pkg.go.dev/github.com/bankdata/styra-controller)
3 | [](https://github.com/bankdata/styra-controller/releases/latest)
4 | [](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 |
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 |
--------------------------------------------------------------------------------