From fadba6de25b1054250744b26c0393ecbf7a563b9 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 | 64 ++++++ .../v1beta1/opensearchegressrules_webhook.go | 83 ++++++++ .../v1beta1/webhook_suite_test.go | 3 + .../v1beta1/zz_generated.deepcopy.go | 89 ++++++++ ...instaclustr.com_opensearchegressrules.yaml | 66 ++++++ 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 | 16 ++ config/webhook/manifests.yaml | 20 ++ .../opensearchegressrules_controller.go | 195 ++++++++++++++++++ main.go | 13 ++ pkg/instaclustr/client.go | 50 +++++ pkg/instaclustr/config.go | 1 + pkg/instaclustr/interfaces.go | 2 + pkg/instaclustr/mock/client.go | 8 + pkg/models/errors.go | 1 + 21 files changed, 733 insertions(+) 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 e802ca2f0..c5e70ce13 100644 --- a/PROJECT +++ b/PROJECT @@ -309,6 +309,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..823585770 --- /dev/null +++ b/apis/clusterresources/v1beta1/opensearchegressrules_types.go @@ -0,0 +1,64 @@ +/* +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"` + Name string `json:"name"` + 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..fb08ee3a0 --- /dev/null +++ b/apis/clusterresources/v1beta1/opensearchegressrules_webhook.go @@ -0,0 +1,83 @@ +/* +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" + 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 openSearchBindingIDPattern, _ = regexp.Compile(`[\w-]+`) + +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) + + if r.Spec.ClusterID == "" || r.Spec.OpenSearchBindingID == "" || r.Spec.Source == "" { + return fmt.Errorf("clusterId, openSearchBindingId, source must be filled") + } + + if !openSearchBindingIDPattern.MatchString(r.Spec.OpenSearchBindingID) { + return fmt.Errorf("mismatching openSearchBindingId to [\\w-]+ pattern") + } + + 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 da695c7b5..0c22230f9 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 0e92affe4..0f03f4cc6 100644 --- a/apis/clusterresources/v1beta1/zz_generated.deepcopy.go +++ b/apis/clusterresources/v1beta1/zz_generated.deepcopy.go @@ -1194,6 +1194,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/config/crd/bases/clusterresources.instaclustr.com_opensearchegressrules.yaml b/config/crd/bases/clusterresources.instaclustr.com_opensearchegressrules.yaml new file mode 100644 index 000000000..d5b3fa015 --- /dev/null +++ b/config/crd/bases/clusterresources.instaclustr.com_opensearchegressrules.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: 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 + name: + type: string + openSearchBindingId: + type: string + source: + type: string + type: + type: string + required: + - clusterId + - name + - openSearchBindingId + - source + - type + type: object + status: + properties: + id: + type: string + status: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index a13602a00..511f04441 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -27,6 +27,7 @@ resources: - bases/clusterresources.instaclustr.com_cassandrausers.yaml - bases/clusterresources.instaclustr.com_opensearchusers.yaml - bases/clusterresources.instaclustr.com_awsendpointserviceprincipals.yaml +- bases/clusterresources.instaclustr.com_opensearchegressrules.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -56,6 +57,7 @@ patchesStrategicMerge: #- patches/webhook_in_cassandrausers.yaml #- patches/webhook_in_clusterbackups.yaml #- patches/webhook_in_awsendpointserviceprincipals.yaml +#- patches/webhook_in_opensearchegressrules.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -88,6 +90,7 @@ patchesStrategicMerge: #- patches/cainjection_in_clusterbackups.yaml #- patches/cainjection_in_maintenanceevents.yaml #- patches/cainjection_in_awsendpointserviceprincipals.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 06b25de63..a3f58af6d 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -335,6 +335,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..569fdfb6d --- /dev/null +++ b/config/samples/clusterresources_v1beta1_opensearchegressrules.yaml @@ -0,0 +1,16 @@ +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: "cd668eb4-22e0-403d-8f53-229614bdbd5f" + name: "test-Webhook" + openSearchBindingId: "Eo_PQYoBaxsPF8JFJyDv" + source: "NOTIFICATIONS" + type: "SLACK" diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 3ae276584..77db0025e 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -572,6 +572,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..039ebfbc0 --- /dev/null +++ b/controllers/clusterresources/opensearchegressrules_controller.go @@ -0,0 +1,195 @@ +/* +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" + "encoding/json" + "errors" + "fmt" + + "github.com/go-logr/logr" + "github.com/instaclustr/operator/pkg/instaclustr" + "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/scheduler" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + clusterresourcesv1beta1 "github.com/instaclustr/operator/apis/clusterresources/v1beta1" +) + +// 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 + } + + // Handle resource deletion + if rule.DeletionTimestamp != nil { + err = r.handleDelete(ctx, l, rule) + if err != nil { + return models.ReconcileRequeue, err + } + + return models.ExitReconcile, nil + } + + // Handle 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 { + b, err := r.API.CreateOpenSearchEgressRules(rule.Spec.ClusterID, rule.Spec) + 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 + } + + patch := rule.NewPatch() + err = json.Unmarshal(b, &rule.Status) + if err != nil { + l.Error(err, "failed to parse OpenSearch Egress Rule resource response from Instaclustr") + r.EventRecorder.Eventf(rule, models.Warning, models.ConvertionFailed, + "Failed to parse OpenSearch Egress Rule response from Instaclustr. Reason: %v", err, + ) + + return err + } + + // This hack was added because instaclustr api returns null value egress rule id, but it should be equal to pattern {clusterId}~{source}~{bindingId} + // This code could be removed after instaclustr fix bugs, to make code cleaner + if rule.Status.ID == "" { + rule.Status.ID = fmt.Sprintf("%s~%s~%s", rule.Spec.ClusterID, rule.Spec.Source, rule.Spec.OpenSearchBindingID) + } + + 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 { + 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 513ed0342..20996eaa6 100644 --- a/main.go +++ b/main.go @@ -458,6 +458,19 @@ func main() { setupLog.Error(err, "unable to create webhook", "webhook", "AWSEndpointServicePrincipal") 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 e9ed4f8d6..2ba780334 100644 --- a/pkg/instaclustr/client.go +++ b/pkg/instaclustr/client.go @@ -2291,3 +2291,53 @@ func (c *Client) DeleteAWSEndpointServicePrincipal(principalID string) error { return nil } + +func (c *Client) CreateOpenSearchEgressRules(clusterID string, spec any) ([]byte, error) { + url := c.serverHostname + OpenSearchEgressRulesEndpoint + + b, err := json.Marshal(spec) + if err != nil { + return nil, err + } + + resp, err := c.DoRequest(url, http.MethodPost, b) + 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.StatusAccepted { + return nil, fmt.Errorf("status code: %d, message: %s", resp.StatusCode, b) + } + + return b, nil +} + +func (c *Client) DeleteOpenSearchEgressRule(egressRuleId string) error { + url := c.serverHostname + OpenSearchEgressRulesEndpoint + egressRuleId + 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 717f4a935..44f47da1e 100644 --- a/pkg/instaclustr/config.go +++ b/pkg/instaclustr/config.go @@ -52,6 +52,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/" ) diff --git a/pkg/instaclustr/interfaces.go b/pkg/instaclustr/interfaces.go index 6e2ca0aee..4ccf19b3a 100644 --- a/pkg/instaclustr/interfaces.go +++ b/pkg/instaclustr/interfaces.go @@ -98,6 +98,8 @@ type API interface { ListAppVersions(app string) ([]*models.AppVersions, error) GetDefaultCredentialsV1(clusterID string) (string, string, error) UpdateClusterSettings(clusterID string, settings *models.ClusterSettings) error + CreateOpenSearchEgressRules(clusterID string, spec any) ([]byte, error) + DeleteOpenSearchEgressRule(egressRuleId string) error CreateAWSEndpointServicePrincipal(spec any) ([]byte, error) DeleteAWSEndpointServicePrincipal(principalID string) error } diff --git a/pkg/instaclustr/mock/client.go b/pkg/instaclustr/mock/client.go index f53ebcb91..e9beaee9f 100644 --- a/pkg/instaclustr/mock/client.go +++ b/pkg/instaclustr/mock/client.go @@ -359,3 +359,11 @@ func (c *mockClient) CreateAWSEndpointServicePrincipal(spec any) ([]byte, error) func (c *mockClient) DeleteAWSEndpointServicePrincipal(principalID string) error { panic("DeleteAWSEndpointServicePrincipal: is not implemented") } + +func (c *mockClient) CreateOpenSearchEgressRules(clusterID string, spec any) ([]byte, error) { + panic("CreateOpenSearchEgressRules: is not implemented") +} + +func (c *mockClient) DeleteOpenSearchEgressRule(egressRuleId string) error { + panic("DeleteOpenSearchEgressRule: is not implemented") +} diff --git a/pkg/models/errors.go b/pkg/models/errors.go index b45303af4..1ba2616f3 100644 --- a/pkg/models/errors.go +++ b/pkg/models/errors.go @@ -59,4 +59,5 @@ var ( ErrMissingSecretKeys = errors.New("the secret is missing the correct keys for the user") ErrUserStillExist = errors.New("the user is still attached to the cluster. If you want to delete the user, remove the user from the cluster specification first") ErrOnlyOneEntityTwoFactorDelete = errors.New("currently only one entity of two factor delete can be filled") + ErrImmutableSpec = errors.New("spec is immutable") )