From 3a0d3f348d3032e9753ebf10bdcd1fe927016764 Mon Sep 17 00:00:00 2001 From: Bohdan Siryk Date: Fri, 30 Jun 2023 15:17:55 +0300 Subject: [PATCH] issue-439, implemented opensearch user resource --- .../v1beta1/opensearchuser_types.go | 75 ++++ .../v1beta1/zz_generated.deepcopy.go | 94 +++++ apis/clusters/v1beta1/opensearch_types.go | 1 + .../clusters/v1beta1/zz_generated.deepcopy.go | 5 + ...urces.instaclustr.com_opensearchusers.yaml | 66 ++++ ...clusters.instaclustr.com_opensearches.yaml | 10 + config/crd/kustomization.yaml | 1 + ...n_in_clusterresources_opensearchusers.yaml | 7 + ...k_in_clusterresources_opensearchusers.yaml | 16 + ...rresources_opensearchuser_editor_role.yaml | 31 ++ ...rresources_opensearchuser_viewer_role.yaml | 27 ++ config/rbac/role.yaml | 27 ++ ...usterresources_v1beta1_opensearchuser.yaml | 16 + .../samples/clusters_v1beta1_opensearch.yaml | 7 +- .../cassandrauser_controller.go | 15 +- controllers/clusterresources/helpers.go | 14 + .../opensearchuser_controller.go | 342 ++++++++++++++++++ .../clusters/datatest/opensearch_v1beta1.yaml | 3 + controllers/clusters/opensearch_controller.go | 71 ++++ main.go | 9 + pkg/instaclustr/client.go | 4 + pkg/models/apiv1.go | 10 + pkg/models/operator.go | 1 + 23 files changed, 837 insertions(+), 15 deletions(-) create mode 100644 apis/clusterresources/v1beta1/opensearchuser_types.go create mode 100644 config/crd/bases/clusterresources.instaclustr.com_opensearchusers.yaml create mode 100644 config/crd/patches/cainjection_in_clusterresources_opensearchusers.yaml create mode 100644 config/crd/patches/webhook_in_clusterresources_opensearchusers.yaml create mode 100644 config/rbac/clusterresources_opensearchuser_editor_role.yaml create mode 100644 config/rbac/clusterresources_opensearchuser_viewer_role.yaml create mode 100644 config/samples/clusterresources_v1beta1_opensearchuser.yaml create mode 100644 controllers/clusterresources/opensearchuser_controller.go diff --git a/apis/clusterresources/v1beta1/opensearchuser_types.go b/apis/clusterresources/v1beta1/opensearchuser_types.go new file mode 100644 index 000000000..c45ae6e56 --- /dev/null +++ b/apis/clusterresources/v1beta1/opensearchuser_types.go @@ -0,0 +1,75 @@ +/* +Copyright 2022. + +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 v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/instaclustr/operator/pkg/models" +) + +// OpenSearchUserSpec defines the desired state of OpenSearchUser +type OpenSearchUserSpec struct { + SecretRef *SecretReference `json:"secretRef"` +} + +// OpenSearchUserStatus defines the observed state of OpenSearchUser +type OpenSearchUserStatus struct { + State string `json:"state"` + ClusterID string `json:"clusterId"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// OpenSearchUser is the Schema for the opensearchusers API +type OpenSearchUser struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec OpenSearchUserSpec `json:"spec,omitempty"` + Status OpenSearchUserStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// OpenSearchUserList contains a list of OpenSearchUser +type OpenSearchUserList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []OpenSearchUser `json:"items"` +} + +func (u *OpenSearchUser) ToInstaAPI(username, password string) *models.InstaOpenSearchUser { + return &models.InstaOpenSearchUser{ + InstaUser: &models.InstaUser{ + Username: username, + Password: password, + InitialPermission: "standard", + }, + } +} + +func (u *OpenSearchUser) NewPatch() client.Patch { + old := u.DeepCopy() + return client.MergeFrom(old) +} + +func init() { + SchemeBuilder.Register(&OpenSearchUser{}, &OpenSearchUserList{}) +} diff --git a/apis/clusterresources/v1beta1/zz_generated.deepcopy.go b/apis/clusterresources/v1beta1/zz_generated.deepcopy.go index c7469853b..2ff3d2e59 100644 --- a/apis/clusterresources/v1beta1/zz_generated.deepcopy.go +++ b/apis/clusterresources/v1beta1/zz_generated.deepcopy.go @@ -1098,6 +1098,100 @@ func (in *NodeReloadStatus) DeepCopy() *NodeReloadStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenSearchUser) DeepCopyInto(out *OpenSearchUser) { + *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 OpenSearchUser. +func (in *OpenSearchUser) DeepCopy() *OpenSearchUser { + if in == nil { + return nil + } + out := new(OpenSearchUser) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OpenSearchUser) 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 *OpenSearchUserList) DeepCopyInto(out *OpenSearchUserList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OpenSearchUser, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenSearchUserList. +func (in *OpenSearchUserList) DeepCopy() *OpenSearchUserList { + if in == nil { + return nil + } + out := new(OpenSearchUserList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OpenSearchUserList) 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 *OpenSearchUserSpec) DeepCopyInto(out *OpenSearchUserSpec) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(SecretReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenSearchUserSpec. +func (in *OpenSearchUserSpec) DeepCopy() *OpenSearchUserSpec { + if in == nil { + return nil + } + out := new(OpenSearchUserSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenSearchUserStatus) DeepCopyInto(out *OpenSearchUserStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenSearchUserStatus. +func (in *OpenSearchUserStatus) DeepCopy() *OpenSearchUserStatus { + if in == nil { + return nil + } + out := new(OpenSearchUserStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Operation) DeepCopyInto(out *Operation) { *out = *in diff --git a/apis/clusters/v1beta1/opensearch_types.go b/apis/clusters/v1beta1/opensearch_types.go index 564d87d6d..141084dc1 100644 --- a/apis/clusters/v1beta1/opensearch_types.go +++ b/apis/clusters/v1beta1/opensearch_types.go @@ -52,6 +52,7 @@ type OpenSearchSpec struct { IndexManagementPlugin bool `json:"indexManagementPlugin,omitempty"` AlertingPlugin bool `json:"alertingPlugin,omitempty"` BundledUseOnly bool `json:"bundleUseOnly,omitempty"` + UserRef *UserReference `json:"userRef,omitempty"` } type OpenSearchDataCentre struct { diff --git a/apis/clusters/v1beta1/zz_generated.deepcopy.go b/apis/clusters/v1beta1/zz_generated.deepcopy.go index bdd5a001b..3d396ec1c 100644 --- a/apis/clusters/v1beta1/zz_generated.deepcopy.go +++ b/apis/clusters/v1beta1/zz_generated.deepcopy.go @@ -1386,6 +1386,11 @@ func (in *OpenSearchSpec) DeepCopyInto(out *OpenSearchSpec) { } } } + if in.UserRef != nil { + in, out := &in.UserRef, &out.UserRef + *out = new(UserReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenSearchSpec. diff --git a/config/crd/bases/clusterresources.instaclustr.com_opensearchusers.yaml b/config/crd/bases/clusterresources.instaclustr.com_opensearchusers.yaml new file mode 100644 index 000000000..eb7ca15b9 --- /dev/null +++ b/config/crd/bases/clusterresources.instaclustr.com_opensearchusers.yaml @@ -0,0 +1,66 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: opensearchusers.clusterresources.instaclustr.com +spec: + group: clusterresources.instaclustr.com + names: + kind: OpenSearchUser + listKind: OpenSearchUserList + plural: opensearchusers + singular: opensearchuser + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: OpenSearchUser is the Schema for the opensearchusers 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: OpenSearchUserSpec defines the desired state of OpenSearchUser + properties: + secretRef: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + required: + - secretRef + type: object + status: + description: OpenSearchUserStatus defines the observed state of OpenSearchUser + properties: + clusterId: + type: string + state: + type: string + required: + - clusterId + - state + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/clusters.instaclustr.com_opensearches.yaml b/config/crd/bases/clusters.instaclustr.com_opensearches.yaml index a3c69f765..63214ffd1 100644 --- a/config/crd/bases/clusters.instaclustr.com_opensearches.yaml +++ b/config/crd/bases/clusters.instaclustr.com_opensearches.yaml @@ -212,6 +212,16 @@ spec: - email type: object type: array + userRef: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object version: type: string required: diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 5bcd4dea7..5d023140a 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -25,6 +25,7 @@ resources: - bases/clusterresources.instaclustr.com_redisusers.yaml - bases/clusterresources.instaclustr.com_awsencryptionkeys.yaml - bases/clusterresources.instaclustr.com_cassandrausers.yaml +- bases/clusterresources.instaclustr.com_opensearchusers.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: diff --git a/config/crd/patches/cainjection_in_clusterresources_opensearchusers.yaml b/config/crd/patches/cainjection_in_clusterresources_opensearchusers.yaml new file mode 100644 index 000000000..f33613a34 --- /dev/null +++ b/config/crd/patches/cainjection_in_clusterresources_opensearchusers.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: opensearchusers.clusterresources.instaclustr.com diff --git a/config/crd/patches/webhook_in_clusterresources_opensearchusers.yaml b/config/crd/patches/webhook_in_clusterresources_opensearchusers.yaml new file mode 100644 index 000000000..1189ae440 --- /dev/null +++ b/config/crd/patches/webhook_in_clusterresources_opensearchusers.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: opensearchusers.clusterresources.instaclustr.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/clusterresources_opensearchuser_editor_role.yaml b/config/rbac/clusterresources_opensearchuser_editor_role.yaml new file mode 100644 index 000000000..3fd5b8ebc --- /dev/null +++ b/config/rbac/clusterresources_opensearchuser_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit opensearchusers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: opensearchuser-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: operator + app.kubernetes.io/part-of: operator + app.kubernetes.io/managed-by: kustomize + name: opensearchuser-editor-role +rules: +- apiGroups: + - clusterresources.instaclustr.com + resources: + - opensearchusers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - clusterresources.instaclustr.com + resources: + - opensearchusers/status + verbs: + - get diff --git a/config/rbac/clusterresources_opensearchuser_viewer_role.yaml b/config/rbac/clusterresources_opensearchuser_viewer_role.yaml new file mode 100644 index 000000000..3c7e6cee8 --- /dev/null +++ b/config/rbac/clusterresources_opensearchuser_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view opensearchusers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: opensearchuser-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: operator + app.kubernetes.io/part-of: operator + app.kubernetes.io/managed-by: kustomize + name: opensearchuser-viewer-role +rules: +- apiGroups: + - clusterresources.instaclustr.com + resources: + - opensearchusers + verbs: + - get + - list + - watch +- apiGroups: + - clusterresources.instaclustr.com + resources: + - opensearchusers/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index d23931ae1..7cb83e103 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -309,6 +309,33 @@ rules: - get - patch - update +- apiGroups: + - clusterresources.instaclustr.com + resources: + - opensearchusers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - clusterresources.instaclustr.com + resources: + - opensearchusers/finalizers + verbs: + - patch + - update +- apiGroups: + - clusterresources.instaclustr.com + resources: + - opensearchusers/status + verbs: + - get + - patch + - update - apiGroups: - clusterresources.instaclustr.com resources: diff --git a/config/samples/clusterresources_v1beta1_opensearchuser.yaml b/config/samples/clusterresources_v1beta1_opensearchuser.yaml new file mode 100644 index 000000000..f7b375e32 --- /dev/null +++ b/config/samples/clusterresources_v1beta1_opensearchuser.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Secret +metadata: + name: test-secret-1 +data: + username: dGVzdC11c2VyLTEK # test-user-1 + password: VGVzdFBhc3MxMjMhCg== # TestPass123! +--- +apiVersion: clusterresources.instaclustr.com/v1beta1 +kind: OpenSearchUser +metadata: + name: test-user-1 +spec: + secretRef: + name: "test-secret-1" + namespace: "default" \ No newline at end of file diff --git a/config/samples/clusters_v1beta1_opensearch.yaml b/config/samples/clusters_v1beta1_opensearch.yaml index ca35dddf6..fdbc5abcd 100644 --- a/config/samples/clusters_v1beta1_opensearch.yaml +++ b/config/samples/clusters_v1beta1_opensearch.yaml @@ -14,6 +14,9 @@ spec: alertingPlugin: false anomalyDetectionPlugin: false asynchronousSearchPlugin: false +# userRef: +# name: "test-user-1" +# namespace: "default" clusterManagerNodes: - dedicatedManager: false nodeSize: SRH-DEV-t4g.small-5 @@ -31,13 +34,13 @@ spec: indexManagementPlugin: true knnPlugin: false loadBalancer: false - name: operatorOpenSearch + name: bohdan-test notificationsPlugin: false # opensearchDashboards: # - nodeSize: SRH-DEV-t4g.small-5 # oidcProvider: '' # version: opensearch-dashboards:2.5.0 - version: 2.5.0 + version: 2.7.0 pciCompliance: false privateNetworkCluster: false reportingPlugin: false diff --git a/controllers/clusterresources/cassandrauser_controller.go b/controllers/clusterresources/cassandrauser_controller.go index 4bc3c6456..24616c205 100644 --- a/controllers/clusterresources/cassandrauser_controller.go +++ b/controllers/clusterresources/cassandrauser_controller.go @@ -132,7 +132,7 @@ func (r *CassandraUserReconciler) Reconcile(ctx context.Context, req ctrl.Reques if u.Status.ClusterID != "" && u.Status.State != models.Created { patch := u.NewPatch() - username, password, err := r.getUserCreds(s) + username, password, err := getUserCreds(s) if err != nil { l.Error(err, "Cannot get user credentials", "user", u.Name) r.EventRecorder.Eventf(u, models.Warning, models.CreatingEvent, @@ -204,24 +204,13 @@ func (r *CassandraUserReconciler) Reconcile(ctx context.Context, req ctrl.Reques return models.ExitReconcile, nil } -func (r *CassandraUserReconciler) getUserCreds(secret *k8sCore.Secret) (username, password string, err error) { - password = string(secret.Data[models.Password]) - username = string(secret.Data[models.Username]) - - if len(username) == 0 || len(password) == 0 { - return "", "", models.ErrMissingSecretKeys - } - - return username[:len(username)-1], password[:len(password)-1], nil -} - func (r *CassandraUserReconciler) handleDeleteUser( ctx context.Context, l logr.Logger, s *k8sCore.Secret, u *v1beta1.CassandraUser, ) error { - username, _, err := r.getUserCreds(s) + username, _, err := getUserCreds(s) if err != nil { l.Error(err, "Cannot get user credentials", "user", u.Name) r.EventRecorder.Eventf(u, models.Warning, models.CreatingEvent, diff --git a/controllers/clusterresources/helpers.go b/controllers/clusterresources/helpers.go index a2ba6f637..cab686110 100644 --- a/controllers/clusterresources/helpers.go +++ b/controllers/clusterresources/helpers.go @@ -17,6 +17,9 @@ limitations under the License. package clusterresources import ( + k8sCore "k8s.io/api/core/v1" + + "github.com/instaclustr/operator/pkg/models" "github.com/instaclustr/operator/apis/clusterresources/v1beta1" ) @@ -61,3 +64,14 @@ func areEncryptionKeyStatusesEqual(a, b *v1beta1.AWSEncryptionKeyStatus) bool { return true } + +func getUserCreds(secret *k8sCore.Secret) (username, password string, err error) { + password = string(secret.Data["password"]) + username = string(secret.Data["username"]) + + if len(username) == 0 || len(password) == 0 { + return "", "", models.ErrMissingSecretKeys + } + + return username[:len(username)-1], password[:len(password)-1], nil +} diff --git a/controllers/clusterresources/opensearchuser_controller.go b/controllers/clusterresources/opensearchuser_controller.go new file mode 100644 index 000000000..8ade4c950 --- /dev/null +++ b/controllers/clusterresources/opensearchuser_controller.go @@ -0,0 +1,342 @@ +/* +Copyright 2022. + +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 clusterresources + +import ( + "context" + "errors" + + "github.com/go-logr/logr" + k8sCore "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + 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/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + clusterresourcesv1beta1 "github.com/instaclustr/operator/apis/clusterresources/v1beta1" + "github.com/instaclustr/operator/pkg/instaclustr" + "github.com/instaclustr/operator/pkg/models" +) + +// OpenSearchUserReconciler reconciles a OpenSearchUser object +type OpenSearchUserReconciler struct { + client.Client + Scheme *runtime.Scheme + API instaclustr.API + EventRecorder record.EventRecorder +} + +//+kubebuilder:rbac:groups=clusterresources.instaclustr.com,resources=opensearchusers,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=clusterresources.instaclustr.com,resources=opensearchusers/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=clusterresources.instaclustr.com,resources=opensearchusers/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.4/pkg/reconcile +func (r *OpenSearchUserReconciler) Reconcile( + ctx context.Context, + req ctrl.Request, +) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + user := &clusterresourcesv1beta1.OpenSearchUser{} + err := r.Get(ctx, req.NamespacedName, user) + if err != nil { + if k8sErrors.IsNotFound(err) { + logger.Info("OpenSearch user resource is not found", + "request", req, + ) + return models.ExitReconcile, nil + } + logger.Error(err, "Cannot fetch OpenSearch user resource", + "request", req, + ) + return models.ReconcileRequeue, nil + } + + secret := &k8sCore.Secret{} + err = r.Get(ctx, types.NamespacedName{ + Namespace: user.Spec.SecretRef.Namespace, + Name: user.Spec.SecretRef.Name, + }, secret) + if err != nil { + if k8sErrors.IsNotFound(err) { + logger.Info("OpenSearch user secret is not found", + "request", req, + ) + r.EventRecorder.Event( + user, models.Warning, models.NotFound, + "Secret is not found, please create a new secret or set an actual reference", + ) + return models.ReconcileRequeue, nil + } + + logger.Error(err, "Cannot get OpenSearch user's secret") + r.EventRecorder.Eventf( + user, models.Warning, models.FetchFailed, + "User's secret fetching has been failed. Reason: %v", err, + ) + + return models.ReconcileRequeue, nil + } + + if user.DeletionTimestamp != nil { + if secret.DeletionTimestamp != nil { + return r.deleteUser(ctx, user, secret, logger) + } + logger.Info("The user resource waits until the secret has been deleted. Please delete the user's secret") + r.EventRecorder.Eventf( + user, models.Warning, models.DeletingEvent, + "The user resource waits until the secret has been deleted. Please delete the user's secret", + ) + return models.ExitReconcile, nil + } + + if user.GetAnnotations()[models.ResourceStateAnnotation] == models.SecretEvent { + if secret.DeletionTimestamp != nil { + logger.Info("The user's secret waits until user has been deleted. Please delete the user resource") + r.EventRecorder.Eventf( + user, models.Warning, models.DeletingEvent, + "The user's secret waits until user has been deleted. Please delete the user resource", + ) + + patch := user.NewPatch() + user.GetAnnotations()[models.ResourceStateAnnotation] = models.DeletingEvent + err = r.Client.Patch(ctx, user, patch) + if err != nil { + logger.Error(err, "Cannot patch OpenSearchUser resource after deleting the secret") + r.EventRecorder.Eventf( + user, models.Warning, models.PatchFailed, + "Patching resource after deleting the secret has been failed", + ) + return models.ReconcileRequeue, nil + } + return models.ExitReconcile, nil + } + } + + patch := client.MergeFrom(secret.DeepCopy()) + + if secret.Labels == nil { + secret.Labels = map[string]string{} + } + + if secret.Labels[models.OpenSearchUserNamespaceLabel] == "" || secret.Labels[models.ControlledByLabel] == "" { + secret.Labels[models.OpenSearchUserNamespaceLabel] = user.Namespace + secret.Labels[models.ControlledByLabel] = user.Name + controllerutil.AddFinalizer(secret, models.DeletionFinalizer) + err = r.Patch(ctx, secret, patch) + if err != nil { + logger.Error(err, "Cannot patch OpenSearch user's secret with deletion secret") + r.EventRecorder.Eventf( + user, models.Warning, models.PatchFailed, + "Resource patching with deletion finalizer has been failed. Reason: %v", err, + ) + return models.ReconcileRequeue, nil + } + + patch = user.NewPatch() + if controllerutil.AddFinalizer(user, models.DeletionFinalizer) { + err = r.Patch(ctx, user, patch) + if err != nil { + logger.Error(err, "Cannot patch OpenSearch user resource with deletion finalizer", + "cluster ID", user.Status.ClusterID, + ) + r.EventRecorder.Eventf( + user, models.Warning, models.PatchFailed, + "Resource patching with deletion finalizer has been failed. Reason: %v", + err, + ) + return models.ReconcileRequeue, nil + } + } + } + + if user.Status.ClusterID != "" && user.Status.State == "" { + return r.createUser(ctx, user, secret, logger) + } + + return models.ExitReconcile, nil +} + +func (r *OpenSearchUserReconciler) createUser( + ctx context.Context, + user *clusterresourcesv1beta1.OpenSearchUser, + secret *k8sCore.Secret, + logger logr.Logger, +) (ctrl.Result, error) { + username, password, err := getUserCreds(secret) + if err != nil { + logger.Error(err, "Cannot get user's credentials during creating on the cluster") + r.EventRecorder.Eventf( + user, models.Warning, models.CreatingEvent, + "Cannot get user's credentials during creating on the cluster. Reason: %v", err, + ) + return models.ReconcileRequeue, nil + } + + err = r.API.CreateUser(user.ToInstaAPI(username, password), user.Status.ClusterID, models.OpenSearchAppKind) + if err != nil { + logger.Error(err, "Cannot create OpenSearch user on Instaclustr", + "username", username, + "cluster ID", user.Status.ClusterID, + ) + r.EventRecorder.Eventf( + user, models.Warning, models.CreationFailed, + "OpenSearch user creating on Instaclustr has been failed. Reason: %v", err, + ) + return models.ReconcileRequeue, nil + } + + patch := user.NewPatch() + user.Status.State = models.Created + err = r.Status().Patch(ctx, user, patch) + if err != nil { + logger.Error(err, "Cannot patch user resource with created state", + "cluster ID", user.Status.ClusterID, + ) + r.EventRecorder.Eventf( + user, models.Warning, models.PatchFailed, + "Resource patching with created state has been failed. Reason: %v", err, + ) + return models.ReconcileRequeue, nil + } + + logger.Info("OpenSearch user has been created", + "cluster ID", user.Status.ClusterID, + ) + + return models.ExitReconcile, nil +} + +func (r *OpenSearchUserReconciler) deleteUser( + ctx context.Context, + user *clusterresourcesv1beta1.OpenSearchUser, + secret *k8sCore.Secret, + logger logr.Logger, +) (ctrl.Result, error) { + username, _, err := getUserCreds(secret) + if err != nil { + logger.Error(err, "Cannot get user's credentials during deleting") + r.EventRecorder.Eventf( + user, models.Warning, models.DeletingEvent, + "Resource deleting has been failed. Reason: %v", err, + ) + return models.ReconcileRequeue, nil + } + + err = r.API.DeleteUser(username, user.Status.ClusterID, models.OpenSearchAppKind) + if err != nil && !errors.Is(err, instaclustr.NotFound) { + logger.Error(err, "Cannot delete OpenSearch user resource from Instaclustr", + "cluster ID", user.Status.ClusterID, + ) + r.EventRecorder.Eventf( + user, models.Warning, models.DeletionFailed, + "Resource deletion on Instaclustr has been failed. Reason: %v", + err, + ) + return models.ReconcileRequeue, nil + } + + r.EventRecorder.Eventf( + user, models.Normal, models.DeletionStarted, + "Resource deletion request has been sent to the Instaclustr API.", + ) + + patch := client.MergeFrom(secret.DeepCopy()) + controllerutil.RemoveFinalizer(secret, models.DeletionFinalizer) + err = r.Patch(ctx, secret, patch) + if err != nil { + logger.Error(err, "Cannot delete finalizer from the user's secret") + r.EventRecorder.Eventf( + user, models.Warning, models.UpdateFailed, + "Deleting finalizer from the user's secret has been failed. Reason: %v", + ) + return models.ReconcileRequeue, nil + } + + patch = user.NewPatch() + controllerutil.RemoveFinalizer(user, models.DeletionFinalizer) + err = r.Patch(ctx, user, patch) + if err != nil { + logger.Error(err, "Cannot delete deletion finalizer from the OpenSearch user resource", + "cluster ID", user.Status.ClusterID, + ) + r.EventRecorder.Eventf( + user, models.Warning, models.PatchFailed, + "Deleting deletion finalizer from the OpenSearch user resource has been failed. Reason: %v", + err, + ) + return models.ReconcileRequeue, nil + } + + logger.Info("OpenSearch user has been deleted", + "cluster ID", user.Status.ClusterID, + ) + r.EventRecorder.Eventf( + user, models.Normal, models.Deleted, + "OpenSearchUser resource has been deleted deleted", + ) + + return models.ExitReconcile, nil +} + +func (r *OpenSearchUserReconciler) newUserReconcileRequest(secret client.Object) []reconcile.Request { + userNamespacedName := types.NamespacedName{ + Namespace: secret.GetLabels()[models.OpenSearchUserNamespaceLabel], + Name: secret.GetLabels()[models.ControlledByLabel], + } + + user := &clusterresourcesv1beta1.OpenSearchUser{} + + err := r.Get(context.Background(), userNamespacedName, user) + if err != nil { + return nil + } + + patch := user.NewPatch() + + annots := user.GetAnnotations() + if annots == nil { + annots = map[string]string{} + } + annots[models.ResourceStateAnnotation] = models.SecretEvent + + err = r.Patch(context.Background(), user, patch) + if err != nil { + return []reconcile.Request{} + } + + return []reconcile.Request{{NamespacedName: userNamespacedName}} +} + +// SetupWithManager sets up the controller with the Manager. +func (r *OpenSearchUserReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&clusterresourcesv1beta1.OpenSearchUser{}). + Watches(&source.Kind{Type: &k8sCore.Secret{}}, handler.EnqueueRequestsFromMapFunc(r.newUserReconcileRequest)). + Complete(r) +} diff --git a/controllers/clusters/datatest/opensearch_v1beta1.yaml b/controllers/clusters/datatest/opensearch_v1beta1.yaml index 7a29e407f..6b9b284b3 100644 --- a/controllers/clusters/datatest/opensearch_v1beta1.yaml +++ b/controllers/clusters/datatest/opensearch_v1beta1.yaml @@ -6,6 +6,9 @@ spec: alertingPlugin: false anomalyDetectionPlugin: false asynchronousSearchPlugin: false +# userRef: +# name: test-user +# namespace: default clusterManagerNodes: - dedicatedManager: false nodeSize: SRH-DEV-t4g.small-5 diff --git a/controllers/clusters/opensearch_controller.go b/controllers/clusters/opensearch_controller.go index 5f7017d1f..a89e3d98e 100644 --- a/controllers/clusters/opensearch_controller.go +++ b/controllers/clusters/opensearch_controller.go @@ -227,6 +227,22 @@ func (r *OpenSearchReconciler) HandleCreateCluster( "cluster name", o.Name, "cluster ID", o.Status.ID, "api version", o.APIVersion, "namespace", o.Namespace) + if o.Spec.UserRef != nil { + patch := o.NewPatch() + o.Annotations[models.ResourceStateAnnotation] = models.UpdatingEvent + err = r.Patch(ctx, o, patch) + if err != nil { + logger.Error(err, "Cannot patch cluster", + "cluster name", o.Spec.Name, + "cluster ID", o.Status.ID, + ) + r.EventRecorder.Eventf(o, models.Warning, models.PatchFailed, + "Cluster resource patch is failed. Reason: %v", err, + ) + return models.ReconcileRequeue + } + } + return models.ExitReconcile } @@ -290,6 +306,13 @@ func (r *OpenSearchReconciler) HandleUpdateCluster( } } + if o.Spec.UserRef != nil { + err = r.handleCreateUser(ctx, o, logger) + if err != nil { + return models.ReconcileRequeue + } + } + patch := o.NewPatch() o.Annotations[models.ResourceStateAnnotation] = models.UpdatedEvent err = r.Patch(ctx, o, patch) @@ -312,6 +335,54 @@ func (r *OpenSearchReconciler) HandleUpdateCluster( return models.ExitReconcile } +func (r *OpenSearchReconciler) handleCreateUser( + ctx context.Context, + o *v1beta1.OpenSearch, + logger logr.Logger, +) error { + req := types.NamespacedName{ + Namespace: o.Spec.UserRef.Namespace, + Name: o.Spec.UserRef.Name, + } + + u := &clusterresourcesv1beta1.OpenSearchUser{} + err := r.Get(ctx, req, u) + if err != nil { + if k8serrors.IsNotFound(err) { + logger.Info("OpenSearch user is not found", "request", req) + r.EventRecorder.Event( + u, models.Warning, "Not Found", + "User is not found, create a new one OpenSearch User or provide correct userRef.", + ) + return err + } + + logger.Error(err, "Cannot get OpenSearch user secret", "user", u.Spec) + return err + } + + if u.Status.ClusterID == "" { + u.Status.ClusterID = o.Status.ID + err = r.Status().Update(ctx, u) + if err != nil { + logger.Error(err, "Cannot update OpenSearch User with cluster ID", + "cluster name", o.Spec.Name, + "cluster ID", o.Status.ID, + ) + r.EventRecorder.Eventf(o, models.Warning, models.CreationFailed, + "Cannot update OpenSearch User with cluster ID. Reason: %v", err, + ) + return err + } + logger.Info("OpenSearch user has been assigned to cluster", + "cluster name", o.Spec.Name, + "cluster ID", o.Status.ID, + ) + } + + return nil +} + func (r *OpenSearchReconciler) handleExternalChanges(o, iO *v1beta1.OpenSearch, l logr.Logger) reconcile.Result { if o.Annotations[models.AllowSpecAmendAnnotation] != models.True { l.Info("Update is blocked until k8s resource specification is equal with Instaclustr", diff --git a/main.go b/main.go index 9263fa998..e651c4572 100644 --- a/main.go +++ b/main.go @@ -409,6 +409,15 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "CassandraUser") os.Exit(1) } + if err = (&clusterresourcescontrollers.OpenSearchUserReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + API: instaClient, + EventRecorder: eventRecorder, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "OpenSearchUser") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/pkg/instaclustr/client.go b/pkg/instaclustr/client.go index 96f3099e0..6423f793d 100644 --- a/pkg/instaclustr/client.go +++ b/pkg/instaclustr/client.go @@ -2153,6 +2153,10 @@ func (c *Client) DeleteUser(username, clusterID, app string) error { return err } + if resp.StatusCode == http.StatusNotFound { + return NotFound + } + if resp.StatusCode != http.StatusOK { return fmt.Errorf("status code: %d, message: %s", resp.StatusCode, body) } diff --git a/pkg/models/apiv1.go b/pkg/models/apiv1.go index ce662049f..fdd1f4d26 100644 --- a/pkg/models/apiv1.go +++ b/pkg/models/apiv1.go @@ -179,3 +179,13 @@ type InstaUser struct { Password string `json:"password"` InitialPermission string `json:"initial-permissions"` } + +type InstaOpenSearchUser struct { + *InstaUser `json:",inline"` + Options Options `json:"options"` +} + +type Options struct { + IndexPattern string `json:"indexPattern"` + Role string `json:"role"` +} diff --git a/pkg/models/operator.go b/pkg/models/operator.go index 1a5ea801c..c3d1fe4ed 100644 --- a/pkg/models/operator.go +++ b/pkg/models/operator.go @@ -38,6 +38,7 @@ const ( ClusterresourcesV1beta1APIVersion = "clusterresources.instaclustr.com/v1beta1" RedisUserNamespaceLabel = "instaclustr.com/redisUserNamespace" CassandraUserNamespaceLabel = "instaclustr.com/cassandraUserNamespace" + OpenSearchUserNamespaceLabel = "instaclustr.com/openSearchUserNamespace" CassandraKind = "Cassandra" CassandraChildPrefix = "cassandra-"