From 71086899dc025728329bc0440a058e308e887747 Mon Sep 17 00:00:00 2001
From: bdumpp <144795224+bdumpp@users.noreply.github.com>
Date: Wed, 8 Nov 2023 11:43:36 +0100
Subject: [PATCH] :sparkles: New Library CRD (#203)
Added new Library CRD with controller. This makes it possible to deploy
Library resources and the controller will create an equivalent Library
in Styra.
Also fixed flaky tests
---
Makefile | 6 +-
PROJECT | 13 +
api/styra/v1alpha1/library_types.go | 140 +++++
api/styra/v1alpha1/library_webhook.go | 79 +++
api/styra/v1alpha1/zz_generated.deepcopy.go | 184 +++++++
cmd/main.go | 21 +
.../bases/styra.bankdata.dk_libraries.yaml | 124 +++++
config/crd/kustomization.yaml | 3 +
.../cainjection_in_styra_libraries.yaml | 7 +
.../patches/webhook_in_styra_libraries.yaml | 16 +
config/default/webhookcainjection_patch.yaml | 29 ++
config/rbac/role.yaml | 26 +
config/rbac/styra_library_editor_role.yaml | 31 ++
config/rbac/styra_library_viewer_role.yaml | 27 +
config/samples/kustomization.yaml | 1 +
config/samples/styra_v1alpha1_library.yaml | 28 +
config/webhook/manifests.yaml | 40 ++
docs/apis/styra/v1alpha1.md | 446 +++++++++++++++-
docs/apis/styra/v1beta1.md | 2 +-
.../controller/styra/library_controller.go | 487 ++++++++++++++++++
pkg/styra/authz.go | 6 +
pkg/styra/client.go | 3 +
pkg/styra/library.go | 145 ++++++
pkg/styra/library_test.go | 245 +++++++++
pkg/styra/mocks/client_interface.go | 52 ++
.../controller/controller_suite_test.go | 18 +
.../globaldatasource_controller_test.go | 300 ++++++-----
.../controller/library_controller_test.go | 319 ++++++++++++
.../controller/system_controller_test.go | 19 +-
29 files changed, 2669 insertions(+), 148 deletions(-)
create mode 100644 api/styra/v1alpha1/library_types.go
create mode 100644 api/styra/v1alpha1/library_webhook.go
create mode 100644 config/crd/bases/styra.bankdata.dk_libraries.yaml
create mode 100644 config/crd/patches/cainjection_in_styra_libraries.yaml
create mode 100644 config/crd/patches/webhook_in_styra_libraries.yaml
create mode 100644 config/default/webhookcainjection_patch.yaml
create mode 100644 config/rbac/styra_library_editor_role.yaml
create mode 100644 config/rbac/styra_library_viewer_role.yaml
create mode 100644 config/samples/styra_v1alpha1_library.yaml
create mode 100644 internal/controller/styra/library_controller.go
create mode 100644 pkg/styra/library.go
create mode 100644 pkg/styra/library_test.go
create mode 100644 test/integration/controller/library_controller_test.go
diff --git a/Makefile b/Makefile
index 21ac4be..0370c67 100644
--- a/Makefile
+++ b/Makefile
@@ -76,15 +76,15 @@ lint: golangci-lint ## Run linters
.PHONY: test
test: ginkgo manifests generate lint envtest generate-mocks ## Run all tests.
- KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" $(GINKGO) -r --coverprofile cover.out
+ KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" $(GINKGO) -r -p --output-interceptor-mode=none
.PHONY: test-unit
test-unit: ginkgo manifests generate lint generate-mocks ## Run unit tests.
- $(GINKGO) -r --label-filter "!integration" --coverprofile cover.out
+ $(GINKGO) -r --label-filter "!integration" --output-interceptor-mode=none
.PHONY: test-integration ## Run integration tests.
test-integration: ginkgo manifests generate lint envtest generate-mocks ## Run integration tests.
- KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" $(GINKGO) -r --label-filter "integration" --coverprofile cover.out
+ KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" $(GINKGO) -r -p --label-filter "integration" --output-interceptor-mode=none
.PHONY: kind-create
kind-create: kind ## Create kind cluster
diff --git a/PROJECT b/PROJECT
index 7465265..dbac610 100644
--- a/PROJECT
+++ b/PROJECT
@@ -63,4 +63,17 @@ resources:
kind: ProjectConfig
path: github.com/bankdata/styra-controller/api/config/v2alpha2
version: v2alpha2
+- api:
+ crdVersion: v1
+ namespaced: true
+ controller: true
+ domain: bankdata.dk
+ group: styra
+ kind: Library
+ path: github.com/bankdata/styra-controller/api/styra/v1alpha1
+ version: v1alpha1
+ webhooks:
+ defaulting: true
+ validation: true
+ webhookVersion: v1
version: "3"
diff --git a/api/styra/v1alpha1/library_types.go b/api/styra/v1alpha1/library_types.go
new file mode 100644
index 0000000..899dc98
--- /dev/null
+++ b/api/styra/v1alpha1/library_types.go
@@ -0,0 +1,140 @@
+/*
+Copyright (C) 2023 Bankdata (bankdata@bankdata.dk)
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// LibrarySpec defines the desired state of Library
+type LibrarySpec struct {
+
+ // Name is the name the Library will have in Styra DAS
+ Name string `json:"name"`
+
+ // Description is the description of the Library
+ Description string `json:"description"`
+
+ // Subjects is the list of subjects which should have access to the system.
+ Subjects []LibrarySubject `json:"subjects,omitempty"`
+
+ // SourceControl is the sourcecontrol configuration for the Library
+ SourceControl *SourceControl `json:"sourceControl"`
+
+ // Datasources is the list of datasources in the Library
+ Datasources []LibraryDatasource `json:"datasources,omitempty"`
+}
+
+// LibraryDatasource contains metadata of a datasource, stored in a library
+type LibraryDatasource struct {
+ // Path is the path within the system where the datasource should reside.
+ Path string `json:"path"`
+
+ // Description is a description of the datasource
+ Description string `json:"description,omitempty"`
+}
+
+// SourceControl is a struct from styra where we only use a single field
+// but kept for clarity when comparing to the API
+type SourceControl struct {
+ LibraryOrigin *GitRepo `json:"libraryOrigin"`
+}
+
+// LibrarySubjectKind represents a kind of a subject.
+type LibrarySubjectKind string
+
+const (
+ // LibrarySubjectKindUser is the subject kind user.
+ LibrarySubjectKindUser LibrarySubjectKind = "user"
+
+ // LibrarySubjectKindGroup is the subject kind group.
+ LibrarySubjectKindGroup LibrarySubjectKind = "group"
+)
+
+// LibrarySubject represents a subject which has been granted access to the Library.
+// The subject is assigned to the LibraryViewer role.
+type LibrarySubject struct {
+ // Kind is the LibrarySubjectKind of the subject.
+ //+kubebuilder:validation:Enum=user;group
+ Kind LibrarySubjectKind `json:"kind,omitempty"`
+
+ // Name is the name of the subject. The meaning of this field depends on the
+ // SubjectKind.
+ Name string `json:"name"`
+}
+
+// IsUser returns whether or not the kind of the subject is a user.
+func (subject LibrarySubject) IsUser() bool {
+ return subject.Kind == LibrarySubjectKindUser || subject.Kind == ""
+}
+
+// GitRepo defines the Git configurations a library can be defined by
+type GitRepo struct {
+ // Path is the path in the git repo where the policies are located.
+ Path string `json:"path,omitempty"`
+
+ // Reference is used to point to a tag or branch. This will be ignored if
+ // `Commit` is specified.
+ Reference string `json:"reference,omitempty"`
+
+ // Commit is used to point to a specific commit SHA. This takes precedence
+ // over `Reference` if both are specified.
+ Commit string `json:"commit,omitempty"`
+
+ // URL is the URL of the git repo.
+ URL string `json:"url"`
+}
+
+// LibrarySecretRef defines how to access a k8s secret for the library.
+type LibrarySecretRef struct {
+ // Namespace is the namespace where the secret resides.
+ Namespace string `json:"namespace"`
+ // Name is the name of the secret.
+ Name string `json:"name"`
+}
+
+// LibraryStatus defines the observed state of Library
+type LibraryStatus struct {
+ // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
+ // Important: Run "make" to regenerate code after modifying this file
+}
+
+//+kubebuilder:object:root=true
+//+kubebuilder:subresource:status
+//+kubebuilder:resource:scope=Cluster
+
+// Library is the Schema for the libraries API
+type Library struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ Spec LibrarySpec `json:"spec,omitempty"`
+ Status LibraryStatus `json:"status,omitempty"`
+}
+
+//+kubebuilder:object:root=true
+
+// LibraryList contains a list of Library
+type LibraryList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty"`
+ Items []Library `json:"items"`
+}
+
+func init() {
+ SchemeBuilder.Register(&Library{}, &LibraryList{})
+}
diff --git a/api/styra/v1alpha1/library_webhook.go b/api/styra/v1alpha1/library_webhook.go
new file mode 100644
index 0000000..ba83b9c
--- /dev/null
+++ b/api/styra/v1alpha1/library_webhook.go
@@ -0,0 +1,79 @@
+/*
+Copyright (C) 2023 Bankdata (bankdata@bankdata.dk)
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha1
+
+import (
+ "k8s.io/apimachinery/pkg/runtime"
+ ctrl "sigs.k8s.io/controller-runtime"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/webhook"
+ "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+)
+
+// log is for logging in this package.
+var librarylog = logf.Log.WithName("library-resource")
+
+// SetupWebhookWithManager sets up the Library webhooks with the Manager.
+func (r *Library) SetupWebhookWithManager(mgr ctrl.Manager) error {
+ return ctrl.NewWebhookManagedBy(mgr).
+ For(r).
+ Complete()
+}
+
+// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
+
+//+kubebuilder:webhook:path=/mutate-styra-bankdata-dk-v1alpha1-library,mutating=true,failurePolicy=fail,sideEffects=None,groups=styra.bankdata.dk,resources=libraries,verbs=create;update,versions=v1alpha1,name=mlibrary.kb.io,admissionReviewVersions=v1
+
+var _ webhook.Defaulter = &Library{}
+
+// Default implements webhook.Defaulter so a webhook will be registered for the type
+func (r *Library) Default() {
+ librarylog.Info("default", "name", r.Name)
+
+ if r.Spec.SourceControl.LibraryOrigin.Commit != "" && r.Spec.SourceControl.LibraryOrigin.Reference != "" {
+ r.Spec.SourceControl.LibraryOrigin.Reference = ""
+ }
+}
+
+// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
+//+kubebuilder:webhook:path=/validate-styra-bankdata-dk-v1alpha1-library,mutating=false,failurePolicy=fail,sideEffects=None,groups=styra.bankdata.dk,resources=libraries,verbs=create;update,versions=v1alpha1,name=vlibrary.kb.io,admissionReviewVersions=v1
+
+var _ webhook.Validator = &Library{}
+
+// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
+func (r *Library) ValidateCreate() (admission.Warnings, error) {
+ librarylog.Info("validate create", "name", r.Name)
+
+ // TODO(user): fill in your validation logic upon object creation.
+ return nil, nil
+}
+
+// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
+func (r *Library) ValidateUpdate(_ runtime.Object) (admission.Warnings, error) {
+ librarylog.Info("validate update", "name", r.Name)
+
+ // TODO(user): fill in your validation logic upon object update.
+ return nil, nil
+}
+
+// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
+func (r *Library) ValidateDelete() (admission.Warnings, error) {
+ librarylog.Info("validate delete", "name", r.Name)
+
+ // TODO(user): fill in your validation logic upon object deletion.
+ return nil, nil
+}
diff --git a/api/styra/v1alpha1/zz_generated.deepcopy.go b/api/styra/v1alpha1/zz_generated.deepcopy.go
index 971b716..d340485 100644
--- a/api/styra/v1alpha1/zz_generated.deepcopy.go
+++ b/api/styra/v1alpha1/zz_generated.deepcopy.go
@@ -24,6 +24,21 @@ import (
"k8s.io/apimachinery/pkg/runtime"
)
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GitRepo) DeepCopyInto(out *GitRepo) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitRepo.
+func (in *GitRepo) DeepCopy() *GitRepo {
+ if in == nil {
+ return nil
+ }
+ out := new(GitRepo)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GlobalDatasource) DeepCopyInto(out *GlobalDatasource) {
*out = *in
@@ -142,3 +157,172 @@ func (in *GlobalDatasourceStatus) DeepCopy() *GlobalDatasourceStatus {
in.DeepCopyInto(out)
return out
}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Library) DeepCopyInto(out *Library) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ in.Spec.DeepCopyInto(&out.Spec)
+ out.Status = in.Status
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Library.
+func (in *Library) DeepCopy() *Library {
+ if in == nil {
+ return nil
+ }
+ out := new(Library)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Library) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LibraryDatasource) DeepCopyInto(out *LibraryDatasource) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LibraryDatasource.
+func (in *LibraryDatasource) DeepCopy() *LibraryDatasource {
+ if in == nil {
+ return nil
+ }
+ out := new(LibraryDatasource)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LibraryList) DeepCopyInto(out *LibraryList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]Library, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LibraryList.
+func (in *LibraryList) DeepCopy() *LibraryList {
+ if in == nil {
+ return nil
+ }
+ out := new(LibraryList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *LibraryList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LibrarySecretRef) DeepCopyInto(out *LibrarySecretRef) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LibrarySecretRef.
+func (in *LibrarySecretRef) DeepCopy() *LibrarySecretRef {
+ if in == nil {
+ return nil
+ }
+ out := new(LibrarySecretRef)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LibrarySpec) DeepCopyInto(out *LibrarySpec) {
+ *out = *in
+ if in.Subjects != nil {
+ in, out := &in.Subjects, &out.Subjects
+ *out = make([]LibrarySubject, len(*in))
+ copy(*out, *in)
+ }
+ if in.SourceControl != nil {
+ in, out := &in.SourceControl, &out.SourceControl
+ *out = new(SourceControl)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Datasources != nil {
+ in, out := &in.Datasources, &out.Datasources
+ *out = make([]LibraryDatasource, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LibrarySpec.
+func (in *LibrarySpec) DeepCopy() *LibrarySpec {
+ if in == nil {
+ return nil
+ }
+ out := new(LibrarySpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LibraryStatus) DeepCopyInto(out *LibraryStatus) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LibraryStatus.
+func (in *LibraryStatus) DeepCopy() *LibraryStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(LibraryStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LibrarySubject) DeepCopyInto(out *LibrarySubject) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LibrarySubject.
+func (in *LibrarySubject) DeepCopy() *LibrarySubject {
+ if in == nil {
+ return nil
+ }
+ out := new(LibrarySubject)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SourceControl) DeepCopyInto(out *SourceControl) {
+ *out = *in
+ if in.LibraryOrigin != nil {
+ in, out := &in.LibraryOrigin, &out.LibraryOrigin
+ *out = new(GitRepo)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceControl.
+func (in *SourceControl) DeepCopy() *SourceControl {
+ if in == nil {
+ return nil
+ }
+ out := new(SourceControl)
+ in.DeepCopyInto(out)
+ return out
+}
diff --git a/cmd/main.go b/cmd/main.go
index 39feacf..d5450cf 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -208,6 +208,27 @@ func main() {
}
}
+ libraryReconciler := &controllers.LibraryReconciler{
+ Client: mgr.GetClient(),
+ Scheme: mgr.GetScheme(),
+ Config: ctrlConfig,
+ Styra: styraClient,
+ }
+ if ctrlConfig.NotificationWebhook != nil {
+ libraryReconciler.WebhookClient = webhook.New(ctrlConfig.NotificationWebhook.Address)
+ }
+
+ if err = libraryReconciler.SetupWithManager(mgr); err != nil {
+ log.Error(err, "unable to create controller", "controller", "Library")
+ os.Exit(1)
+ }
+
+ if !ctrlConfig.DisableCRDWebhooks {
+ if err = (&styrav1alpha1.Library{}).SetupWebhookWithManager(mgr); err != nil {
+ log.Error(err, "unable to create webhook", "webhook", "Library")
+ os.Exit(1)
+ }
+ }
//+kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
log.Error(err, "unable to set up health check")
diff --git a/config/crd/bases/styra.bankdata.dk_libraries.yaml b/config/crd/bases/styra.bankdata.dk_libraries.yaml
new file mode 100644
index 0000000..96cb59d
--- /dev/null
+++ b/config/crd/bases/styra.bankdata.dk_libraries.yaml
@@ -0,0 +1,124 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.13.0
+ name: libraries.styra.bankdata.dk
+spec:
+ group: styra.bankdata.dk
+ names:
+ kind: Library
+ listKind: LibraryList
+ plural: libraries
+ singular: library
+ scope: Cluster
+ versions:
+ - name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: Library is the Schema for the libraries API
+ properties:
+ apiVersion:
+ description: 'APIVersion defines the versioned schema of this representation
+ of an object. Servers should convert recognized schemas to the latest
+ internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+ type: string
+ kind:
+ description: 'Kind is a string value representing the REST resource this
+ object represents. Servers may infer this from the endpoint the client
+ submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: LibrarySpec defines the desired state of Library
+ properties:
+ datasources:
+ description: Datasources is the list of datasources in the Library
+ items:
+ description: LibraryDatasource contains metadata of a datasource,
+ stored in a library
+ properties:
+ description:
+ description: Description is a description of the datasource
+ type: string
+ path:
+ description: Path is the path within the system where the datasource
+ should reside.
+ type: string
+ required:
+ - path
+ type: object
+ type: array
+ description:
+ description: Description is the description of the Library
+ type: string
+ name:
+ description: Name is the name the Library will have in Styra DAS
+ type: string
+ sourceControl:
+ description: SourceControl is the sourcecontrol configuration for
+ the Library
+ properties:
+ libraryOrigin:
+ description: GitRepo defines the Git configurations a library
+ can be defined by
+ properties:
+ commit:
+ description: Commit is used to point to a specific commit
+ SHA. This takes precedence over `Reference` if both are
+ specified.
+ type: string
+ path:
+ description: Path is the path in the git repo where the policies
+ are located.
+ type: string
+ reference:
+ description: Reference is used to point to a tag or branch.
+ This will be ignored if `Commit` is specified.
+ type: string
+ url:
+ description: URL is the URL of the git repo.
+ type: string
+ required:
+ - url
+ type: object
+ required:
+ - libraryOrigin
+ type: object
+ subjects:
+ description: Subjects is the list of subjects which should have access
+ to the system.
+ items:
+ description: LibrarySubject represents a subject which has been
+ granted access to the Library. The subject is assigned to the
+ LibraryViewer role.
+ properties:
+ kind:
+ description: Kind is the LibrarySubjectKind of the subject.
+ enum:
+ - user
+ - group
+ type: string
+ name:
+ description: Name is the name of the subject. The meaning of
+ this field depends on the SubjectKind.
+ type: string
+ required:
+ - name
+ type: object
+ type: array
+ required:
+ - description
+ - name
+ - sourceControl
+ type: object
+ status:
+ description: LibraryStatus defines the observed state of Library
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 36c1e13..f3e8d15 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -7,6 +7,7 @@ kind: Kustomization
resources:
- bases/styra.bankdata.dk_systems.yaml
- bases/styra.bankdata.dk_globaldatasources.yaml
+- bases/styra.bankdata.dk_libraries.yaml
#+kubebuilder:scaffold:crdkustomizeresource
patches:
@@ -14,12 +15,14 @@ patches:
# patches here are for enabling the conversion webhook for each CRD
- path: patches/webhook_in_styra_systems.yaml
- path: patches/webhook_in_styra_globaldatasources.yaml
+- path: patches/webhook_in_styra_libraries.yaml
#+kubebuilder:scaffold:crdkustomizewebhookpatch
# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
# patches here are for enabling the CA injection for each CRD
- path: patches/cainjection_in_styra_systems.yaml
- path: patches/cainjection_in_styra_globaldatasources.yaml
+- path: patches/cainjection_in_styra_libraries.yaml
#+kubebuilder:scaffold:crdkustomizecainjectionpatch
# the following config is for teaching kustomize how to do kustomization for CRDs.
diff --git a/config/crd/patches/cainjection_in_styra_libraries.yaml b/config/crd/patches/cainjection_in_styra_libraries.yaml
new file mode 100644
index 0000000..f3429e8
--- /dev/null
+++ b/config/crd/patches/cainjection_in_styra_libraries.yaml
@@ -0,0 +1,7 @@
+# The following patch adds a directive for certmanager to inject CA into the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME
+ name: libraries.styra.bankdata.dk
diff --git a/config/crd/patches/webhook_in_styra_libraries.yaml b/config/crd/patches/webhook_in_styra_libraries.yaml
new file mode 100644
index 0000000..cbe5f54
--- /dev/null
+++ b/config/crd/patches/webhook_in_styra_libraries.yaml
@@ -0,0 +1,16 @@
+# The following patch enables a conversion webhook for the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: libraries.styra.bankdata.dk
+spec:
+ conversion:
+ strategy: Webhook
+ webhook:
+ clientConfig:
+ service:
+ namespace: system
+ name: webhook-service
+ path: /convert
+ conversionReviewVersions:
+ - v1
diff --git a/config/default/webhookcainjection_patch.yaml b/config/default/webhookcainjection_patch.yaml
new file mode 100644
index 0000000..abad6d3
--- /dev/null
+++ b/config/default/webhookcainjection_patch.yaml
@@ -0,0 +1,29 @@
+# This patch add annotation to admission webhook config and
+# CERTIFICATE_NAMESPACE and CERTIFICATE_NAME will be substituted by kustomize
+apiVersion: admissionregistration.k8s.io/v1
+kind: MutatingWebhookConfiguration
+metadata:
+ labels:
+ app.kubernetes.io/name: mutatingwebhookconfiguration
+ app.kubernetes.io/instance: mutating-webhook-configuration
+ app.kubernetes.io/component: webhook
+ app.kubernetes.io/created-by: styra-controller
+ app.kubernetes.io/part-of: styra-controller
+ app.kubernetes.io/managed-by: kustomize
+ name: mutating-webhook-configuration
+ annotations:
+ cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+ labels:
+ app.kubernetes.io/name: validatingwebhookconfiguration
+ app.kubernetes.io/instance: validating-webhook-configuration
+ app.kubernetes.io/component: webhook
+ app.kubernetes.io/created-by: styra-controller
+ app.kubernetes.io/part-of: styra-controller
+ app.kubernetes.io/managed-by: kustomize
+ name: validating-webhook-configuration
+ annotations:
+ cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 7b4c137..54b1652 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -61,6 +61,32 @@ rules:
- get
- patch
- update
+- apiGroups:
+ - styra.bankdata.dk
+ resources:
+ - libraries
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - styra.bankdata.dk
+ resources:
+ - libraries/finalizers
+ verbs:
+ - update
+- apiGroups:
+ - styra.bankdata.dk
+ resources:
+ - libraries/status
+ verbs:
+ - get
+ - patch
+ - update
- apiGroups:
- styra.bankdata.dk
resources:
diff --git a/config/rbac/styra_library_editor_role.yaml b/config/rbac/styra_library_editor_role.yaml
new file mode 100644
index 0000000..25ade9e
--- /dev/null
+++ b/config/rbac/styra_library_editor_role.yaml
@@ -0,0 +1,31 @@
+# permissions for end users to edit libraries.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: clusterrole
+ app.kubernetes.io/instance: library-editor-role
+ app.kubernetes.io/component: rbac
+ app.kubernetes.io/created-by: styra-controller
+ app.kubernetes.io/part-of: styra-controller
+ app.kubernetes.io/managed-by: kustomize
+ name: library-editor-role
+rules:
+- apiGroups:
+ - styra.bankdata.dk
+ resources:
+ - libraries
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - styra.bankdata.dk
+ resources:
+ - libraries/status
+ verbs:
+ - get
diff --git a/config/rbac/styra_library_viewer_role.yaml b/config/rbac/styra_library_viewer_role.yaml
new file mode 100644
index 0000000..f959179
--- /dev/null
+++ b/config/rbac/styra_library_viewer_role.yaml
@@ -0,0 +1,27 @@
+# permissions for end users to view libraries.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: clusterrole
+ app.kubernetes.io/instance: library-viewer-role
+ app.kubernetes.io/component: rbac
+ app.kubernetes.io/created-by: styra-controller
+ app.kubernetes.io/part-of: styra-controller
+ app.kubernetes.io/managed-by: kustomize
+ name: library-viewer-role
+rules:
+- apiGroups:
+ - styra.bankdata.dk
+ resources:
+ - libraries
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - styra.bankdata.dk
+ resources:
+ - libraries/status
+ verbs:
+ - get
diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml
index dfc7dde..bcbdc46 100644
--- a/config/samples/kustomization.yaml
+++ b/config/samples/kustomization.yaml
@@ -6,4 +6,5 @@ resources:
- test_v1_object.yaml
- config_v2alpha1_projectconfig.yaml
- config_v2alpha2_projectconfig.yaml
+- styra_v1alpha1_library.yaml
#+kubebuilder:scaffold:manifestskustomizesamples
diff --git a/config/samples/styra_v1alpha1_library.yaml b/config/samples/styra_v1alpha1_library.yaml
new file mode 100644
index 0000000..1723da8
--- /dev/null
+++ b/config/samples/styra_v1alpha1_library.yaml
@@ -0,0 +1,28 @@
+apiVersion: styra.bankdata.dk/v1alpha1
+kind: Library
+metadata:
+ labels:
+ app.kubernetes.io/name: library
+ app.kubernetes.io/instance: library-sample
+ app.kubernetes.io/part-of: styra-controller
+ app.kubernetes.io/managed-by: kustomize
+ app.kubernetes.io/created-by: styra-controller
+ name: my-library
+spec:
+ name: mylibrary
+ description: my library
+ sourceControl:
+ libraryOrigin:
+ url: https://github.com/Bankdata/styra-controller.git
+ reference: refs/heads/master
+ commit: f37cc9d87251921cbe49349235d9b5305c833769
+ path: path
+ datasources:
+ - path: seconds/datasource
+ description: this is the second datasource
+ subjects:
+ - kind: user
+ name: user1@mail.dk
+ - kind: group
+ name: mygroup
+
\ No newline at end of file
diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml
index c232af4..2267548 100644
--- a/config/webhook/manifests.yaml
+++ b/config/webhook/manifests.yaml
@@ -24,6 +24,26 @@ webhooks:
resources:
- globaldatasources
sideEffects: None
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: webhook-service
+ namespace: system
+ path: /mutate-styra-bankdata-dk-v1alpha1-library
+ failurePolicy: Fail
+ name: mlibrary.kb.io
+ rules:
+ - apiGroups:
+ - styra.bankdata.dk
+ apiVersions:
+ - v1alpha1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - libraries
+ sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
@@ -70,6 +90,26 @@ webhooks:
resources:
- globaldatasources
sideEffects: None
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: webhook-service
+ namespace: system
+ path: /validate-styra-bankdata-dk-v1alpha1-library
+ failurePolicy: Fail
+ name: vlibrary.kb.io
+ rules:
+ - apiGroups:
+ - styra.bankdata.dk
+ apiVersions:
+ - v1alpha1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - libraries
+ sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
diff --git a/docs/apis/styra/v1alpha1.md b/docs/apis/styra/v1alpha1.md
index c1e211d..eb11640 100644
--- a/docs/apis/styra/v1alpha1.md
+++ b/docs/apis/styra/v1alpha1.md
@@ -11,6 +11,70 @@ group.
Resource Types:
@@ -417,8 +481,388 @@ string
GlobalDatasourceStatus holds the status of the GlobalDatasource resource.
+
Library
+
+
+
Library is the Schema for the libraries API
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+metadata
+
+
+k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta
+
+
+ |
+
+Refer to the Kubernetes API documentation for the fields of the
+metadata field.
+ |
+
+
+
+spec
+
+
+LibrarySpec
+
+
+ |
+
+
+
+
+
+
+name
+
+string
+
+ |
+
+ Name is the name the Library will have in Styra DAS
+ |
+
+
+
+description
+
+string
+
+ |
+
+ Description is the description of the Library
+ |
+
+
+
+subjects
+
+
+[]LibrarySubject
+
+
+ |
+
+ Subjects is the list of subjects which should have access to the system.
+ |
+
+
+
+sourceControl
+
+
+SourceControl
+
+
+ |
+
+ SourceControl is the sourcecontrol configuration for the Library
+ |
+
+
+
+datasources
+
+
+[]LibraryDatasource
+
+
+ |
+
+ Datasources is the list of datasources in the Library
+ |
+
+
+ |
+
+
+
+status
+
+
+LibraryStatus
+
+
+ |
+
+ |
+
+
+
+
LibraryDatasource
+
+
+(Appears on:LibrarySpec)
+
+
+
LibraryDatasource contains metadata of a datasource, stored in a library
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+path
+
+string
+
+ |
+
+ Path is the path within the system where the datasource should reside.
+ |
+
+
+
+description
+
+string
+
+ |
+
+ Description is a description of the datasource
+ |
+
+
+
+
LibrarySecretRef
+
+
+
LibrarySecretRef defines how to access a k8s secret for the library.
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+namespace
+
+string
+
+ |
+
+ Namespace is the namespace where the secret resides.
+ |
+
+
+
+name
+
+string
+
+ |
+
+ Name is the name of the secret.
+ |
+
+
+
+
LibrarySpec
+
+
+(Appears on:Library)
+
+
+
LibrarySpec defines the desired state of Library
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+name
+
+string
+
+ |
+
+ Name is the name the Library will have in Styra DAS
+ |
+
+
+
+description
+
+string
+
+ |
+
+ Description is the description of the Library
+ |
+
+
+
+subjects
+
+
+[]LibrarySubject
+
+
+ |
+
+ Subjects is the list of subjects which should have access to the system.
+ |
+
+
+
+sourceControl
+
+
+SourceControl
+
+
+ |
+
+ SourceControl is the sourcecontrol configuration for the Library
+ |
+
+
+
+datasources
+
+
+[]LibraryDatasource
+
+
+ |
+
+ Datasources is the list of datasources in the Library
+ |
+
+
+
+
LibraryStatus
+
+
+(Appears on:Library)
+
+
+
LibraryStatus defines the observed state of Library
+
+
LibrarySubject
+
+
+(Appears on:LibrarySpec)
+
+
+
LibrarySubject represents a subject which has been granted access to the Library.
+The subject is assigned to the LibraryViewer role.
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+kind
+
+
+LibrarySubjectKind
+
+
+ |
+
+ Kind is the LibrarySubjectKind of the subject.
+ |
+
+
+
+name
+
+string
+
+ |
+
+ Name is the name of the subject. The meaning of this field depends on the
+SubjectKind.
+ |
+
+
+
+
LibrarySubjectKind
+(string
alias)
+
+(Appears on:LibrarySubject)
+
+
+
LibrarySubjectKind represents a kind of a subject.
+
+
+
+
+Value |
+Description |
+
+
+"group" |
+LibrarySubjectKindGroup is the subject kind group.
+ |
+
"user" |
+LibrarySubjectKindUser is the subject kind user.
+ |
+
+
+
SourceControl
+
+
+(Appears on:LibrarySpec)
+
+
+
SourceControl is a struct from styra where we only use a single field
+but kept for clarity when comparing to the API
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+libraryOrigin
+
+
+GitRepo
+
+
+ |
+
+ |
+
+
+
Generated with gen-crd-api-reference-docs
-on git commit 534b87f
.
+on git commit d1f19e4
.
diff --git a/docs/apis/styra/v1beta1.md b/docs/apis/styra/v1beta1.md
index bc285e8..84e1563 100644
--- a/docs/apis/styra/v1beta1.md
+++ b/docs/apis/styra/v1beta1.md
@@ -1172,5 +1172,5 @@ System.
Generated with gen-crd-api-reference-docs
-on git commit d2179ec
.
+on git commit d1f19e4
.
diff --git a/internal/controller/styra/library_controller.go b/internal/controller/styra/library_controller.go
new file mode 100644
index 0000000..7356ec1
--- /dev/null
+++ b/internal/controller/styra/library_controller.go
@@ -0,0 +1,487 @@
+/*
+Copyright (C) 2023 Bankdata (bankdata@bankdata.dk)
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package styra
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "path"
+
+ ctrlerr "github.com/bankdata/styra-controller/internal/errors"
+ "github.com/bankdata/styra-controller/internal/webhook"
+ "github.com/bankdata/styra-controller/pkg/styra"
+ "github.com/go-logr/logr"
+ "github.com/pkg/errors"
+
+ k8serrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/runtime"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/log"
+
+ configv2alpha2 "github.com/bankdata/styra-controller/api/config/v2alpha2"
+ styrav1alpha1 "github.com/bankdata/styra-controller/api/styra/v1alpha1"
+)
+
+// LibraryReconciler reconciles a Library object
+type LibraryReconciler struct {
+ client.Client
+ Scheme *runtime.Scheme
+ Styra styra.ClientInterface
+ Config *configv2alpha2.ProjectConfig
+ WebhookClient webhook.Client
+}
+
+//+kubebuilder:rbac:groups=styra.bankdata.dk,resources=libraries,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=styra.bankdata.dk,resources=libraries/status,verbs=get;update;patch
+//+kubebuilder:rbac:groups=styra.bankdata.dk,resources=libraries/finalizers,verbs=update
+
+// Reconcile implements renconcile.Renconciler and has responsibility of
+// ensuring that the current state of the Library resource renconciled
+// towards the desired state.
+func (r *LibraryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ log := log.FromContext(ctx)
+ log.Info("Reconciliation begins")
+
+ var k8sLib styrav1alpha1.Library
+ if err := r.Get(ctx, req.NamespacedName, &k8sLib); err != nil {
+ if k8serrors.IsNotFound(err) {
+ log.Info("Could not find Library")
+ return ctrl.Result{}, nil
+ }
+ return ctrl.Result{}, errors.Wrap(err, "Could not get Library")
+ }
+
+ if !k8sLib.ObjectMeta.DeletionTimestamp.IsZero() {
+ log.Info(fmt.Sprintf("Library %s is under deletion in k8s. Ignoring it.", k8sLib.Spec.Name))
+ return ctrl.Result{}, nil
+ }
+
+ log.Info("Reconciling git credentials from default credentials")
+ gitCredential := r.Config.GetGitCredentialForRepo(k8sLib.Spec.SourceControl.LibraryOrigin.URL)
+ if gitCredential == nil {
+ log.Info("Could not find matching credentials", "url", k8sLib.Spec.SourceControl.LibraryOrigin.URL)
+ } else {
+ _, err := r.Styra.CreateUpdateSecret(
+ ctx,
+ path.Join("libraries", k8sLib.Spec.Name, "git"),
+ &styra.CreateUpdateSecretsRequest{
+ Name: gitCredential.User,
+ Secret: gitCredential.Password,
+ },
+ )
+ if err != nil {
+ return ctrl.Result{}, ctrlerr.Wrap(err, "Could not update Styra secret")
+ }
+ }
+
+ log.Info("Reconciling Library")
+ update := false
+ libResp, err := r.Styra.GetLibrary(ctx, k8sLib.Spec.Name)
+ if err != nil {
+ var httpErr *styra.HTTPError
+ if errors.As(err, &httpErr) {
+ if httpErr.StatusCode == http.StatusNotFound {
+ update = true
+ }
+ }
+ } else if r.needsUpdate(&k8sLib, libResp.LibraryEntityExpanded) {
+ update = true
+ }
+
+ if update {
+ log.Info("UpsertLibrary")
+ _, err := r.Styra.UpsertLibrary(ctx, k8sLib.Spec.Name, r.specToUpdate(&k8sLib))
+ if err != nil {
+ return ctrl.Result{}, ctrlerr.Wrap(err, "Could not upsert library")
+ }
+ }
+
+ if libResp == nil {
+ // This is often the case when a library has just been created.
+ log.Info(fmt.Sprint("Library ", k8sLib.Spec.Name, " could not be fetched. Requeueing..."))
+ return ctrl.Result{Requeue: true}, nil
+ }
+
+ if result, err := r.reconcileDatasources(ctx, log, &k8sLib, libResp.LibraryEntityExpanded); err != nil {
+ return result, ctrlerr.Wrap(err, "Could not reconcile library datasources")
+ }
+
+ if result, err := r.reconcileSubjects(ctx, log, &k8sLib); err != nil {
+ return result, err
+ }
+
+ log.Info("Reconciliation completed")
+ return ctrl.Result{}, nil
+}
+
+func (r *LibraryReconciler) specToUpdate(k8sLib *styrav1alpha1.Library) *styra.UpsertLibraryRequest {
+ if k8sLib == nil {
+ return nil
+ }
+ specs := k8sLib.Spec
+ k8sSourceControl := specs.SourceControl.LibraryOrigin
+
+ sourceControl := styra.LibraryGitRepoConfig{
+ Commit: k8sSourceControl.Commit,
+ Credentials: path.Join("libraries", specs.Name, "git"),
+ Path: k8sSourceControl.Path,
+ Reference: k8sSourceControl.Reference,
+ URL: k8sSourceControl.URL,
+ }
+
+ req := styra.UpsertLibraryRequest{
+ Description: specs.Description,
+ ReadOnly: true,
+ SourceControl: &styra.LibrarySourceControlConfig{LibraryOrigin: &sourceControl},
+ }
+
+ return &req
+}
+
+func (r *LibraryReconciler) needsUpdate(k8sLib *styrav1alpha1.Library, styraLib *styra.LibraryEntityExpanded) bool {
+ if k8sLib == nil {
+ return false
+ }
+
+ specs := k8sLib.Spec
+ if styraLib == nil ||
+ specs.Name != styraLib.ID ||
+ specs.Description != styraLib.Description ||
+ !styraLib.ReadOnly ||
+ !sameSourceControl(specs.SourceControl, styraLib.SourceControl) {
+ return true
+ }
+
+ if styraLib.SourceControl.LibraryOrigin.Credentials != path.Join("libraries", k8sLib.Spec.Name, "git") {
+ if r.Config.GetGitCredentialForRepo(specs.SourceControl.LibraryOrigin.URL) != nil {
+ return true
+ }
+ }
+
+ return false
+}
+
+func sameSourceControl(k8sLib *styrav1alpha1.SourceControl, styraLib *styra.LibrarySourceControlConfig) bool {
+ return k8sLib.LibraryOrigin.Path == styraLib.LibraryOrigin.Path &&
+ k8sLib.LibraryOrigin.Reference == styraLib.LibraryOrigin.Reference &&
+ k8sLib.LibraryOrigin.Commit == styraLib.LibraryOrigin.Commit &&
+ k8sLib.LibraryOrigin.URL == styraLib.LibraryOrigin.URL
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *LibraryReconciler) SetupWithManager(mgr ctrl.Manager) error {
+ return ctrl.NewControllerManagedBy(mgr).
+ For(&styrav1alpha1.Library{}).
+ Complete(r)
+}
+
+func (r *LibraryReconciler) reconcileDatasources(ctx context.Context, log logr.Logger, k8sLib *styrav1alpha1.Library,
+ styraLib *styra.LibraryEntityExpanded) (ctrl.Result, error) {
+ log.Info("Reconciling library datasources")
+
+ existingByID := map[string]styra.LibraryDatasourceConfig{}
+ if styraLib != nil {
+ for _, ds := range styraLib.DataSources {
+ if ds.ID != "" {
+ existingByID[ds.ID] = ds
+ }
+ }
+ }
+
+ expectedByID := map[string]styrav1alpha1.LibraryDatasource{}
+ if k8sLib.Spec.Datasources != nil {
+ for _, ds := range k8sLib.Spec.Datasources {
+ id := path.Join("libraries", k8sLib.Spec.Name, ds.Path)
+ expectedByID[id] = ds
+ }
+ }
+
+ // Create the missing datasources
+ for id := range expectedByID {
+ ds, exists := existingByID[id]
+ if !exists || ds.Category != "rest" {
+ log := log.WithValues("datasourceID", id)
+ log.Info("Creating or updating datasource")
+ request := &styra.UpsertDatasourceRequest{
+ Category: "rest",
+ Enabled: true,
+ }
+ _, err := r.Styra.UpsertDatasource(ctx, id, request)
+ if err != nil {
+ return ctrl.Result{}, ctrlerr.Wrap(err, "Could not create or update library datasource")
+ }
+
+ if r.WebhookClient != nil {
+ log.Info("Calling library datasource changed webhook")
+ if err := r.WebhookClient.DatasourceChanged(ctx, log, "jwt-library", ""); err != nil {
+ err = ctrlerr.Wrap(err, "could not call 'library datasource changed' webhook")
+ log.Error(err, err.Error())
+ }
+ }
+ }
+ }
+
+ // Delete the unexpected datasources
+ if styraLib == nil {
+ return ctrl.Result{}, nil
+ }
+
+ for _, ds := range styraLib.DataSources {
+ if ds.ID == "" {
+ log.Info("There exists some datasource without id?")
+ continue
+ }
+
+ if _, expected := expectedByID[ds.ID]; !expected {
+ log.Info("Deleting undeclared datasource")
+
+ if _, err := r.Styra.DeleteDatasource(ctx, ds.ID); err != nil {
+ return ctrl.Result{}, ctrlerr.Wrap(err, "Could not delete library datasource")
+ }
+ }
+ }
+
+ log.Info("Reconciled datasources")
+ return ctrl.Result{}, nil
+}
+
+func (r *LibraryReconciler) reconcileSubjects(
+ ctx context.Context,
+ log logr.Logger,
+ k8sLib *styrav1alpha1.Library,
+) (ctrl.Result, error) {
+ log.Info("Reconciling subjects for library")
+
+ // Make sure all users already exist in Styra, otherwise create them
+ if err := r.createUsersIfMissing(ctx, log, k8sLib); err != nil {
+ return ctrl.Result{}, err
+ }
+
+ // Delete Rolebindings to other roles then LibraryViewer
+ if err := r.deleteIncorrectRoleBindings(ctx, log, k8sLib); err != nil {
+ return ctrl.Result{}, err
+ }
+
+ // Create rolebinding for LibraryViewer if it is missing
+ if err := r.createRoleBindingIfMissing(ctx, log, styra.RoleLibraryViewer, k8sLib); err != nil {
+ return ctrl.Result{}, err
+ }
+
+ // Make sure the LibraryViewer rolebinding contains correct subjects
+ if err := r.updateRoleBindingIfNeeded(ctx, log, styra.RoleLibraryViewer, k8sLib); err != nil {
+ return ctrl.Result{}, err
+ }
+
+ return ctrl.Result{}, nil
+}
+
+func (r *LibraryReconciler) createUsersIfMissing(
+ ctx context.Context,
+ log logr.Logger,
+ k8sLib *styrav1alpha1.Library) error {
+ for _, subject := range k8sLib.Spec.Subjects {
+ if subject.IsUser() {
+ log := log.WithValues("user", subject.Name)
+ log.Info("Checking if user exists")
+ res, err := r.Styra.GetUser(ctx, subject.Name)
+ if err != nil {
+ return ctrlerr.Wrap(err, "Could not get user from Styra API")
+ }
+
+ if res.StatusCode == http.StatusNotFound {
+ log.Info("User does not exist in styra. Creating...")
+ _, err := r.Styra.CreateInvitation(ctx, false, subject.Name)
+ if err != nil {
+ return ctrlerr.Wrap(err, "Could not create user in Styra")
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func (r *LibraryReconciler) deleteIncorrectRoleBindings(
+ ctx context.Context,
+ log logr.Logger,
+ k8sLib *styrav1alpha1.Library) error {
+ log.Info("Deleting Rolebindings to roles that are not LibraryViewers")
+ res, err := r.Styra.ListRoleBindingsV2(ctx, &styra.ListRoleBindingsV2Params{
+ ResourceKind: styra.RoleBindingKindLibrary,
+ ResourceID: k8sLib.Spec.Name,
+ })
+ if err != nil {
+ return ctrlerr.Wrap(err, "Could not get rolebindings for Library in Styra")
+ }
+
+ for _, styraRB := range res.Rolebindings {
+ if styraRB.RoleID != styra.Role("LibraryViewer") {
+ if _, err := r.Styra.DeleteRoleBindingV2(ctx, styraRB.ID); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func (r *LibraryReconciler) createRoleBindingIfMissing(
+ ctx context.Context,
+ log logr.Logger,
+ role styra.Role,
+ k8sLib *styrav1alpha1.Library) error {
+ res, err := r.Styra.ListRoleBindingsV2(ctx, &styra.ListRoleBindingsV2Params{
+ ResourceKind: styra.RoleBindingKindLibrary,
+ ResourceID: k8sLib.Spec.Name,
+ })
+ if err != nil {
+ return ctrlerr.Wrap(err, "Could not get rolebindings for Library in Styra")
+ }
+
+ if len(res.Rolebindings) == 0 {
+ log.Info(fmt.Sprintf("No rolebindings exist for the %s Library. Creating rolebinding.", k8sLib.Spec.Name))
+
+ k8sRolebindingSubjects := createLibraryRolebindingSubjects(
+ k8sLib.Spec.Subjects,
+ r.Config.SSO,
+ )
+
+ err := r.createRoleBinding(ctx, log, k8sLib, role, k8sRolebindingSubjects)
+ if err != nil {
+ return ctrlerr.Wrap(err, "Could not create rolebinding in Styra")
+ }
+ }
+ return nil
+}
+
+func (r *LibraryReconciler) updateRoleBindingIfNeeded(
+ ctx context.Context,
+ log logr.Logger,
+ role styra.Role,
+ k8sLib *styrav1alpha1.Library) error {
+ res, err := r.Styra.ListRoleBindingsV2(ctx, &styra.ListRoleBindingsV2Params{
+ ResourceKind: styra.RoleBindingKindLibrary,
+ ResourceID: k8sLib.Spec.Name,
+ })
+ if err != nil {
+ return ctrlerr.Wrap(err, "Could not get rolebindings for Library in Styra")
+ }
+
+ k8sRolebindingSubjects := createLibraryRolebindingSubjects(
+ k8sLib.Spec.Subjects,
+ r.Config.SSO,
+ )
+
+ // res.Rolebindings should only contain one rolebinding, for the "LibraryViewer" role
+ // Also, should contain nothing else after deleteIncorrectRoleBindings
+ for _, rb := range res.Rolebindings {
+ if rb.RoleID == role {
+ if !styra.SubjectsAreEqual(k8sRolebindingSubjects, rb.Subjects) {
+ if err := r.updateRoleBindingSubjects(ctx, log, rb.ID, k8sRolebindingSubjects); err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+func (r *LibraryReconciler) updateRoleBindingSubjects(
+ ctx context.Context,
+ log logr.Logger,
+ roleBindingID string,
+ subjects []*styra.Subject,
+) error {
+ log.Info("Updating rolebinding")
+
+ _, err := r.Styra.UpdateRoleBindingSubjects(
+ ctx,
+ roleBindingID,
+ &styra.UpdateRoleBindingSubjectsRequest{Subjects: subjects},
+ )
+ if err != nil {
+ return ctrlerr.Wrap(err, "Could not update Styra role binding")
+ }
+
+ log.Info("Updated rolebinding")
+ return nil
+}
+
+func createLibraryRolebindingSubjects(
+ subjects []styrav1alpha1.LibrarySubject,
+ sso *configv2alpha2.SSOConfig,
+) []*styra.Subject {
+ styraSubjectsByUserID := map[string]struct{}{}
+ styraSubjectsByClaimValue := map[string]struct{}{}
+
+ styraSubjects := []*styra.Subject{}
+
+ for _, subject := range subjects {
+ if subject.IsUser() {
+ if _, ok := styraSubjectsByUserID[subject.Name]; ok {
+ continue
+ }
+ styraSubjects = append(styraSubjects, &styra.Subject{
+ Kind: styra.SubjectKindUser,
+ ID: subject.Name,
+ })
+ styraSubjectsByUserID[subject.Name] = struct{}{}
+
+ } else if subject.Kind == styrav1alpha1.LibrarySubjectKindGroup && sso != nil {
+ if _, ok := styraSubjectsByClaimValue[subject.Name]; ok {
+ continue
+ }
+
+ styraSubjects = append(styraSubjects, &styra.Subject{
+ Kind: styra.SubjectKindClaim,
+ ClaimConfig: &styra.ClaimConfig{
+ IdentityProvider: sso.IdentityProvider,
+ Key: sso.JWTGroupsClaim,
+ Value: subject.Name,
+ },
+ })
+ styraSubjectsByClaimValue[subject.Name] = struct{}{}
+ }
+ }
+
+ return styraSubjects
+}
+
+func (r *LibraryReconciler) createRoleBinding(
+ ctx context.Context,
+ log logr.Logger,
+ k8sLib *styrav1alpha1.Library,
+ role styra.Role,
+ subjects []*styra.Subject,
+) error {
+ log.Info("Creating rolebinding")
+
+ if _, err := r.Styra.CreateRoleBinding(ctx, &styra.CreateRoleBindingRequest{
+ ResourceFilter: &styra.ResourceFilter{
+ ID: k8sLib.Spec.Name,
+ Kind: styra.RoleBindingKindLibrary,
+ },
+ RoleID: role,
+ Subjects: subjects,
+ }); err != nil {
+ return ctrlerr.Wrap(err, "Could not create rolebinding")
+ }
+
+ log.Info("Created rolebinding")
+ return nil
+}
diff --git a/pkg/styra/authz.go b/pkg/styra/authz.go
index b23dd08..15e9772 100644
--- a/pkg/styra/authz.go
+++ b/pkg/styra/authz.go
@@ -40,6 +40,9 @@ const (
// RoleSystemPolicyEditor is the Styra SystemPolicyEditor role.
RoleSystemPolicyEditor Role = "SystemPolicyEditor"
+
+ // RoleLibraryViewer is the Styra LibraryViewer role.
+ RoleLibraryViewer Role = "LibraryViewer"
)
// RoleBindingKind is the kind of the role binding.
@@ -49,6 +52,9 @@ const (
// RoleBindingKindSystem is a RoleBindingKind used when the role is for a
// System.
RoleBindingKindSystem RoleBindingKind = "system"
+ // RoleBindingKindLibrary is a RoleBindingKind used when the role is for a
+ // Library.
+ RoleBindingKindLibrary RoleBindingKind = "library"
)
// SubjectKind is the kind of a subject.
diff --git a/pkg/styra/client.go b/pkg/styra/client.go
index 9dc725e..cc2456c 100644
--- a/pkg/styra/client.go
+++ b/pkg/styra/client.go
@@ -62,6 +62,9 @@ type ClientInterface interface {
DeleteDatasource(ctx context.Context, id string) (*DeleteDatasourceResponse, error)
+ GetLibrary(ctx context.Context, id string) (*GetLibraryResponse, error)
+ UpsertLibrary(ctx context.Context, id string, request *UpsertLibraryRequest) (*UpsertLibraryResponse, error)
+
UpdateSystem(ctx context.Context, id string, request *UpdateSystemRequest) (*UpdateSystemResponse, error)
DeleteSystem(ctx context.Context, id string) (*DeleteSystemResponse, error)
diff --git a/pkg/styra/library.go b/pkg/styra/library.go
new file mode 100644
index 0000000..31d362e
--- /dev/null
+++ b/pkg/styra/library.go
@@ -0,0 +1,145 @@
+/*
+Copyright (C) 2023 Bankdata (bankdata@bankdata.dk)
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package styra
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/pkg/errors"
+)
+
+const (
+ endpointV1Libraries = "/v1/libraries"
+)
+
+type getLibraryJSONResponse struct {
+ Result *LibraryEntityExpanded `json:"result"`
+}
+
+// GetLibraryResponse is the response type for calls to the
+// GET /v1/libraries/{id} endpoint in the Styra API.
+type GetLibraryResponse struct {
+ Statuscode int
+ Body []byte
+ LibraryEntityExpanded *LibraryEntityExpanded
+}
+
+// LibraryEntityExpanded is the type that defines of a Library
+type LibraryEntityExpanded struct {
+ DataSources []LibraryDatasourceConfig `json:"datasources"`
+ Description string `json:"description"`
+ ID string `json:"id"`
+ ReadOnly bool `json:"read_only"`
+ SourceControl *LibrarySourceControlConfig `json:"source_control"`
+}
+
+// LibraryDatasourceConfig defines metadata of a datasource
+type LibraryDatasourceConfig struct {
+ Category string `json:"category"`
+ ID string `json:"id"`
+}
+
+// LibrarySourceControlConfig is a struct from styra where we only use a single field
+// but kept for clarity when comparing to the API
+type LibrarySourceControlConfig struct {
+ LibraryOrigin *LibraryGitRepoConfig `json:"library_origin"`
+}
+
+// LibraryGitRepoConfig defines the Git configurations a library can be defined by
+type LibraryGitRepoConfig struct {
+ Commit string `json:"commit"`
+ Credentials string `json:"credentials"`
+ Path string `json:"path"`
+ Reference string `json:"reference"`
+ URL string `json:"url"`
+}
+
+// UpsertLibraryRequest is the request body for the
+// PUT /v1/libraries/{id} endpoint in the Styra API.
+type UpsertLibraryRequest struct {
+ Description string `json:"description"`
+ ReadOnly bool `json:"read_only"`
+ SourceControl *LibrarySourceControlConfig `json:"source_control"`
+}
+
+// UpsertLibraryResponse is the response body for the
+// PUT /v1/libraries/{id} endpoint in the Styra API.
+type UpsertLibraryResponse struct {
+ StatusCode int
+ Body []byte
+}
+
+// GetLibrary calls the GET /v1/libraries/{id} endpoint in the
+// Styra API.
+func (c *Client) GetLibrary(ctx context.Context, id string) (*GetLibraryResponse, error) {
+ res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("%s/%s", endpointV1Libraries, id), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ body, err := io.ReadAll(res.Body)
+ if err != nil {
+ return nil, errors.Wrap(err, "could not read body")
+ }
+
+ if res.StatusCode != http.StatusOK {
+ err := NewHTTPError(res.StatusCode, string(body))
+ return nil, err
+ }
+
+ var jsonRes getLibraryJSONResponse
+ if err := json.Unmarshal(body, &jsonRes); err != nil {
+ return nil, errors.Wrap(err, "could not unmarshal body")
+ }
+
+ return &GetLibraryResponse{
+ Statuscode: res.StatusCode,
+ Body: body,
+ LibraryEntityExpanded: jsonRes.Result,
+ }, nil
+}
+
+// UpsertLibrary calls the PUT /v1/libraries/{id} endpoint in the
+// Styra API.
+func (c *Client) UpsertLibrary(ctx context.Context, id string, request *UpsertLibraryRequest,
+) (*UpsertLibraryResponse, error) {
+ res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("%s/%s", endpointV1Libraries, id), request)
+ if err != nil {
+ return nil, err
+ }
+
+ body, err := io.ReadAll(res.Body)
+ if err != nil {
+ return nil, errors.Wrap(err, "could not read body")
+ }
+
+ if res.StatusCode != http.StatusOK {
+ err := NewHTTPError(res.StatusCode, string(body))
+ return nil, err
+ }
+
+ resp := UpsertLibraryResponse{
+ StatusCode: res.StatusCode,
+ Body: body,
+ }
+
+ return &resp, nil
+}
diff --git a/pkg/styra/library_test.go b/pkg/styra/library_test.go
new file mode 100644
index 0000000..dd18d20
--- /dev/null
+++ b/pkg/styra/library_test.go
@@ -0,0 +1,245 @@
+/*
+Copyright (C) 2023 Bankdata (bankdata@bankdata.dk)
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package styra_test
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "io"
+ "net/http"
+
+ ginkgo "github.com/onsi/ginkgo/v2"
+ gomega "github.com/onsi/gomega"
+
+ "github.com/bankdata/styra-controller/pkg/styra"
+)
+
+var _ = ginkgo.Describe("GetLibrary", func() {
+ type test struct {
+ libraryID string
+ responseCode int
+ responseBody string
+ expectedLibraryEntityExpanded *styra.LibraryEntityExpanded
+ expectStyraErr bool
+ }
+
+ ginkgo.DescribeTable("GetLibrary", func(test test) {
+ c := newTestClient(func(r *http.Request) *http.Response {
+ gomega.Expect(r.Method).To(gomega.Equal(http.MethodGet))
+ gomega.Expect(r.URL.String()).To(gomega.Equal("http://test.com/v1/libraries/" + test.libraryID))
+
+ return &http.Response{
+ Header: make(http.Header),
+ StatusCode: test.responseCode,
+ Body: io.NopCloser(bytes.NewBufferString(test.responseBody)),
+ }
+ })
+
+ res, err := c.GetLibrary(context.Background(), test.libraryID)
+ if test.expectStyraErr {
+ gomega.Expect(res).To(gomega.BeNil())
+ target := &styra.HTTPError{}
+ gomega.Expect(errors.As(err, &target)).To(gomega.BeTrue())
+ } else {
+ gomega.Expect(err).ToNot(gomega.HaveOccurred())
+ gomega.Expect(res.Statuscode).To(gomega.Equal(test.responseCode))
+ gomega.Expect(res.LibraryEntityExpanded).To(gomega.Equal(test.expectedLibraryEntityExpanded))
+ }
+ },
+
+ ginkgo.Entry("happy path", test{
+ libraryID: "test1",
+ responseCode: http.StatusOK,
+ responseBody: `
+ {
+ "result": {
+ "id": "test1",
+ "description": "test2",
+ "read_only": true,
+ "source_control": {
+ "use_workspace_settings": false,
+ "origin": {
+ "url": "https://github.com/test.git",
+ "reference": "refs/heads/master",
+ "commit": "",
+ "credentials": "gitCreds",
+ "path": ""
+ },
+ "library_origin": {
+ "url": "https://github.com/test.git",
+ "reference": "refs/heads/master",
+ "commit": "",
+ "credentials": "path/to/creds",
+ "path": ""
+ }
+ },
+ "policies": [],
+ "datasources": []
+ }
+ }
+ `,
+ expectedLibraryEntityExpanded: &styra.LibraryEntityExpanded{
+ DataSources: []styra.LibraryDatasourceConfig{},
+ Description: "test2",
+ ID: "test1",
+ ReadOnly: true,
+ SourceControl: &styra.LibrarySourceControlConfig{
+ LibraryOrigin: &styra.LibraryGitRepoConfig{
+ Commit: "",
+ Credentials: "path/to/creds",
+ Path: "",
+ Reference: "refs/heads/master",
+ URL: "https://github.com/test.git",
+ },
+ },
+ },
+ }),
+
+ ginkgo.Entry("unexpected status code", test{
+ libraryID: "test",
+ responseCode: http.StatusInternalServerError,
+ expectStyraErr: true,
+ }),
+ )
+})
+
+var _ = ginkgo.Describe("UpsertLibrary", func() {
+
+ type test struct {
+ libraryID string
+ upsertLibraryRequest *styra.UpsertLibraryRequest
+ responseCode int
+ responseBody string
+ expectedBody []byte
+ expectStyraErr bool
+ }
+
+ ginkgo.DescribeTable("UpsertLibrary", func(test test) {
+ c := newTestClient(func(r *http.Request) *http.Response {
+ // First make sure the body is readable
+ bs, err := io.ReadAll(r.Body)
+ gomega.Expect(err).NotTo(gomega.HaveOccurred())
+
+ // Then make sure the body is encoded correctly
+ var b bytes.Buffer
+ gomega.Expect(json.NewEncoder(&b).Encode(test.upsertLibraryRequest)).To(gomega.Succeed())
+ gomega.Expect(bs).To(gomega.Equal(b.Bytes()))
+
+ // Then ensure the correct http request is made
+ gomega.Expect(r.Method).To(gomega.Equal(http.MethodPut))
+ gomega.Expect(r.URL.String()).To(gomega.Equal("http://test.com/v1/libraries/" + test.libraryID))
+
+ return &http.Response{
+ Header: make(http.Header),
+ StatusCode: test.responseCode,
+ Body: io.NopCloser(bytes.NewBufferString(test.responseBody)),
+ }
+ })
+
+ res, err := c.UpsertLibrary(context.Background(), test.libraryID, test.upsertLibraryRequest)
+ if test.expectStyraErr {
+ gomega.Expect(res).To(gomega.BeNil())
+ target := &styra.HTTPError{}
+ gomega.Expect(errors.As(err, &target)).To(gomega.BeTrue())
+ } else {
+ gomega.Expect(err).ToNot(gomega.HaveOccurred())
+ gomega.Expect(res.StatusCode).To(gomega.Equal(test.responseCode))
+ gomega.Expect(res.Body).To(gomega.Equal(test.expectedBody))
+ }
+ },
+
+ ginkgo.Entry("happy", test{
+ libraryID: "test",
+ responseCode: http.StatusOK,
+ responseBody: `expected response from styra api`,
+ upsertLibraryRequest: &styra.UpsertLibraryRequest{
+ Description: "test2",
+ ReadOnly: true,
+ SourceControl: &styra.LibrarySourceControlConfig{
+ LibraryOrigin: &styra.LibraryGitRepoConfig{
+ Commit: "",
+ Credentials: "path/to/creds",
+ Path: "",
+ Reference: "refs/heads/master",
+ URL: "https://github.com/test.git",
+ },
+ },
+ },
+ expectedBody: []byte(`expected response from styra api`)},
+ ),
+
+ ginkgo.Entry("sad", test{
+ libraryID: "test",
+ responseCode: http.StatusInternalServerError,
+ expectStyraErr: true,
+ }),
+ )
+})
+
+var _ = ginkgo.Describe("DeleteDatasource", func() {
+
+ type test struct {
+ datasourceID string
+ responseCode int
+ responseBody string
+ expectedBody []byte
+ expectStyraErr bool
+ }
+
+ ginkgo.DescribeTable("DeleteDatasource", func(test test) {
+ c := newTestClient(func(r *http.Request) *http.Response {
+ bs, err := io.ReadAll(r.Body)
+ gomega.Expect(err).NotTo(gomega.HaveOccurred())
+ gomega.Expect(bs).To(gomega.Equal([]byte("")))
+ gomega.Expect(r.Method).To(gomega.Equal(http.MethodDelete))
+ gomega.Expect(r.URL.String()).To(gomega.Equal("http://test.com/v1/datasources/" + test.datasourceID))
+
+ return &http.Response{
+ Header: make(http.Header),
+ StatusCode: test.responseCode,
+ Body: io.NopCloser(bytes.NewBufferString(test.responseBody)),
+ }
+ })
+
+ res, err := c.DeleteDatasource(context.Background(), test.datasourceID)
+ if test.expectStyraErr {
+ gomega.Expect(res).To(gomega.BeNil())
+ target := &styra.HTTPError{}
+ gomega.Expect(errors.As(err, &target)).To(gomega.BeTrue())
+ } else {
+ gomega.Expect(err).ToNot(gomega.HaveOccurred())
+ gomega.Expect(res.StatusCode).To(gomega.Equal(test.responseCode))
+ gomega.Expect(res.Body).To(gomega.Equal(test.expectedBody))
+ }
+ },
+
+ ginkgo.Entry("something", test{
+ datasourceID: "datasourceID",
+ responseCode: http.StatusOK,
+ responseBody: `expected response from styra api`,
+ expectedBody: []byte(`expected response from styra api`)},
+ ),
+
+ ginkgo.Entry("styra http error", test{
+ datasourceID: "datasourceID",
+ responseCode: http.StatusInternalServerError,
+ expectStyraErr: true,
+ }),
+ )
+})
diff --git a/pkg/styra/mocks/client_interface.go b/pkg/styra/mocks/client_interface.go
index 35b7d90..3343166 100644
--- a/pkg/styra/mocks/client_interface.go
+++ b/pkg/styra/mocks/client_interface.go
@@ -248,6 +248,32 @@ func (_m *ClientInterface) GetDatasource(ctx context.Context, id string) (*styra
return r0, r1
}
+// GetLibrary provides a mock function with given fields: ctx, id
+func (_m *ClientInterface) GetLibrary(ctx context.Context, id string) (*styra.GetLibraryResponse, error) {
+ ret := _m.Called(ctx, id)
+
+ var r0 *styra.GetLibraryResponse
+ var r1 error
+ if rf, ok := ret.Get(0).(func(context.Context, string) (*styra.GetLibraryResponse, error)); ok {
+ return rf(ctx, id)
+ }
+ if rf, ok := ret.Get(0).(func(context.Context, string) *styra.GetLibraryResponse); ok {
+ r0 = rf(ctx, id)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*styra.GetLibraryResponse)
+ }
+ }
+
+ if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+ r1 = rf(ctx, id)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
// GetOPAConfig provides a mock function with given fields: ctx, systemID
func (_m *ClientInterface) GetOPAConfig(ctx context.Context, systemID string) (styra.OPAConfig, error) {
ret := _m.Called(ctx, systemID)
@@ -428,6 +454,32 @@ func (_m *ClientInterface) UpsertDatasource(ctx context.Context, id string, requ
return r0, r1
}
+// UpsertLibrary provides a mock function with given fields: ctx, id, request
+func (_m *ClientInterface) UpsertLibrary(ctx context.Context, id string, request *styra.UpsertLibraryRequest) (*styra.UpsertLibraryResponse, error) {
+ ret := _m.Called(ctx, id, request)
+
+ var r0 *styra.UpsertLibraryResponse
+ var r1 error
+ if rf, ok := ret.Get(0).(func(context.Context, string, *styra.UpsertLibraryRequest) (*styra.UpsertLibraryResponse, error)); ok {
+ return rf(ctx, id, request)
+ }
+ if rf, ok := ret.Get(0).(func(context.Context, string, *styra.UpsertLibraryRequest) *styra.UpsertLibraryResponse); ok {
+ r0 = rf(ctx, id, request)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*styra.UpsertLibraryResponse)
+ }
+ }
+
+ if rf, ok := ret.Get(1).(func(context.Context, string, *styra.UpsertLibraryRequest) error); ok {
+ r1 = rf(ctx, id, request)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
// VerifyGitConfiguration provides a mock function with given fields: ctx, request
func (_m *ClientInterface) VerifyGitConfiguration(ctx context.Context, request *styra.VerfiyGitConfigRequest) (*styra.VerfiyGitConfigResponse, error) {
ret := _m.Called(ctx, request)
diff --git a/test/integration/controller/controller_suite_test.go b/test/integration/controller/controller_suite_test.go
index 4dfc7c3..ed388ba 100644
--- a/test/integration/controller/controller_suite_test.go
+++ b/test/integration/controller/controller_suite_test.go
@@ -141,6 +141,24 @@ var _ = ginkgo.BeforeSuite(func() {
err = globalDatasourceReconciler.SetupWithManager(k8sManager)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
+ libraryReconciler := &styractrls.LibraryReconciler{
+ Config: &configv2alpha2.ProjectConfig{
+ SSO: &configv2alpha2.SSOConfig{
+ IdentityProvider: "AzureAD Bankdata",
+ JWTGroupsClaim: "groups",
+ },
+ GitCredentials: []*configv2alpha2.GitCredential{
+ {User: "test-user", Password: "test-secret"},
+ },
+ },
+ Client: k8sClient,
+ Styra: styraClientMock,
+ WebhookClient: webhookMock,
+ }
+
+ err = libraryReconciler.SetupWithManager(k8sManager)
+ gomega.Expect(err).NotTo(gomega.HaveOccurred())
+
managerCtx, managerCancel = context.WithCancel(context.Background())
go func() {
defer ginkgo.GinkgoRecover()
diff --git a/test/integration/controller/globaldatasource_controller_test.go b/test/integration/controller/globaldatasource_controller_test.go
index 50023f7..b2eaa1a 100644
--- a/test/integration/controller/globaldatasource_controller_test.go
+++ b/test/integration/controller/globaldatasource_controller_test.go
@@ -1,3 +1,19 @@
+/*
+Copyright (C) 2023 Bankdata (bankdata@bankdata.dk)
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
package styra
import (
@@ -17,164 +33,164 @@ import (
"github.com/bankdata/styra-controller/pkg/styra"
)
-var _ = ginkgo.Describe("GlobalDatasourceReconciler", func() {
- ginkgo.Describe("Reconcile", ginkgo.Label("integration"), func() {
- ginkgo.It("reconciles GlobalDatasource", func() {
- key := types.NamespacedName{Name: uuid.NewString()}
-
- toCreate := &styrav1alpha1.GlobalDatasource{
- ObjectMeta: metav1.ObjectMeta{
- Name: key.Name,
- },
- Spec: styrav1alpha1.GlobalDatasourceSpec{
- Name: key.Name,
- Category: styrav1alpha1.GlobalDatasourceCategoryGitRego,
- URL: "http://test.com/test.git",
- },
+var _ = ginkgo.Describe("GlobalDatasourceReconciler.Reconcile", ginkgo.Label("integration"), func() {
+ ginkgo.It("reconciles GlobalDatasource", func() {
+ key := types.NamespacedName{Name: uuid.NewString()}
+
+ toCreate := &styrav1alpha1.GlobalDatasource{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: key.Name,
+ },
+ Spec: styrav1alpha1.GlobalDatasourceSpec{
+ Name: key.Name,
+ Category: styrav1alpha1.GlobalDatasourceCategoryGitRego,
+ URL: "http://test.com/test.git",
+ },
+ }
+
+ ctx := context.Background()
+
+ ginkgo.By("creating the datasource")
+
+ styraClientMock.On(
+ "CreateUpdateSecret",
+ mock.Anything,
+ path.Join("libraries/global", key.Name, "git"),
+ &styra.CreateUpdateSecretsRequest{
+ Name: "test-user",
+ Secret: "test-secret",
+ },
+ ).Return(&styra.CreateUpdateSecretResponse{
+ StatusCode: http.StatusOK,
+ }, nil)
+
+ styraClientMock.On(
+ "GetDatasource",
+ mock.Anything,
+ path.Join("global", key.Name),
+ ).Return(
+ nil, &styra.HTTPError{
+ StatusCode: http.StatusNotFound,
+ },
+ )
+
+ styraClientMock.On(
+ "UpsertDatasource",
+ mock.Anything,
+ path.Join("global", key.Name),
+ &styra.UpsertDatasourceRequest{
+ Category: "git/rego",
+ Enabled: true,
+ Credentials: path.Join("libraries/global", key.Name, "git"),
+ URL: "http://test.com/test.git",
+ },
+ ).Return(&styra.UpsertDatasourceResponse{}, nil)
+
+ gomega.Ω(k8sClient.Create(ctx, toCreate)).To(gomega.Succeed())
+
+ gomega.Eventually(func() bool {
+ var gds styrav1alpha1.GlobalDatasource
+ if err := k8sClient.Get(ctx, key, &gds); err != nil {
+ return false
}
+ return true
+ }, timeout, interval).Should(gomega.BeTrue())
+
+ gomega.Eventually(func() bool {
+ var (
+ createUpdateSecret int
+ getDatasource int
+ upsertDatasource int
+ )
+ for _, call := range styraClientMock.Calls {
+ switch call.Method {
+ case "CreateUpdateSecret":
+ createUpdateSecret++
+ case "GetDatasource":
+ getDatasource++
+ case "UpsertDatasource":
+ upsertDatasource++
+ }
+ }
+ return createUpdateSecret == 1 &&
+ getDatasource == 1 &&
+ upsertDatasource == 1
+ }, timeout, interval).Should(gomega.BeTrue())
- ctx := context.Background()
+ styraClientMock.AssertExpectations(ginkgo.GinkgoT())
+ resetMock(&styraClientMock.Mock)
- ginkgo.By("creating the datasource")
+ ginkgo.By("using a git credential from a secret")
- styraClientMock.On(
- "CreateUpdateSecret",
- mock.Anything,
- path.Join("libraries/global", key.Name, "git"),
- &styra.CreateUpdateSecretsRequest{
- Name: "test-user",
- Secret: "test-secret",
- },
- ).Return(&styra.CreateUpdateSecretResponse{
+ gomega.Ω(k8sClient.Create(ctx, &v1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: key.Name,
+ Namespace: "default",
+ },
+ Data: map[string][]byte{
+ "name": []byte("test-user-2"),
+ "secret": []byte("test-secret-2"),
+ },
+ })).To(gomega.Succeed())
+
+ gomega.Eventually(func() bool {
+ var s v1.Secret
+ return k8sClient.Get(ctx, types.NamespacedName{Name: key.Name, Namespace: "default"}, &s) == nil
+ }, timeout, interval).Should(gomega.BeTrue())
+
+ styraClientMock.On(
+ "CreateUpdateSecret",
+ mock.Anything,
+ path.Join("libraries/global", key.Name, "git"),
+ &styra.CreateUpdateSecretsRequest{
+ Name: "test-user-2",
+ Secret: "test-secret-2",
+ },
+ ).Return(&styra.CreateUpdateSecretResponse{
+ StatusCode: http.StatusOK,
+ }, nil)
+
+ styraClientMock.On(
+ "GetDatasource",
+ mock.Anything,
+ path.Join("global", key.Name),
+ ).Return(
+ &styra.GetDatasourceResponse{
StatusCode: http.StatusOK,
- }, nil)
-
- styraClientMock.On(
- "GetDatasource",
- mock.Anything,
- path.Join("global", key.Name),
- ).Return(
- nil, &styra.HTTPError{
- StatusCode: http.StatusNotFound,
- },
- )
-
- styraClientMock.On(
- "UpsertDatasource",
- mock.Anything,
- path.Join("global", key.Name),
- &styra.UpsertDatasourceRequest{
+ DatasourceConfig: &styra.DatasourceConfig{
Category: "git/rego",
Enabled: true,
Credentials: path.Join("libraries/global", key.Name, "git"),
URL: "http://test.com/test.git",
},
- ).Return(&styra.UpsertDatasourceResponse{}, nil)
-
- gomega.Ω(k8sClient.Create(ctx, toCreate)).To(gomega.Succeed())
+ }, nil,
+ )
- gomega.Eventually(func() bool {
- var gds styrav1alpha1.GlobalDatasource
- if err := k8sClient.Get(ctx, key, &gds); err != nil {
- return false
- }
- return true
- }, timeout, interval).Should(gomega.BeTrue())
-
- gomega.Eventually(func() bool {
- var (
- createUpdateSecret int
- getDatasource int
- upsertDatasource int
- )
- for _, call := range styraClientMock.Calls {
- switch call.Method {
- case "CreateUpdateSecret":
- createUpdateSecret++
- case "GetDatasource":
- getDatasource++
- case "UpsertDatasource":
- upsertDatasource++
- }
- }
- return createUpdateSecret == 1 &&
- getDatasource == 1 &&
- upsertDatasource == 1
- }, timeout, interval).Should(gomega.BeTrue())
+ toCreate.Spec.CredentialsSecretRef = &styrav1alpha1.GlobalDatasourceSecretRef{
+ Name: key.Name,
+ Namespace: "default",
+ }
- styraClientMock.AssertExpectations(ginkgo.GinkgoT())
- resetMock(&styraClientMock.Mock)
+ gomega.Ω(k8sClient.Update(ctx, toCreate)).To(gomega.Succeed())
- ginkgo.By("using a git credential from a secret")
-
- gomega.Ω(k8sClient.Create(ctx, &v1.Secret{
- ObjectMeta: metav1.ObjectMeta{
- Name: key.Name,
- Namespace: "default",
- },
- Data: map[string][]byte{
- "name": []byte("test-user-2"),
- "secret": []byte("test-secret-2"),
- },
- })).To(gomega.Succeed())
-
- gomega.Eventually(func() bool {
- var s v1.Secret
- return k8sClient.Get(ctx, types.NamespacedName{Name: key.Name, Namespace: "default"}, &s) == nil
- }, timeout, interval).Should(gomega.BeTrue())
-
- styraClientMock.On(
- "CreateUpdateSecret",
- mock.Anything,
- path.Join("libraries/global", key.Name, "git"),
- &styra.CreateUpdateSecretsRequest{
- Name: "test-user-2",
- Secret: "test-secret-2",
- },
- ).Return(&styra.CreateUpdateSecretResponse{
- StatusCode: http.StatusOK,
- }, nil)
-
- styraClientMock.On(
- "GetDatasource",
- mock.Anything,
- path.Join("global", key.Name),
- ).Return(
- &styra.GetDatasourceResponse{
- StatusCode: http.StatusOK,
- DatasourceConfig: &styra.DatasourceConfig{
- Category: "git/rego",
- Enabled: true,
- Credentials: path.Join("libraries/global", key.Name, "git"),
- URL: "http://test.com/test.git",
- },
- }, nil,
+ gomega.Eventually(func() bool {
+ var (
+ createUpdateSecret int
+ getDatasource int
)
-
- toCreate.Spec.CredentialsSecretRef = &styrav1alpha1.GlobalDatasourceSecretRef{
- Name: key.Name,
- Namespace: "default",
+ for _, call := range styraClientMock.Calls {
+ switch call.Method {
+ case "CreateUpdateSecret":
+ createUpdateSecret++
+ case "GetDatasource":
+ getDatasource++
+ }
}
+ return createUpdateSecret == 1 && getDatasource == 1
+ }, timeout, interval).Should(gomega.BeTrue())
- gomega.Ω(k8sClient.Update(ctx, toCreate)).To(gomega.Succeed())
-
- gomega.Eventually(func() bool {
- var (
- createUpdateSecret int
- getDatasource int
- )
- for _, call := range styraClientMock.Calls {
- switch call.Method {
- case "CreateUpdateSecret":
- createUpdateSecret++
- case "GetDatasource":
- getDatasource++
- }
- }
- return createUpdateSecret == 1 && getDatasource == 1
- }, timeout, interval).Should(gomega.BeTrue())
+ styraClientMock.AssertExpectations(ginkgo.GinkgoT())
- styraClientMock.AssertExpectations(ginkgo.GinkgoT())
- })
+ resetMock(&styraClientMock.Mock)
})
})
diff --git a/test/integration/controller/library_controller_test.go b/test/integration/controller/library_controller_test.go
new file mode 100644
index 0000000..49577ac
--- /dev/null
+++ b/test/integration/controller/library_controller_test.go
@@ -0,0 +1,319 @@
+/*
+Copyright (C) 2023 Bankdata (bankdata@bankdata.dk)
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package styra
+
+import (
+ "context"
+ "net/http"
+ "path"
+
+ ginkgo "github.com/onsi/ginkgo/v2"
+ gomega "github.com/onsi/gomega"
+ "github.com/stretchr/testify/mock"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+
+ styrav1alpha1 "github.com/bankdata/styra-controller/api/styra/v1alpha1"
+ "github.com/bankdata/styra-controller/pkg/styra"
+)
+
+var _ = ginkgo.Describe("LibraryReconciler.Reconcile", ginkgo.Label("integration"), func() {
+ ginkgo.It("reconciles Library", func() {
+ key := types.NamespacedName{Name: "uuidewtring", Namespace: "default"}
+ toCreate := &styrav1alpha1.Library{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: key.Name,
+ Namespace: key.Namespace,
+ },
+ Spec: styrav1alpha1.LibrarySpec{
+ Name: key.Name,
+ Subjects: []styrav1alpha1.LibrarySubject{
+ {
+ Kind: styrav1alpha1.LibrarySubjectKindUser,
+ Name: "user1@mail.com",
+ },
+ {
+ // kind
+ Name: "user2@mail.com",
+ },
+ {
+ Kind: styrav1alpha1.LibrarySubjectKindGroup,
+ Name: "testGroup",
+ },
+ },
+ SourceControl: &styrav1alpha1.SourceControl{
+ LibraryOrigin: &styrav1alpha1.GitRepo{
+ Path: "libraries/library",
+ Reference: "refs/heads/master",
+ Commit: "commit-sha",
+ URL: "github.com",
+ },
+ },
+ Description: "description",
+ Datasources: []styrav1alpha1.LibraryDatasource{
+ {
+ Path: "oidc/sandbox",
+ Description: "desc",
+ },
+ {
+ Path: "oidc/sandbox/2",
+ Description: "desc2",
+ },
+ },
+ },
+ }
+ ctx := context.Background()
+ ginkgo.By("creating the Library")
+
+ styraClientMock.On("CreateUpdateSecret",
+ mock.Anything,
+ path.Join("libraries", key.Name, "git"),
+ &styra.CreateUpdateSecretsRequest{
+ Name: "test-user",
+ Secret: "test-secret",
+ },
+ ).Return(&styra.CreateUpdateSecretResponse{
+ StatusCode: http.StatusOK,
+ }, nil).Once()
+
+ styraClientMock.On("GetLibrary", mock.Anything, key.Name).
+ Return(nil, &styra.HTTPError{StatusCode: http.StatusNotFound}).Once()
+
+ defaultSourceControl := &styra.LibrarySourceControlConfig{
+ LibraryOrigin: &styra.LibraryGitRepoConfig{
+ Commit: "commit-sha",
+ Credentials: path.Join("libraries", key.Name, "git"),
+ Path: "libraries/library",
+ Reference: "refs/heads/master",
+ URL: "github.com",
+ },
+ }
+
+ styraClientMock.On("UpsertLibrary", mock.Anything, key.Name, &styra.UpsertLibraryRequest{
+ Description: "description",
+ ReadOnly: true,
+ SourceControl: defaultSourceControl,
+ }).Return(&styra.UpsertLibraryResponse{}, nil).Once()
+
+ // New reconciliation started
+
+ styraClientMock.On("CreateUpdateSecret",
+ mock.Anything,
+ path.Join("libraries", key.Name, "git"),
+ &styra.CreateUpdateSecretsRequest{
+ Name: "test-user",
+ Secret: "test-secret",
+ },
+ ).Return(&styra.CreateUpdateSecretResponse{
+ StatusCode: http.StatusOK,
+ }, nil).Once()
+
+ styraClientMock.On("GetLibrary", mock.Anything, key.Name).Return(&styra.GetLibraryResponse{
+ Statuscode: http.StatusOK,
+ LibraryEntityExpanded: &styra.LibraryEntityExpanded{
+ DataSources: []styra.LibraryDatasourceConfig{},
+ Description: "description",
+ ID: key.Name,
+ ReadOnly: true,
+ SourceControl: defaultSourceControl,
+ },
+ }, nil).Once()
+
+ styraClientMock.On("UpsertDatasource", mock.Anything, path.Join("libraries", key.Name, "oidc/sandbox"),
+ &styra.UpsertDatasourceRequest{
+ Category: "rest",
+ Enabled: true,
+ }).Return(&styra.UpsertDatasourceResponse{
+ StatusCode: http.StatusOK,
+ }, nil).Once()
+
+ webhookMock.On(
+ "DatasourceChanged",
+ mock.Anything,
+ mock.Anything,
+ "jwt-library",
+ "",
+ ).Return(nil).Once()
+
+ styraClientMock.On("UpsertDatasource", mock.Anything, path.Join("libraries", key.Name, "oidc/sandbox/2"),
+ &styra.UpsertDatasourceRequest{
+ Category: "rest",
+ Enabled: true,
+ }).Return(&styra.UpsertDatasourceResponse{
+ StatusCode: http.StatusOK,
+ }, nil).Once()
+
+ webhookMock.On(
+ "DatasourceChanged",
+ mock.Anything,
+ mock.Anything,
+ "jwt-library",
+ "",
+ ).Return(nil).Once()
+
+ // createUsersIfMissing:
+ styraClientMock.On("GetUser", mock.Anything, "user1@mail.com").
+ Return(&styra.GetUserResponse{
+ StatusCode: http.StatusNotFound,
+ }, nil).Once()
+
+ styraClientMock.On("CreateInvitation", mock.Anything, false, "user1@mail.com").
+ Return(&styra.CreateInvitationResponse{}, nil).Once()
+
+ styraClientMock.On("GetUser", mock.Anything, "user2@mail.com").
+ Return(&styra.GetUserResponse{
+ StatusCode: http.StatusOK,
+ }, nil).Once()
+
+ // deleteIncorrectRoleBindings:
+ styraClientMock.On("ListRoleBindingsV2", mock.Anything, &styra.ListRoleBindingsV2Params{
+ ResourceKind: styra.RoleBindingKindLibrary,
+ ResourceID: toCreate.Spec.Name,
+ }).Return(&styra.ListRoleBindingsV2Response{
+ Rolebindings: []*styra.RoleBindingConfig{},
+ }, nil).Once() //Test deletions also?
+
+ // createRoleBindingIfMissing:
+ styraClientMock.On("ListRoleBindingsV2", mock.Anything, &styra.ListRoleBindingsV2Params{
+ ResourceKind: styra.RoleBindingKindLibrary,
+ ResourceID: toCreate.Spec.Name,
+ }).Return(&styra.ListRoleBindingsV2Response{
+ Rolebindings: []*styra.RoleBindingConfig{},
+ }, nil).Once()
+
+ styraClientMock.On("CreateRoleBinding", mock.Anything, &styra.CreateRoleBindingRequest{
+ ResourceFilter: &styra.ResourceFilter{
+ ID: toCreate.Spec.Name,
+ Kind: styra.RoleBindingKindLibrary,
+ },
+ RoleID: styra.RoleLibraryViewer,
+ Subjects: []*styra.Subject{
+ {
+ ID: "user1@mail.com",
+ Kind: styra.SubjectKindUser,
+ }, {
+ ID: "user2@mail.com",
+ Kind: styra.SubjectKindUser,
+ }, {
+ Kind: styra.SubjectKindClaim,
+ ClaimConfig: &styra.ClaimConfig{
+ IdentityProvider: "AzureAD Bankdata",
+ Key: "groups",
+ Value: "testGroup",
+ },
+ },
+ },
+ }).Return(&styra.CreateRoleBindingResponse{}, nil).Once()
+
+ // updateRoleBindingIfNeeded:
+ styraClientMock.On("ListRoleBindingsV2", mock.Anything, &styra.ListRoleBindingsV2Params{
+ ResourceKind: styra.RoleBindingKindLibrary,
+ ResourceID: toCreate.Spec.Name,
+ }).Return(&styra.ListRoleBindingsV2Response{
+ Rolebindings: []*styra.RoleBindingConfig{
+ {
+ Subjects: []*styra.Subject{
+ {
+ ID: "user1@mail.com",
+ Kind: styra.SubjectKindUser,
+ }, {
+ ID: "user2@mail.com",
+ Kind: styra.SubjectKindUser,
+ }, {
+ Kind: styra.SubjectKindClaim,
+ ClaimConfig: &styra.ClaimConfig{
+ IdentityProvider: "AzureAD Bankdata",
+ Key: "groups",
+ Value: "testGroup",
+ },
+ },
+ },
+ RoleID: "LibraryViewer",
+ },
+ },
+ }, nil).Once()
+
+ gomega.Ω(k8sClient.Create(ctx, toCreate)).
+ To(gomega.Succeed())
+
+ gomega.Eventually(func() bool {
+ var k8sLib styrav1alpha1.Library
+ if err := k8sClient.Get(ctx, key, &k8sLib); err != nil {
+ return false
+ }
+ return true
+ }, timeout, interval).Should(gomega.BeTrue())
+
+ gomega.Eventually(func() bool {
+ var (
+ createUpdateSecret int
+ getLibrary int
+ upsertLibrary int
+ upsertDatasource int
+ datasourceChanged int
+ getUser int
+ createInvitation int
+ createRoleBinding int
+ listRoleBindings int
+ )
+
+ for _, call := range styraClientMock.Calls {
+ switch call.Method {
+ case "CreateUpdateSecret":
+ createUpdateSecret++
+ case "GetLibrary":
+ getLibrary++
+ case "UpsertLibrary":
+ upsertLibrary++
+ case "UpsertDatasource":
+ upsertDatasource++
+ case "GetUser":
+ getUser++
+ case "CreateInvitation":
+ createInvitation++
+ case "CreateRoleBinding":
+ createRoleBinding++
+ case "ListRoleBindingsV2":
+ listRoleBindings++
+ }
+ }
+
+ for _, call := range webhookMock.Calls {
+ switch call.Method {
+ case "DatasourceChanged":
+ datasourceChanged++
+ }
+ }
+
+ return createUpdateSecret == 2 &&
+ getLibrary == 2 &&
+ upsertLibrary == 1 &&
+ upsertDatasource == 2 &&
+ datasourceChanged == 2 &&
+ getUser == 2 &&
+ createInvitation == 1 &&
+ listRoleBindings == 3 &&
+ createRoleBinding == 1
+ }, timeout, interval).Should(gomega.BeTrue())
+
+ resetMock(&webhookMock.Mock)
+ resetMock(&styraClientMock.Mock)
+
+ styraClientMock.AssertExpectations(ginkgo.GinkgoT())
+
+ })
+})
diff --git a/test/integration/controller/system_controller_test.go b/test/integration/controller/system_controller_test.go
index 0734a1a..1fbe24b 100644
--- a/test/integration/controller/system_controller_test.go
+++ b/test/integration/controller/system_controller_test.go
@@ -1,3 +1,19 @@
+/*
+Copyright (C) 2023 Bankdata (bankdata@bankdata.dk)
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
package styra
import (
@@ -20,7 +36,6 @@ import (
)
var _ = ginkgo.Describe("SystemReconciler.Reconcile", ginkgo.Label("integration"), func() {
-
ginkgo.It("should reconcile", func() {
spec := styrav1beta1.SystemSpec{
DeletionProtection: ptr.Bool(false),
@@ -838,6 +853,7 @@ discovery:
}, timeout, interval).Should(gomega.BeTrue())
resetMock(&styraClientMock.Mock)
+ resetMock(&webhookMock.Mock)
ginkgo.By("Setting a datasource")
@@ -1193,6 +1209,7 @@ discovery:
return k8serrors.IsNotFound(err)
}, timeout, interval).Should(gomega.BeTrue())
+ resetMock(&styraClientMock.Mock)
styraClientMock.AssertExpectations(ginkgo.GinkgoT())
})
})