From ead3b9274f9a9739e7e02ac2cd67536d3286aad3 Mon Sep 17 00:00:00 2001 From: "assaf.admi" Date: Mon, 30 Dec 2024 11:49:28 +0200 Subject: [PATCH] Add CustomRole CRD --- PROJECT | 9 + api/coralogix/v1alpha1/customrole_types.go | 87 ++++++++ .../v1alpha1/zz_generated.deepcopy.go | 99 +++++++++ .../templates/cluster_role.yaml | 26 +++ .../crds/coralogix.com_customroles.yaml | 70 +++++++ cmd/main.go | 12 ++ .../crd/bases/coralogix.com_customroles.yaml | 70 +++++++ config/crd/kustomization.yaml | 2 + .../coralogix_customrole_editor_role.yaml | 27 +++ .../coralogix_customrole_viewer_role.yaml | 23 +++ config/rbac/role.yaml | 26 +++ .../customroles/example-custom-role.yaml | 17 ++ docs/api.md | 130 ++++++++++++ go.mod | 2 +- go.sum | 4 +- .../coralogix/customrole_controller.go | 189 ++++++++++++++++++ scripts/helm-sync-check.sh | 2 +- tests/e2e/custom_role_test.go | 114 +++++++++++ 18 files changed, 905 insertions(+), 4 deletions(-) create mode 100644 api/coralogix/v1alpha1/customrole_types.go create mode 100644 charts/coralogix-operator/templates/crds/coralogix.com_customroles.yaml create mode 100644 config/crd/bases/coralogix.com_customroles.yaml create mode 100644 config/rbac/coralogix_customrole_editor_role.yaml create mode 100644 config/rbac/coralogix_customrole_viewer_role.yaml create mode 100644 config/samples/customroles/example-custom-role.yaml create mode 100644 internal/controller/coralogix/customrole_controller.go create mode 100644 tests/e2e/custom_role_test.go diff --git a/PROJECT b/PROJECT index c66092f1..c863646d 100644 --- a/PROJECT +++ b/PROJECT @@ -63,4 +63,13 @@ resources: webhooks: validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: coralogix.com + group: coralogix + kind: CustomRole + path: github.com/coralogix/coralogix-operator/api/coralogix/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/coralogix/v1alpha1/customrole_types.go b/api/coralogix/v1alpha1/customrole_types.go new file mode 100644 index 00000000..a7b3e736 --- /dev/null +++ b/api/coralogix/v1alpha1/customrole_types.go @@ -0,0 +1,87 @@ +// Copyright 2024 Coralogix Ltd. +// +// 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 ( + "strconv" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + cxsdk "github.com/coralogix/coralogix-management-sdk/go" +) + +// CustomRoleSpec defines the desired state of CustomRole. +type CustomRoleSpec struct { + Name string `json:"name"` + + Description string `json:"description"` + + ParentRoleName string `json:"parentRoleName"` + + Permissions []string `json:"permissions"` +} + +func (s *CustomRoleSpec) ExtractCreateCustomRoleRequest() *cxsdk.CreateRoleRequest { + return &cxsdk.CreateRoleRequest{ + Name: s.Name, + Description: s.Description, + ParentRole: ptr.To(cxsdk.CreateRoleRequestParentRoleName{ParentRoleName: s.ParentRoleName}), + Permissions: s.Permissions, + } +} + +func (s *CustomRoleSpec) ExtractUpdateCustomRoleRequest(id string) (*cxsdk.UpdateRoleRequest, error) { + roleID, err := strconv.Atoi(id) + if err != nil { + return &cxsdk.UpdateRoleRequest{}, err + } + return &cxsdk.UpdateRoleRequest{ + RoleId: uint32(roleID), + NewName: ptr.To(s.Name), + NewDescription: ptr.To(s.Description), + NewPermissions: ptr.To(cxsdk.RolePermissions{Permissions: s.Permissions}), + }, nil +} + +// CustomRoleStatus defines the observed state of CustomRole. +type CustomRoleStatus struct { + ID *string `json:"id"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// CustomRole is the Schema for the customroles API. +type CustomRole struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec CustomRoleSpec `json:"spec,omitempty"` + Status CustomRoleStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// CustomRoleList contains a list of CustomRole. +type CustomRoleList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []CustomRole `json:"items"` +} + +func init() { + SchemeBuilder.Register(&CustomRole{}, &CustomRoleList{}) +} diff --git a/api/coralogix/v1alpha1/zz_generated.deepcopy.go b/api/coralogix/v1alpha1/zz_generated.deepcopy.go index 2b3c12ae..25dd457c 100644 --- a/api/coralogix/v1alpha1/zz_generated.deepcopy.go +++ b/api/coralogix/v1alpha1/zz_generated.deepcopy.go @@ -368,6 +368,105 @@ func (in *Block) DeepCopy() *Block { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CustomRole) DeepCopyInto(out *CustomRole) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomRole. +func (in *CustomRole) DeepCopy() *CustomRole { + if in == nil { + return nil + } + out := new(CustomRole) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CustomRole) 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 *CustomRoleList) DeepCopyInto(out *CustomRoleList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CustomRole, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomRoleList. +func (in *CustomRoleList) DeepCopy() *CustomRoleList { + if in == nil { + return nil + } + out := new(CustomRoleList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CustomRoleList) 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 *CustomRoleSpec) DeepCopyInto(out *CustomRoleSpec) { + *out = *in + if in.Permissions != nil { + in, out := &in.Permissions, &out.Permissions + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomRoleSpec. +func (in *CustomRoleSpec) DeepCopy() *CustomRoleSpec { + if in == nil { + return nil + } + out := new(CustomRoleSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CustomRoleStatus) DeepCopyInto(out *CustomRoleStatus) { + *out = *in + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomRoleStatus. +func (in *CustomRoleStatus) DeepCopy() *CustomRoleStatus { + if in == nil { + return nil + } + out := new(CustomRoleStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Demisto) DeepCopyInto(out *Demisto) { *out = *in diff --git a/charts/coralogix-operator/templates/cluster_role.yaml b/charts/coralogix-operator/templates/cluster_role.yaml index a5d77b99..a60959ef 100644 --- a/charts/coralogix-operator/templates/cluster_role.yaml +++ b/charts/coralogix-operator/templates/cluster_role.yaml @@ -125,6 +125,32 @@ rules: - get - patch - update +- apiGroups: + - coralogix.com + resources: + - customroles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - coralogix.com + resources: + - customroles/finalizers + verbs: + - update +- apiGroups: + - coralogix.com + resources: + - customroles/status + verbs: + - get + - patch + - update - apiGroups: - coralogix.com resources: diff --git a/charts/coralogix-operator/templates/crds/coralogix.com_customroles.yaml b/charts/coralogix-operator/templates/crds/coralogix.com_customroles.yaml new file mode 100644 index 00000000..d6277a67 --- /dev/null +++ b/charts/coralogix-operator/templates/crds/coralogix.com_customroles.yaml @@ -0,0 +1,70 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: customroles.coralogix.com +spec: + group: coralogix.com + names: + kind: CustomRole + listKind: CustomRoleList + plural: customroles + singular: customrole + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: CustomRole is the Schema for the customroles 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: CustomRoleSpec defines the desired state of CustomRole. + properties: + description: + type: string + name: + type: string + parentRoleName: + type: string + permissions: + items: + type: string + type: array + required: + - description + - name + - parentRoleName + - permissions + type: object + status: + description: CustomRoleStatus defines the observed state of CustomRole. + properties: + id: + type: string + required: + - id + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/cmd/main.go b/cmd/main.go index 011f4a9b..a764483e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -37,6 +37,8 @@ import ( // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" + cxsdk "github.com/coralogix/coralogix-management-sdk/go" + utils "github.com/coralogix/coralogix-operator/api" coralogixv1alpha1 "github.com/coralogix/coralogix-operator/api/coralogix/v1alpha1" controllers "github.com/coralogix/coralogix-operator/internal/controller" @@ -203,6 +205,8 @@ func main() { os.Exit(1) } + SDKClientSet := cxsdk.NewClientSet(targetUrl, apiKey, apiKey) + if err = (&coralogixcontrollers.RuleGroupReconciler{ CoralogixClientSet: clientset.NewClientSet(targetUrl, apiKey), Client: mgr.GetClient(), @@ -255,6 +259,14 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "ApiKey") os.Exit(1) } + if err = (&coralogixcontrollers.CustomRoleReconciler{ + CustomRolesClient: SDKClientSet.Roles(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "CustomRole") + os.Exit(1) + } if prometheusRuleController { if err = (&controllers.AlertmanagerConfigReconciler{ diff --git a/config/crd/bases/coralogix.com_customroles.yaml b/config/crd/bases/coralogix.com_customroles.yaml new file mode 100644 index 00000000..d6277a67 --- /dev/null +++ b/config/crd/bases/coralogix.com_customroles.yaml @@ -0,0 +1,70 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: customroles.coralogix.com +spec: + group: coralogix.com + names: + kind: CustomRole + listKind: CustomRoleList + plural: customroles + singular: customrole + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: CustomRole is the Schema for the customroles 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: CustomRoleSpec defines the desired state of CustomRole. + properties: + description: + type: string + name: + type: string + parentRoleName: + type: string + permissions: + items: + type: string + type: array + required: + - description + - name + - parentRoleName + - permissions + type: object + status: + description: CustomRoleStatus defines the observed state of CustomRole. + properties: + id: + type: string + required: + - id + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 6a98c2a5..219d1b09 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -7,6 +7,7 @@ resources: - bases/coralogix.com_recordingrulegroupsets.yaml - bases/coralogix.com_outboundwebhooks.yaml - bases/coralogix.com_apikeys.yaml + - bases/coralogix.com_customroles.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -25,6 +26,7 @@ patchesStrategicMerge: #- patches/cainjection_in_recordingrulegroupsets.yaml #- patches/cainjection_in_outboundwebhooks.yaml #- patches/cainjection_in_coralogix_apikeys.yaml +#- patches/cainjection_in_coralogix_customroles.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/rbac/coralogix_customrole_editor_role.yaml b/config/rbac/coralogix_customrole_editor_role.yaml new file mode 100644 index 00000000..d26566d8 --- /dev/null +++ b/config/rbac/coralogix_customrole_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit customroles. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: coralogix-operator + app.kubernetes.io/managed-by: kustomize + name: coralogix-customrole-editor-role +rules: +- apiGroups: + - coralogix.com + resources: + - customroles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - coralogix.com + resources: + - customroles/status + verbs: + - get diff --git a/config/rbac/coralogix_customrole_viewer_role.yaml b/config/rbac/coralogix_customrole_viewer_role.yaml new file mode 100644 index 00000000..6edb15d0 --- /dev/null +++ b/config/rbac/coralogix_customrole_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view customroles. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: coralogix-operator + app.kubernetes.io/managed-by: kustomize + name: coralogix-customrole-viewer-role +rules: +- apiGroups: + - coralogix.com + resources: + - customroles + verbs: + - get + - list + - watch +- apiGroups: + - coralogix.com + resources: + - customroles/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 969c37cf..0d848305 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -68,6 +68,32 @@ rules: - get - patch - update +- apiGroups: + - coralogix.com + resources: + - customroles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - coralogix.com + resources: + - customroles/finalizers + verbs: + - update +- apiGroups: + - coralogix.com + resources: + - customroles/status + verbs: + - get + - patch + - update - apiGroups: - coralogix.com resources: diff --git a/config/samples/customroles/example-custom-role.yaml b/config/samples/customroles/example-custom-role.yaml new file mode 100644 index 00000000..1b284699 --- /dev/null +++ b/config/samples/customroles/example-custom-role.yaml @@ -0,0 +1,17 @@ +apiVersion: coralogix.com/v1alpha1 +kind: CustomRole +metadata: + labels: + app.kubernetes.io/name: coralogix-operator + app.kubernetes.io/instance: custom-role-sample + app.kubernetes.io/part-of: coralogix-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: coralogix-operator + name: custom-role-sample +spec: + name: custom-role-sample + description: This is a sample custom role + parentRoleName: Standard User + permissions: + - team-actions:UpdateConfig + - TEAM-CUSTOM-API-KEYS:READCONFIG diff --git a/docs/api.md b/docs/api.md index 60be43d1..be858a7c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -12,6 +12,8 @@ Resource Types: - [ApiKey](#apikey) +- [CustomRole](#customrole) + - [OutboundWebhook](#outboundwebhook) - [RecordingRuleGroupSet](#recordingrulegroupset) @@ -2531,6 +2533,134 @@ ApiKeyStatus defines the observed state of ApiKey. +## CustomRole +[↩ Parent](#coralogixcomv1alpha1 ) + + + + + + +CustomRole is the Schema for the customroles API. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
apiVersionstringcoralogix.com/v1alpha1true
kindstringCustomRoletrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
specobject + CustomRoleSpec defines the desired state of CustomRole.
+
false
statusobject + CustomRoleStatus defines the observed state of CustomRole.
+
false
+ + +### CustomRole.spec +[↩ Parent](#customrole) + + + +CustomRoleSpec defines the desired state of CustomRole. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
descriptionstring +
+
true
namestring +
+
true
parentRoleNamestring +
+
true
permissions[]string +
+
true
+ + +### CustomRole.status +[↩ Parent](#customrole) + + + +CustomRoleStatus defines the observed state of CustomRole. + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
idstring +
+
true
+ ## OutboundWebhook [↩ Parent](#coralogixcomv1alpha1 ) diff --git a/go.mod b/go.mod index f5fe226e..f7fc1e0d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/coralogix/coralogix-operator go 1.23.0 require ( - github.com/coralogix/coralogix-management-sdk v0.2.2-0.20241211122224-51a71322df90 + github.com/coralogix/coralogix-management-sdk v0.12.2-0.20241230141909-7424ae8fb966 github.com/go-logr/logr v1.4.2 github.com/golang/protobuf v1.5.4 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index 870336b6..ba17178e 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/coralogix/coralogix-management-sdk v0.2.2-0.20241211122224-51a71322df90 h1:2RKd6KdaAp2PqbzGtgkfhuJ0Gim1CkIywPXSiJcvCj4= -github.com/coralogix/coralogix-management-sdk v0.2.2-0.20241211122224-51a71322df90/go.mod h1:4j43DiCGPC8pQghrZRgQp+V3dTzObZ2ohRXswnRkIrE= +github.com/coralogix/coralogix-management-sdk v0.12.2-0.20241230141909-7424ae8fb966 h1:s3p8BP5lvn/7FMpJBUuKN3IVAw+44RqmgvioYkOFT8w= +github.com/coralogix/coralogix-management-sdk v0.12.2-0.20241230141909-7424ae8fb966/go.mod h1:4j43DiCGPC8pQghrZRgQp+V3dTzObZ2ohRXswnRkIrE= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/controller/coralogix/customrole_controller.go b/internal/controller/coralogix/customrole_controller.go new file mode 100644 index 00000000..3b33483b --- /dev/null +++ b/internal/controller/coralogix/customrole_controller.go @@ -0,0 +1,189 @@ +// Copyright 2024 Coralogix Ltd. +// +// 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 coralogix + +import ( + "context" + "fmt" + "strconv" + + "github.com/go-logr/logr" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/encoding/protojson" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + cxsdk "github.com/coralogix/coralogix-management-sdk/go" + + coralogixv1alpha1 "github.com/coralogix/coralogix-operator/api/coralogix/v1alpha1" +) + +// CustomRoleReconciler reconciles a CustomRole object +type CustomRoleReconciler struct { + client.Client + CustomRolesClient *cxsdk.RolesClient + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=coralogix.com,resources=customroles,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=coralogix.com,resources=customroles/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=coralogix.com,resources=customroles/finalizers,verbs=update + +var ( + customRoleFinalizerName = "custom-role.coralogix.com/finalizer" +) + +func (r *CustomRoleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx).WithValues( + "customRole", req.NamespacedName.Name, + "namespace", req.NamespacedName.Namespace, + ) + + customRole := &coralogixv1alpha1.CustomRole{} + if err := r.Get(ctx, req.NamespacedName, customRole); err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + return ctrl.Result{}, nil + } + return ctrl.Result{RequeueAfter: defaultErrRequeuePeriod}, err + } + + if ptr.Deref(customRole.Status.ID, "") == "" { + err := r.create(ctx, log, customRole) + if err != nil { + log.Error(err, "Error on creating CustomRole") + return ctrl.Result{RequeueAfter: defaultErrRequeuePeriod}, err + } + return ctrl.Result{}, nil + } + + if !customRole.ObjectMeta.DeletionTimestamp.IsZero() { + err := r.delete(ctx, log, customRole) + if err != nil { + log.Error(err, "Error on deleting CustomRole") + return ctrl.Result{RequeueAfter: defaultErrRequeuePeriod}, err + } + return ctrl.Result{}, nil + } + + err := r.update(ctx, log, customRole) + if err != nil { + log.Error(err, "Error on updating CustomRole") + return ctrl.Result{RequeueAfter: defaultErrRequeuePeriod}, err + } + + return ctrl.Result{}, nil +} + +func (r *CustomRoleReconciler) create(ctx context.Context, log logr.Logger, customRole *coralogixv1alpha1.CustomRole) error { + createRequest := customRole.Spec.ExtractCreateCustomRoleRequest() + log.V(1).Info("Creating remote custom-role", "custom-role", protojson.Format(createRequest)) + createResponse, err := r.CustomRolesClient.Create(ctx, createRequest) + if err != nil { + return fmt.Errorf("error on creating remote custom-role: %w", err) + } + log.V(1).Info("Remote custom-role created", "response", protojson.Format(createResponse)) + + id := strconv.Itoa(int(createResponse.Id)) + customRole.Status = coralogixv1alpha1.CustomRoleStatus{ + ID: &id, + } + + log.V(1).Info("Updating CustomRole status", "id", id) + if err = r.Status().Update(ctx, customRole); err != nil { + if err := r.deleteRemoteCustomRole(ctx, log, customRole.Status.ID); err != nil { + return fmt.Errorf("error to delete custom-role after status update error -\n%v", customRole) + } + return fmt.Errorf("error to update custom-role status -\n%v", customRole) + } + + if !controllerutil.ContainsFinalizer(customRole, customRoleFinalizerName) { + log.V(1).Info("Updating CustomRole to add finalizer", "id", id) + controllerutil.AddFinalizer(customRole, customRoleFinalizerName) + if err := r.Update(ctx, customRole); err != nil { + return fmt.Errorf("error on updating CustomRole: %w", err) + } + } + + return nil +} + +func (r *CustomRoleReconciler) update(ctx context.Context, log logr.Logger, customRole *coralogixv1alpha1.CustomRole) error { + updateRequest, err := customRole.Spec.ExtractUpdateCustomRoleRequest(*customRole.Status.ID) + if err != nil { + return fmt.Errorf("error on extracting update request: %w", err) + } + log.V(1).Info("Updating remote custom-role", "custom-role", protojson.Format(updateRequest)) + updateResponse, err := r.CustomRolesClient.Update(ctx, updateRequest) + if err != nil { + if cxsdk.Code(err) == codes.NotFound { + log.V(1).Info("custom-role not found on remote, removing id from status") + customRole.Status = coralogixv1alpha1.CustomRoleStatus{ + ID: ptr.To(""), + } + if err = r.Status().Update(ctx, customRole); err != nil { + return fmt.Errorf("error on updating CustomRole status: %w", err) + } + return fmt.Errorf("custom-role not found on remote: %w", err) + } + return fmt.Errorf("error on updating custom-role: %w", err) + } + log.V(1).Info("Remote custom-role updated", "custom-role", protojson.Format(updateResponse)) + + return nil +} + +func (r *CustomRoleReconciler) delete(ctx context.Context, log logr.Logger, customRole *coralogixv1alpha1.CustomRole) error { + if err := r.deleteRemoteCustomRole(ctx, log, customRole.Status.ID); err != nil { + return fmt.Errorf("error on deleting remote custom-role: %w", err) + } + + log.V(1).Info("Removing finalizer from CustomRole") + controllerutil.RemoveFinalizer(customRole, customRoleFinalizerName) + if err := r.Update(ctx, customRole); err != nil { + return fmt.Errorf("error on updating CustomRole: %w", err) + } + + return nil +} + +func (r *CustomRoleReconciler) deleteRemoteCustomRole(ctx context.Context, log logr.Logger, customRoleID *string) error { + log.V(1).Info("Deleting custom-role from remote", "id", customRoleID) + id, err := strconv.Atoi(*customRoleID) + if err != nil { + return fmt.Errorf("error on converting custom-role id to int: %w", err) + } + + if _, err := r.CustomRolesClient.Delete(ctx, &cxsdk.DeleteRoleRequest{RoleId: uint32(id)}); err != nil && cxsdk.Code(err) != codes.NotFound { + log.V(1).Error(err, "Error on deleting remote custom-role", "id", customRoleID) + return fmt.Errorf("error to delete remote custom-role -\n%v", customRoleID) + } + log.V(1).Info("custom-role was deleted from remote", "id", customRoleID) + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *CustomRoleReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&coralogixv1alpha1.CustomRole{}). + Complete(r) +} diff --git a/scripts/helm-sync-check.sh b/scripts/helm-sync-check.sh index fb5ef811..760b92cb 100755 --- a/scripts/helm-sync-check.sh +++ b/scripts/helm-sync-check.sh @@ -6,7 +6,7 @@ set -e crds_path="config/crd/bases" chart_crds_path="charts/coralogix-operator/templates/crds" role_file="config/rbac/role.yaml" -chart_role_file="charts/coralogix-operator/templates/clusterrole.yaml" +chart_role_file="charts/coralogix-operator/templates/cluster_role.yaml" webhook_file="config/webhook/manifests.yaml" chart_webhook_file="charts/coralogix-operator/templates/webhook.yaml" diff --git a/tests/e2e/custom_role_test.go b/tests/e2e/custom_role_test.go new file mode 100644 index 00000000..1ffa43fe --- /dev/null +++ b/tests/e2e/custom_role_test.go @@ -0,0 +1,114 @@ +// Copyright 2024 Coralogix Ltd. +// +// 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 e2e + +import ( + "context" + "fmt" + "strconv" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "google.golang.org/grpc/codes" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + cxsdk "github.com/coralogix/coralogix-management-sdk/go" + + coralogixv1alpha1 "github.com/coralogix/coralogix-operator/api/coralogix/v1alpha1" +) + +var _ = Describe("CustomRole", Ordered, func() { + var ( + crClient client.Client + rolesClient *cxsdk.RolesClient + customRoleID uint32 + customRole *coralogixv1alpha1.CustomRole + customRoleName = "custom-role-sample" + ) + + BeforeEach(func() { + crClient = ClientsInstance.GetControllerRuntimeClient() + rolesClient = ClientsInstance.GetCoralogixClientSet().Roles() + customRole = &coralogixv1alpha1.CustomRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: customRoleName, + Namespace: testNamespace, + }, + Spec: coralogixv1alpha1.CustomRoleSpec{ + Name: customRoleName, + Description: "This is a sample custom role", + ParentRoleName: "Standard User", + Permissions: []string{ + "team-actions:UpdateConfig", + "TEAM-CUSTOM-API-KEYS:READCONFIG", + }, + }, + } + }) + + It("Should be created successfully", func(ctx context.Context) { + By("Creating CustomRole") + Expect(crClient.Create(ctx, customRole)).To(Succeed()) + + By("Fetching the CustomRole ID") + fetchedCustomRole := &coralogixv1alpha1.CustomRole{} + Eventually(func(g Gomega) error { + g.Expect(crClient.Get(ctx, types.NamespacedName{Name: customRoleName, Namespace: testNamespace}, fetchedCustomRole)).To(Succeed()) + if fetchedCustomRole.Status.ID != nil { + id, err := strconv.Atoi(*fetchedCustomRole.Status.ID) + Expect(err).ToNot(HaveOccurred()) + customRoleID = uint32(id) + return nil + } + return fmt.Errorf("CustomRole ID is not set") + }, time.Minute, time.Second).Should(Succeed()) + + By("Verifying CustomRole exists in Coralogix backend") + Eventually(func() error { + _, err := rolesClient.Get(ctx, &cxsdk.GetCustomRoleRequest{RoleId: customRoleID}) + return err + }, time.Minute, time.Second).Should(Succeed()) + }) + + It("Should be updated successfully", func(ctx context.Context) { + By("Patching the CustomRole") + newCustomRoleName := "custom-role-updated" + modifiedCustomRole := customRole.DeepCopy() + modifiedCustomRole.Spec.Name = newCustomRoleName + Expect(crClient.Patch(ctx, modifiedCustomRole, client.MergeFrom(customRole))).To(Succeed()) + + By("Verifying CustomRole is updated in Coralogix backend") + Eventually(func() string { + getCustomRoleRes, err := rolesClient.Get(ctx, &cxsdk.GetCustomRoleRequest{RoleId: customRoleID}) + Expect(err).ToNot(HaveOccurred()) + return getCustomRoleRes.Role.Name + }, time.Minute, time.Second).Should(Equal(newCustomRoleName)) + }) + + It("Should be deleted successfully", func(ctx context.Context) { + By("Deleting the CustomRole") + Expect(crClient.Delete(ctx, customRole)).To(Succeed()) + + By("Verifying CustomRole is deleted from Coralogix backend") + Eventually(func() codes.Code { + _, err := rolesClient.Get(ctx, &cxsdk.GetCustomRoleRequest{RoleId: customRoleID}) + return cxsdk.Code(err) + }).Should(Equal(codes.NotFound)) + }) +})