From f45c63314feb80d16d5ae5b458f17ecf9d19f41e Mon Sep 17 00:00:00 2001 From: "mykyta.oleksiienko" Date: Thu, 31 Aug 2023 14:46:45 +0300 Subject: [PATCH] issue-522,implementation for OpenSearch Egress Rules lifecycle on APIv2 --- PROJECT | 12 + .../v1beta1/opensearchegressrules_types.go | 63 ++++++ .../v1beta1/opensearchegressrules_webhook.go | 93 ++++++++ .../v1beta1/webhook_suite_test.go | 3 + .../v1beta1/zz_generated.deepcopy.go | 89 ++++++++ apis/clusters/v1beta1/opensearch_types.go | 2 +- ...instaclustr.com_opensearchegressrules.yaml | 60 +++++ ...clusters.instaclustr.com_opensearches.yaml | 28 +-- config/crd/kustomization.yaml | 3 + ...lusterresources_opensearchegressrules.yaml | 7 + ...lusterresources_opensearchegressrules.yaml | 16 ++ ...ces_opensearchegressrules_editor_role.yaml | 31 +++ ...ces_opensearchegressrules_viewer_role.yaml | 27 +++ config/rbac/role.yaml | 26 +++ ...sources_v1beta1_opensearchegressrules.yaml | 15 ++ .../samples/clusters_v1beta1_opensearch.yaml | 12 +- config/webhook/manifests.yaml | 20 ++ .../opensearchegressrules_controller.go | 210 ++++++++++++++++++ main.go | 13 ++ pkg/instaclustr/client.go | 87 ++++++++ pkg/instaclustr/config.go | 1 + pkg/instaclustr/interfaces.go | 3 + pkg/instaclustr/mock/client.go | 12 + pkg/models/validation.go | 13 +- 24 files changed, 819 insertions(+), 27 deletions(-) create mode 100644 apis/clusterresources/v1beta1/opensearchegressrules_types.go create mode 100644 apis/clusterresources/v1beta1/opensearchegressrules_webhook.go create mode 100644 config/crd/bases/clusterresources.instaclustr.com_opensearchegressrules.yaml create mode 100644 config/crd/patches/cainjection_in_clusterresources_opensearchegressrules.yaml create mode 100644 config/crd/patches/webhook_in_clusterresources_opensearchegressrules.yaml create mode 100644 config/rbac/clusterresources_opensearchegressrules_editor_role.yaml create mode 100644 config/rbac/clusterresources_opensearchegressrules_viewer_role.yaml create mode 100644 config/samples/clusterresources_v1beta1_opensearchegressrules.yaml create mode 100644 controllers/clusterresources/opensearchegressrules_controller.go diff --git a/PROJECT b/PROJECT index 42684d984..87253fe87 100644 --- a/PROJECT +++ b/PROJECT @@ -323,6 +323,18 @@ resources: webhooks: validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: instaclustr.com + group: clusterresources + kind: OpenSearchEgressRules + path: github.com/instaclustr/operator/apis/clusterresources/v1beta1 + version: v1beta1 + webhooks: + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true diff --git a/apis/clusterresources/v1beta1/opensearchegressrules_types.go b/apis/clusterresources/v1beta1/opensearchegressrules_types.go new file mode 100644 index 000000000..88383490d --- /dev/null +++ b/apis/clusterresources/v1beta1/opensearchegressrules_types.go @@ -0,0 +1,63 @@ +/* +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" +) + +type OpenSearchEgressRulesSpec struct { + ClusterID string `json:"clusterId"` + OpenSearchBindingID string `json:"openSearchBindingId"` + Source string `json:"source"` + Type string `json:"type,omitempty"` +} + +type OpenSearchEgressRulesStatus struct { + ID string `json:"id,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// OpenSearchEgressRules is the Schema for the opensearchegressrules API +type OpenSearchEgressRules struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec OpenSearchEgressRulesSpec `json:"spec,omitempty"` + Status OpenSearchEgressRulesStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// OpenSearchEgressRulesList contains a list of OpenSearchEgressRules +type OpenSearchEgressRulesList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []OpenSearchEgressRules `json:"items"` +} + +func (er *OpenSearchEgressRules) NewPatch() client.Patch { + old := er.DeepCopy() + return client.MergeFrom(old) +} + +func init() { + SchemeBuilder.Register(&OpenSearchEgressRules{}, &OpenSearchEgressRulesList{}) +} diff --git a/apis/clusterresources/v1beta1/opensearchegressrules_webhook.go b/apis/clusterresources/v1beta1/opensearchegressrules_webhook.go new file mode 100644 index 000000000..f505e6fe4 --- /dev/null +++ b/apis/clusterresources/v1beta1/opensearchegressrules_webhook.go @@ -0,0 +1,93 @@ +/* +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 ( + "fmt" + "regexp" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/strings/slices" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/instaclustr/operator/pkg/models" +) + +// log is for logging in this package. + +var opensearchegressruleslog = logf.Log.WithName("opensearchegressrules-resource") + +var destinationTypes = []string{"SLACK", "WEBHOOK", "CUSTOM_WEBHOOK", "CHIME"} +var sourcePlugins = []string{"NOTIFICATIONS", "ALERTING"} + +func (r *OpenSearchEgressRules) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +//+kubebuilder:webhook:path=/validate-clusterresources-instaclustr-com-v1beta1-opensearchegressrules,mutating=false,failurePolicy=fail,sideEffects=None,groups=clusterresources.instaclustr.com,resources=opensearchegressrules,verbs=create;update,versions=v1beta1,name=vopensearchegressrules.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &OpenSearchEgressRules{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *OpenSearchEgressRules) ValidateCreate() error { + opensearchegressruleslog.Info("validate create", "name", r.Name) + matched, err := regexp.MatchString(models.OpenSearchBindingIDPattern, r.Spec.OpenSearchBindingID) + if err != nil { + return fmt.Errorf("can`t match openSearchBindingId to pattern: %s, error: %w", models.OpenSearchBindingIDPattern, err) + } + if !matched { + return fmt.Errorf("mismatching openSearchBindingId to pattern: %s", models.OpenSearchBindingIDPattern) + } + + if !slices.Contains(sourcePlugins, r.Spec.Source) { + return fmt.Errorf("the source should be equal to one of the options: %q , got: %q", sourcePlugins, r.Spec.Source) + } + + if !slices.Contains(destinationTypes, r.Spec.Type) { + return fmt.Errorf("the type should be equal to one of the options: %q , got: %q", destinationTypes, r.Spec.Type) + } + + return nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *OpenSearchEgressRules) ValidateUpdate(old runtime.Object) error { + opensearchegressruleslog.Info("validate update", "name", r.Name) + + oldRules := old.(*OpenSearchEgressRules) + + if r.Status.ID == "" { + return r.ValidateCreate() + } + + if r.Spec != oldRules.Spec { + return models.ErrImmutableSpec + } + + return nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *OpenSearchEgressRules) ValidateDelete() error { + opensearchegressruleslog.Info("validate delete", "name", r.Name) + + return nil +} diff --git a/apis/clusterresources/v1beta1/webhook_suite_test.go b/apis/clusterresources/v1beta1/webhook_suite_test.go index b8682394b..7b6882975 100644 --- a/apis/clusterresources/v1beta1/webhook_suite_test.go +++ b/apis/clusterresources/v1beta1/webhook_suite_test.go @@ -129,6 +129,9 @@ var _ = BeforeSuite(func() { err = (&RedisUser{}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) + err = (&OpenSearchEgressRules{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + err = (&AWSEndpointServicePrincipal{}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) diff --git a/apis/clusterresources/v1beta1/zz_generated.deepcopy.go b/apis/clusterresources/v1beta1/zz_generated.deepcopy.go index db442acec..6749d8ee7 100644 --- a/apis/clusterresources/v1beta1/zz_generated.deepcopy.go +++ b/apis/clusterresources/v1beta1/zz_generated.deepcopy.go @@ -1299,6 +1299,95 @@ 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 *OpenSearchEgressRules) DeepCopyInto(out *OpenSearchEgressRules) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenSearchEgressRules. +func (in *OpenSearchEgressRules) DeepCopy() *OpenSearchEgressRules { + if in == nil { + return nil + } + out := new(OpenSearchEgressRules) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OpenSearchEgressRules) 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 *OpenSearchEgressRulesList) DeepCopyInto(out *OpenSearchEgressRulesList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OpenSearchEgressRules, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenSearchEgressRulesList. +func (in *OpenSearchEgressRulesList) DeepCopy() *OpenSearchEgressRulesList { + if in == nil { + return nil + } + out := new(OpenSearchEgressRulesList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OpenSearchEgressRulesList) 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 *OpenSearchEgressRulesSpec) DeepCopyInto(out *OpenSearchEgressRulesSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenSearchEgressRulesSpec. +func (in *OpenSearchEgressRulesSpec) DeepCopy() *OpenSearchEgressRulesSpec { + if in == nil { + return nil + } + out := new(OpenSearchEgressRulesSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenSearchEgressRulesStatus) DeepCopyInto(out *OpenSearchEgressRulesStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenSearchEgressRulesStatus. +func (in *OpenSearchEgressRulesStatus) DeepCopy() *OpenSearchEgressRulesStatus { + if in == nil { + return nil + } + out := new(OpenSearchEgressRulesStatus) + in.DeepCopyInto(out) + 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 diff --git a/apis/clusters/v1beta1/opensearch_types.go b/apis/clusters/v1beta1/opensearch_types.go index 8856b2d98..b471405a5 100644 --- a/apis/clusters/v1beta1/opensearch_types.go +++ b/apis/clusters/v1beta1/opensearch_types.go @@ -43,7 +43,7 @@ type OpenSearchSpec struct { ICUPlugin bool `json:"icuPlugin,omitempty"` AsynchronousSearchPlugin bool `json:"asynchronousSearchPlugin,omitempty"` KNNPlugin bool `json:"knnPlugin,omitempty"` - Dashboards []*OpenSearchDashboards `json:"dashboards,omitempty"` + Dashboards []*OpenSearchDashboards `json:"opensearchDashboards,omitempty"` ReportingPlugin bool `json:"reportingPlugin,omitempty"` SQLPlugin bool `json:"sqlPlugin,omitempty"` NotificationsPlugin bool `json:"notificationsPlugin,omitempty"` diff --git a/config/crd/bases/clusterresources.instaclustr.com_opensearchegressrules.yaml b/config/crd/bases/clusterresources.instaclustr.com_opensearchegressrules.yaml new file mode 100644 index 000000000..323c22099 --- /dev/null +++ b/config/crd/bases/clusterresources.instaclustr.com_opensearchegressrules.yaml @@ -0,0 +1,60 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: opensearchegressrules.clusterresources.instaclustr.com +spec: + group: clusterresources.instaclustr.com + names: + kind: OpenSearchEgressRules + listKind: OpenSearchEgressRulesList + plural: opensearchegressrules + singular: opensearchegressrules + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: OpenSearchEgressRules is the Schema for the opensearchegressrules + 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: + properties: + clusterId: + type: string + openSearchBindingId: + type: string + source: + type: string + type: + type: string + required: + - clusterId + - openSearchBindingId + - source + type: object + status: + properties: + id: + type: string + 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 628ea13f4..aaedd586b 100644 --- a/config/crd/bases/clusters.instaclustr.com_opensearches.yaml +++ b/config/crd/bases/clusters.instaclustr.com_opensearches.yaml @@ -68,20 +68,6 @@ spec: - nodeSize type: object type: array - dashboards: - items: - properties: - nodeSize: - type: string - oidcProvider: - type: string - version: - type: string - required: - - nodeSize - - version - type: object - type: array dataCentres: items: properties: @@ -150,6 +136,20 @@ spec: type: string notificationsPlugin: type: boolean + opensearchDashboards: + items: + properties: + nodeSize: + type: string + oidcProvider: + type: string + version: + type: string + required: + - nodeSize + - version + type: object + type: array pciCompliance: description: The PCI compliance standards relate to the security of user data and transactional information. Can only be applied clusters diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 0278a542e..30c40a399 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -29,6 +29,7 @@ resources: - bases/clusterresources.instaclustr.com_awsendpointserviceprincipals.yaml - bases/clusterresources.instaclustr.com_exclusionwindows.yaml - bases/clusterresources.instaclustr.com_postgresqlusers.yaml +- bases/clusterresources.instaclustr.com_opensearchegressrules.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -60,6 +61,7 @@ patchesStrategicMerge: #- patches/webhook_in_awsendpointserviceprincipals.yaml #- patches/webhook_in_exclusionwindows.yaml #- patches/webhook_in_postgresqlusers.yaml +#- patches/webhook_in_opensearchegressrules.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -94,6 +96,7 @@ patchesStrategicMerge: #- patches/cainjection_in_awsendpointserviceprincipals.yaml #- patches/cainjection_in_exclusionwindows.yaml #- patches/cainjection_in_postgresqlusers.yaml +#- patches/cainjection_in_opensearchegressrules.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_clusterresources_opensearchegressrules.yaml b/config/crd/patches/cainjection_in_clusterresources_opensearchegressrules.yaml new file mode 100644 index 000000000..f761c4995 --- /dev/null +++ b/config/crd/patches/cainjection_in_clusterresources_opensearchegressrules.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: opensearchegressrules.clusterresources.instaclustr.com diff --git a/config/crd/patches/webhook_in_clusterresources_opensearchegressrules.yaml b/config/crd/patches/webhook_in_clusterresources_opensearchegressrules.yaml new file mode 100644 index 000000000..8ff254cfa --- /dev/null +++ b/config/crd/patches/webhook_in_clusterresources_opensearchegressrules.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: opensearchegressrules.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_opensearchegressrules_editor_role.yaml b/config/rbac/clusterresources_opensearchegressrules_editor_role.yaml new file mode 100644 index 000000000..2def30307 --- /dev/null +++ b/config/rbac/clusterresources_opensearchegressrules_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit opensearchegressrules. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: opensearchegressrules-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: opensearchegressrules-editor-role +rules: +- apiGroups: + - clusterresources.instaclustr.com + resources: + - opensearchegressrules + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - clusterresources.instaclustr.com + resources: + - opensearchegressrules/status + verbs: + - get diff --git a/config/rbac/clusterresources_opensearchegressrules_viewer_role.yaml b/config/rbac/clusterresources_opensearchegressrules_viewer_role.yaml new file mode 100644 index 000000000..fa078f086 --- /dev/null +++ b/config/rbac/clusterresources_opensearchegressrules_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view opensearchegressrules. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: opensearchegressrules-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: opensearchegressrules-viewer-role +rules: +- apiGroups: + - clusterresources.instaclustr.com + resources: + - opensearchegressrules + verbs: + - get + - list + - watch +- apiGroups: + - clusterresources.instaclustr.com + resources: + - opensearchegressrules/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 26bf3de63..ee225136e 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -369,6 +369,32 @@ rules: - get - patch - update +- apiGroups: + - clusterresources.instaclustr.com + resources: + - opensearchegressrules + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - clusterresources.instaclustr.com + resources: + - opensearchegressrules/finalizers + verbs: + - update +- apiGroups: + - clusterresources.instaclustr.com + resources: + - opensearchegressrules/status + verbs: + - get + - patch + - update - apiGroups: - clusterresources.instaclustr.com resources: diff --git a/config/samples/clusterresources_v1beta1_opensearchegressrules.yaml b/config/samples/clusterresources_v1beta1_opensearchegressrules.yaml new file mode 100644 index 000000000..1ae21d303 --- /dev/null +++ b/config/samples/clusterresources_v1beta1_opensearchegressrules.yaml @@ -0,0 +1,15 @@ +apiVersion: clusterresources.instaclustr.com/v1beta1 +kind: OpenSearchEgressRules +metadata: + labels: + app.kubernetes.io/name: opensearchegressrules + app.kubernetes.io/instance: opensearchegressrules-sample + app.kubernetes.io/part-of: operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: operator + name: opensearchegressrules-sample +spec: + clusterId: "447635ec-78a0-4aad-8d64-512e006acb0e" + openSearchBindingId: "KBCNR4sBwHizf9kKDuii" + source: "NOTIFICATIONS" + type: "SLACK" diff --git a/config/samples/clusters_v1beta1_opensearch.yaml b/config/samples/clusters_v1beta1_opensearch.yaml index 3e006b323..beb1ed157 100644 --- a/config/samples/clusters_v1beta1_opensearch.yaml +++ b/config/samples/clusters_v1beta1_opensearch.yaml @@ -11,7 +11,7 @@ metadata: annotations: test.annotation/first: testAnnotation spec: - name: OpenSearch-example + name: opensearch-test alertingPlugin: false anomalyDetectionPlugin: false asynchronousSearchPlugin: false @@ -38,11 +38,11 @@ spec: knnPlugin: false loadBalancer: false notificationsPlugin: false - # opensearchDashboards: - # - nodeSize: SRH-DEV-t4g.small-5 - # oidcProvider: '' - # version: opensearch-dashboards:2.5.0 - version: 2.7.0 +# opensearchDashboards: +# - nodeSize: SRH-DEV-t4g.small-5 +# oidcProvider: '' +# version: opensearch-dashboards:2.5.0 + version: 2.9.0 pciCompliance: false privateNetworkCluster: false reportingPlugin: false diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 6fe20607d..0e126171b 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -652,6 +652,26 @@ webhooks: resources: - nodereloads sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-clusterresources-instaclustr-com-v1beta1-opensearchegressrules + failurePolicy: Fail + name: vopensearchegressrules.kb.io + rules: + - apiGroups: + - clusterresources.instaclustr.com + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - opensearchegressrules + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/controllers/clusterresources/opensearchegressrules_controller.go b/controllers/clusterresources/opensearchegressrules_controller.go new file mode 100644 index 000000000..0515b90c5 --- /dev/null +++ b/controllers/clusterresources/opensearchegressrules_controller.go @@ -0,0 +1,210 @@ +/* +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" + "fmt" + + clusterresourcesv1beta1 "github.com/instaclustr/operator/apis/clusterresources/v1beta1" + "github.com/instaclustr/operator/pkg/instaclustr" + "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/scheduler" + + "github.com/go-logr/logr" + 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/log" +) + +// OpenSearchEgressRulesReconciler reconciles a OpenSearchEgressRules object +type OpenSearchEgressRulesReconciler struct { + client.Client + Scheme *runtime.Scheme + API instaclustr.API + Scheduler scheduler.Interface + EventRecorder record.EventRecorder +} + +//+kubebuilder:rbac:groups=clusterresources.instaclustr.com,resources=opensearchegressrules,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=clusterresources.instaclustr.com,resources=opensearchegressrules/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=clusterresources.instaclustr.com,resources=opensearchegressrules/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. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the OpenSearchEgressRules object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// 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 *OpenSearchEgressRulesReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + l := log.FromContext(ctx) + + rule := &clusterresourcesv1beta1.OpenSearchEgressRules{} + err := r.Client.Get(ctx, types.NamespacedName{ + Namespace: req.Namespace, + Name: req.Name, + }, rule) + if err != nil { + if k8serrors.IsNotFound(err) { + return models.ExitReconcile, nil + } + + l.Error(err, "Unable to fetch OpenSearch Egress Rules resource") + + return models.ReconcileRequeue, err + } + + // It`s handling resource deletion + if rule.DeletionTimestamp != nil { + err = r.handleDelete(ctx, l, rule) + if err != nil { + return models.ReconcileRequeue, err + } + + return models.ExitReconcile, nil + } + + // It`s handling resource creation + if rule.Status.ID == "" { + err = r.handleCreate(ctx, l, rule) + if err != nil { + return models.ReconcileRequeue, nil + } + + return models.ExitReconcile, nil + } + + return models.ExitReconcile, nil +} + +func (r *OpenSearchEgressRulesReconciler) handleCreate(ctx context.Context, l logr.Logger, rule *clusterresourcesv1beta1.OpenSearchEgressRules) error { + patch := rule.NewPatch() + + if rule.Status.ID == "" { + rule.Status.ID = fmt.Sprintf("%s~%s~%s", rule.Spec.ClusterID, rule.Spec.Source, rule.Spec.OpenSearchBindingID) + } + + _, err := r.API.GetOpenSearchEgressRule(rule.Status.ID) + if !errors.Is(err, instaclustr.NotFound) && err != nil { + l.Error(err, "failed to get OpenSearch Egress Rule resource from Instaclustr") + r.EventRecorder.Eventf(rule, models.Warning, models.CreationFailed, + "Failed to get OpenSearch Egress Rule from Instaclustr. Reason: %v", err, + ) + + return err + } + + if errors.Is(err, instaclustr.NotFound) { + err = r.API.CreateOpenSearchEgressRules(rule) + if err != nil { + l.Error(err, "failed to create OpenSearch Egress Rule resource on Instaclustr") + r.EventRecorder.Eventf(rule, models.Warning, models.CreationFailed, + "Failed to create OpenSearch Egress Rule on Instaclustr. Reason: %v", err, + ) + + return err + } + } + + err = r.Status().Patch(ctx, rule, patch) + if err != nil { + l.Error(err, "failed to patch OpenSearch Egress Rule status with its id") + r.EventRecorder.Eventf(rule, models.Warning, models.PatchFailed, + "Failed to patch OpenSearch Egress Rule with its id. Reason: %v", err, + ) + + return err + } + + controllerutil.AddFinalizer(rule, models.DeletionFinalizer) + err = r.Patch(ctx, rule, patch) + if err != nil { + l.Error(err, "failed to patch OpenSearch Egress Rule with finalizer") + r.EventRecorder.Eventf(rule, models.Warning, models.PatchFailed, + "Failed to patch OpenSearch Egress Rule with finalizer. Reason: %v", err, + ) + + return err + } + + l.Info("OpenSearch Egress Rule has been created") + r.EventRecorder.Event(rule, models.Normal, models.Created, + "OpenSearch Egress Rule has been created", + ) + + return nil +} + +func (r *OpenSearchEgressRulesReconciler) handleDelete(ctx context.Context, logger logr.Logger, rule *clusterresourcesv1beta1.OpenSearchEgressRules) error { + if rule.Status.ID == "" { + rule.Status.ID = fmt.Sprintf("%s~%s~%s", rule.Spec.ClusterID, rule.Spec.Source, rule.Spec.OpenSearchBindingID) + } + + _, err := r.API.GetOpenSearchEgressRule(rule.Status.ID) + if !errors.Is(err, instaclustr.NotFound) && err != nil { + logger.Error(err, "failed to get OpenSearch Egress Rule resource from Instaclustr") + r.EventRecorder.Eventf(rule, models.Warning, models.CreationFailed, + "Failed to get OpenSearch Egress Rule from Instaclustr. Reason: %v", err, + ) + + return err + } + + if !errors.Is(err, instaclustr.NotFound) { + err = r.API.DeleteOpenSearchEgressRule(rule.Status.ID) + if err != nil && !errors.Is(err, instaclustr.NotFound) { + logger.Error(err, "failed to delete OpenSearch Egress Rule on Instaclustr") + r.EventRecorder.Eventf(rule, models.Warning, models.DeletionFailed, + "Failed to delete OpenSearch Egress Rule on Instaclustr. Reason: %v", err, + ) + + return err + } + } + + patch := rule.NewPatch() + controllerutil.RemoveFinalizer(rule, models.DeletionFinalizer) + err = r.Patch(ctx, rule, patch) + if err != nil { + logger.Error(err, "failed to delete finalizer OpenSearch Egress Rule") + r.EventRecorder.Eventf(rule, models.Warning, models.PatchFailed, + "Failed to delete finalizer from OpenSearch Egress Rule. Reason: %v", err, + ) + + return err + } + + logger.Info("OpenSearch Egress Rule has been deleted") + + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *OpenSearchEgressRulesReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&clusterresourcesv1beta1.OpenSearchEgressRules{}). + Complete(r) +} diff --git a/main.go b/main.go index f1854f6eb..f11e46971 100644 --- a/main.go +++ b/main.go @@ -487,6 +487,19 @@ func main() { setupLog.Error(err, "unable to create webhook", "webhook", "PostgreSQLUser") os.Exit(1) } + if err = (&clusterresourcescontrollers.OpenSearchEgressRulesReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + API: instaClient, + EventRecorder: eventRecorder, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "OpenSearchEgressRules") + os.Exit(1) + } + if err = (&clusterresourcesv1beta1.OpenSearchEgressRules{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "OpenSearchEgressRules") + 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 4bfa8119c..de7185691 100644 --- a/pkg/instaclustr/client.go +++ b/pkg/instaclustr/client.go @@ -2320,3 +2320,90 @@ func (c *Client) GetAWSVPCPeering(peerID string) (*models.AWSVPCPeering, error) return &vpcPeering, nil } + +func (c *Client) CreateOpenSearchEgressRules(rule *clusterresourcesv1beta1.OpenSearchEgressRules) error { + url := c.serverHostname + OpenSearchEgressRulesEndpoint + + b, err := json.Marshal(rule.Spec) + if err != nil { + return err + } + + resp, err := c.DoRequest(url, http.MethodPost, b) + if err != nil { + return err + } + + defer resp.Body.Close() + b, err = io.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusAccepted { + return fmt.Errorf("status code: %d, message: %s", resp.StatusCode, b) + } + + err = json.Unmarshal(b, &rule.Status) + if err != nil { + return err + } + + return nil +} + +func (c *Client) GetOpenSearchEgressRule(id string) (*clusterresourcesv1beta1.OpenSearchEgressRules, error) { + rule := clusterresourcesv1beta1.OpenSearchEgressRules{} + url := c.serverHostname + OpenSearchEgressRulesEndpoint + "/" + id + + resp, err := c.DoRequest(url, http.MethodGet, nil) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode == http.StatusNotFound { + return nil, NotFound + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("status code: %d, message: %s", resp.StatusCode, b) + } + + err = json.Unmarshal(b, &rule) + if err != nil { + return nil, err + } + + return &rule, nil +} + +func (c *Client) DeleteOpenSearchEgressRule(id string) error { + url := c.serverHostname + OpenSearchEgressRulesEndpoint + "/" + id + + resp, err := c.DoRequest(url, http.MethodDelete, nil) + if err != nil { + return err + } + + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode == http.StatusNotFound { + return NotFound + } + + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("status code: %d, message: %s", resp.StatusCode, b) + } + + return nil +} diff --git a/pkg/instaclustr/config.go b/pkg/instaclustr/config.go index 8bd94f499..056854f61 100644 --- a/pkg/instaclustr/config.go +++ b/pkg/instaclustr/config.go @@ -54,6 +54,7 @@ const ( AWSEncryptionKeyEndpoint = "/cluster-management/v2/resources/providers/aws/encryption-keys/v2/" ListAppsVersionsEndpoint = "%s/cluster-management/v2/data-sources/applications/%s/versions/v2/" ClusterSettingsEndpoint = "%s/cluster-management/v2/operations/clusters/v2/%s/change-settings/v2" + OpenSearchEgressRulesEndpoint = "/cluster-management/v2/resources/applications/opensearch/egress-rules/v2" AWSEndpointServicePrincipalEndpoint = "/cluster-management/v2/resources/aws-endpoint-service-principals/v2/" ClusterResizeOperationsEndpoint = "/cluster-management/v2/data-sources/cluster-data-centres/v2/%s/resize/operations/v2" ) diff --git a/pkg/instaclustr/interfaces.go b/pkg/instaclustr/interfaces.go index 6e19e5041..47a3080c4 100644 --- a/pkg/instaclustr/interfaces.go +++ b/pkg/instaclustr/interfaces.go @@ -101,6 +101,9 @@ type API interface { GetDefaultCredentialsV1(clusterID string) (string, string, error) UpdateClusterSettings(clusterID string, settings *models.ClusterSettings) error GetAWSEndpointServicePrincipal(id string) (*models.AWSEndpointServicePrincipal, error) + CreateOpenSearchEgressRules(rule *clusterresourcesv1beta1.OpenSearchEgressRules) error + GetOpenSearchEgressRule(id string) (*clusterresourcesv1beta1.OpenSearchEgressRules, error) + DeleteOpenSearchEgressRule(id string) error CreateAWSEndpointServicePrincipal(spec any) ([]byte, error) DeleteAWSEndpointServicePrincipal(principalID string) error GetResizeOperationsByClusterDataCentreID(cdcID string) ([]*v1beta1.ResizeOperation, error) diff --git a/pkg/instaclustr/mock/client.go b/pkg/instaclustr/mock/client.go index 36c8b1e32..22159ab72 100644 --- a/pkg/instaclustr/mock/client.go +++ b/pkg/instaclustr/mock/client.go @@ -379,3 +379,15 @@ func (c *mockClient) GetAWSVPCPeering(peerID string) (*models.AWSVPCPeering, err func (c *mockClient) GetAWSEndpointServicePrincipal(id string) (*models.AWSEndpointServicePrincipal, error) { panic("GetAWSEndpointServicePrincipal: is not implemented") } + +func (c *mockClient) CreateOpenSearchEgressRules(rule *clusterresourcesv1beta1.OpenSearchEgressRules) error { + panic("CreateOpenSearchEgressRules: is not implemented") +} + +func (c *mockClient) GetOpenSearchEgressRule(id string) (*clusterresourcesv1beta1.OpenSearchEgressRules, error) { + panic("GetOpenSearchEgressRule: is not implemented") +} + +func (c *mockClient) DeleteOpenSearchEgressRule(id string) error { + panic("DeleteOpenSearchEgressRule: is not implemented") +} diff --git a/pkg/models/validation.go b/pkg/models/validation.go index f1b754b42..d8afd0257 100644 --- a/pkg/models/validation.go +++ b/pkg/models/validation.go @@ -52,12 +52,13 @@ var ( ACLPatternType = []string{"LITERAL", "PREFIXED"} ACLOperation = []string{"ALL", "READ", "WRITE", "CREATE", "DELETE", "ALTER", "DESCRIBE", "CLUSTER_ACTION", "DESCRIBE_CONFIGS", "ALTER_CONFIGS", "IDEMPOTENT_WRITE"} - ACLResourceType = []string{"CLUSTER", "TOPIC", "GROUP", "DELEGATION_TOKEN", "TRANSACTIONAL_ID"} - ACLUserPrefix = "User:" - ACLPrincipalRegExp = "^User:.*$" - S3URIRegExp = "^s3:\\/\\/[a-zA-Z0-9_-]+[^\\/]$" - DependencyVPCs = []string{"TARGET_VPC", "VPC_PEERED", "SEPARATE_VPC"} - EncryptionKeyAliasRegExp = "^[a-zA-Z0-9_-]{1}[a-zA-Z0-9 _-]*$" + ACLResourceType = []string{"CLUSTER", "TOPIC", "GROUP", "DELEGATION_TOKEN", "TRANSACTIONAL_ID"} + ACLUserPrefix = "User:" + ACLPrincipalRegExp = "^User:.*$" + S3URIRegExp = "^s3:\\/\\/[a-zA-Z0-9_-]+[^\\/]$" + DependencyVPCs = []string{"TARGET_VPC", "VPC_PEERED", "SEPARATE_VPC"} + EncryptionKeyAliasRegExp = "^[a-zA-Z0-9_-]{1}[a-zA-Z0-9 _-]*$" + OpenSearchBindingIDPattern = "[\\w-]+" CassandraReplicationFactors = []int{2, 3, 5} KafkaReplicationFactors = []int{3, 5}