diff --git a/Makefile b/Makefile index 9cbe85839..42a070a43 100644 --- a/Makefile +++ b/Makefile @@ -98,7 +98,7 @@ run: manifests generate fmt vet ## Run a controller from your host. go run ./main.go .PHONY: docker-build -docker-build: manifests generate test ## Build docker image with the manager. +docker-build: manifests generate ## test ## Build docker image with the manager. docker build -t ${IMG} . .PHONY: docker-push diff --git a/PROJECT b/PROJECT index 136962933..508996d60 100644 --- a/PROJECT +++ b/PROJECT @@ -309,4 +309,25 @@ resources: webhooks: validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: instaclustr.com + group: clusters + kind: OpenSearchEgressRules + path: github.com/instaclustr/operator/apis/clusterresources/v1beta1 + version: v1beta1 +- 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 version: "3" diff --git a/apis/clusterresources/v1beta1/opensearchegressrules_types.go b/apis/clusterresources/v1beta1/opensearchegressrules_types.go new file mode 100644 index 000000000..aec0bc9ad --- /dev/null +++ b/apis/clusterresources/v1beta1/opensearchegressrules_types.go @@ -0,0 +1,72 @@ +/* +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" +) + +type OpenSearchEgressRulesSpec struct { + ClusterID string `json:"clusterId"` + Name string `json:"name"` + OpenSearchBindingId string `json:"openSearchBindingId"` + Source string `json:"source"` + Type string `json:"type"` +} + +type OpenSearchEgressRulesStatus struct { + ID string `json:"id,omitempty"` + Status string `json:"status,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) GetJobID(jobName string) string { + return client.ObjectKeyFromObject(er).String() + "/" + jobName +} + +func (er *OpenSearchEgressRules) NewPatch() client.Patch { + old := er.DeepCopy() + old.Annotations[models.ResourceStateAnnotation] = "" + 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..930b8df02 --- /dev/null +++ b/apis/clusterresources/v1beta1/opensearchegressrules_webhook.go @@ -0,0 +1,88 @@ +/* +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-]+`) +var egressRulesIDPattern, _ = regexp.Compile(`[a-zA-Z\d-]+~\w+~[\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("spec.ClusterID, spec.OpenSearchBindingId, spec.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) + + if !openSearchBindingIDPattern.MatchString(r.Spec.OpenSearchBindingId) { + return fmt.Errorf("mismatching openSearchBindingID to [a-zA-Z\\d-]+~\\w+~[\\w-]+ pattern") + } + + return nil +} diff --git a/apis/clusterresources/v1beta1/webhook_suite_test.go b/apis/clusterresources/v1beta1/webhook_suite_test.go index 1577bb6fc..a0589e9cc 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()) + //+kubebuilder:scaffold:webhook go func() { diff --git a/apis/clusterresources/v1beta1/zz_generated.deepcopy.go b/apis/clusterresources/v1beta1/zz_generated.deepcopy.go index 17f6fb467..667105cad 100644 --- a/apis/clusterresources/v1beta1/zz_generated.deepcopy.go +++ b/apis/clusterresources/v1beta1/zz_generated.deepcopy.go @@ -1105,6 +1105,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 107279848..81bfae40d 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -26,6 +26,7 @@ resources: - bases/clusterresources.instaclustr.com_awsencryptionkeys.yaml - bases/clusterresources.instaclustr.com_cassandrausers.yaml - bases/clusterresources.instaclustr.com_opensearchusers.yaml +- bases/clusterresources.instaclustr.com_opensearchegressrules.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -54,6 +55,7 @@ patchesStrategicMerge: #- patches/webhook_in_awsencryptionkeys.yaml #- patches/webhook_in_cassandrausers.yaml #- patches/webhook_in_clusterbackups.yaml +#- patches/webhook_in_opensearchegressrules.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -85,6 +87,7 @@ patchesStrategicMerge: #- patches/cainjection_in_postgresqls.yaml #- patches/cainjection_in_clusterbackups.yaml #- patches/cainjection_in_maintenanceevents.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 2251605e5..37a113675 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -309,6 +309,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..8461e3084 --- /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: "71e4380e-32ac-4fa7-ab42-c165fe35aa55", + name: "Test Webhook", + openSearchBindingId: "qzPJmIQBGW3Cho0V3Ee_", + source: "NOTIFICATIONS", + type: "WEBHOOK" diff --git a/config/samples/clusters_v1beta1_redis.yaml b/config/samples/clusters_v1beta1_redis.yaml index 8ca54013d..552fd0df6 100644 --- a/config/samples/clusters_v1beta1_redis.yaml +++ b/config/samples/clusters_v1beta1_redis.yaml @@ -9,18 +9,18 @@ metadata: app.kubernetes.io/created-by: operator name: redis-sample spec: - name: "Username-redis" + name: "mykyta-redis-test" version: "7.0.11" slaTier: "NON_PRODUCTION" clientEncryption: false passwordAndUserAuth: true userRefs: -# - name: redisuser-sample-1 -# namespace: default -# - name: redisuser-sample-2 -# namespace: default -# - name: redisuser-sample-3 -# namespace: default + - name: redisuser-sample-1 + namespace: default + - name: redisuser-sample-2 + namespace: default + - name: redisuser-sample-3 + namespace: default # twoFactorDelete: # - email: "rostyslp@netapp.com" dataCentres: diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 33fede0e2..9ce8cdbe3 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -552,6 +552,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..1eb0c9b9e --- /dev/null +++ b/controllers/clusterresources/opensearchegressrules_controller.go @@ -0,0 +1,188 @@ +/* +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" + + "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 + } + + 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, resource *clusterresourcesv1beta1.OpenSearchEgressRules) error { + err := r.API.DeleteOpenSearchEgressRule(resource.Status.ID) + if err != nil && !errors.Is(err, instaclustr.NotFound) { + logger.Error(err, "failed to delete OpenSearch Egress Rule on Instaclustr") + r.EventRecorder.Eventf(resource, models.Warning, models.DeletionFailed, + "Failed to delete OpenSearch Egress Rule on Instaclustr. Reason: %v", err, + ) + + return err + } + + patch := resource.NewPatch() + controllerutil.RemoveFinalizer(resource, models.DeletionFinalizer) + err = r.Patch(ctx, resource, patch) + if err != nil { + logger.Error(err, "failed to delete finalizer OpenSearch Egress Rule") + r.EventRecorder.Eventf(resource, 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/controllers/clusterresources/redisuser_controller.go b/controllers/clusterresources/redisuser_controller.go index 9c3b8b008..4119b1e22 100644 --- a/controllers/clusterresources/redisuser_controller.go +++ b/controllers/clusterresources/redisuser_controller.go @@ -171,7 +171,7 @@ func (r *RedisUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( if event == models.DeletingEvent { patch := user.NewPatch() - userID := fmt.Sprintf(instaclustr.RedisUserIDFmt, clusterID, user.Namespace) + userID := fmt.Sprintf(instaclustr.RedisUserIDFmt, clusterID, user.Name) err = r.API.DeleteRedisUser(userID) if err != nil { l.Error(err, "Cannot delete Redis user from the cluster. Cluster ID: %s.", "user", clusterID, user.Name) diff --git a/controllers/clusterresources/suite_test.go b/controllers/clusterresources/suite_test.go index 486f7499e..3de4e9de6 100644 --- a/controllers/clusterresources/suite_test.go +++ b/controllers/clusterresources/suite_test.go @@ -17,34 +17,29 @@ limitations under the License. package clusterresources import ( - "context" "path/filepath" "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" - ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" - "github.com/instaclustr/operator/apis/clusterresources/v1beta1" - "github.com/instaclustr/operator/pkg/instaclustr/mock" - "github.com/instaclustr/operator/pkg/scheduler" + clusterresourcesv1beta1 "github.com/instaclustr/operator/apis/clusterresources/v1beta1" //+kubebuilder:scaffold:imports ) -var ( - cfg *rest.Config - k8sClient client.Client - testEnv *envtest.Environment - ctx context.Context - cancel context.CancelFunc - MockInstAPI = mock.NewInstAPI() -) +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) @@ -54,7 +49,6 @@ func TestAPIs(t *testing.T) { var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) - ctx, cancel = context.WithCancel(context.TODO()) By("bootstrapping test environment") testEnv = &envtest.Environment{ @@ -68,7 +62,7 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) - err = v1beta1.AddToScheme(scheme.Scheme) + err = clusterresourcesv1beta1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:scheme @@ -77,92 +71,9 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) - k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ - Scheme: scheme.Scheme, - }) - Expect(err).ToNot(HaveOccurred()) - - eRecorder := k8sManager.GetEventRecorderFor("instaclustr-operator-tests") - - err = (&AWSVPCPeeringReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - API: MockInstAPI, - Scheduler: scheduler.NewScheduler(logf.Log), - EventRecorder: eRecorder, - }).SetupWithManager(k8sManager) - Expect(err).ToNot(HaveOccurred()) - - err = (&AzureVNetPeeringReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - API: MockInstAPI, - Scheduler: scheduler.NewScheduler(logf.Log), - EventRecorder: eRecorder, - }).SetupWithManager(k8sManager) - Expect(err).ToNot(HaveOccurred()) - - err = (&GCPVPCPeeringReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - API: MockInstAPI, - Scheduler: scheduler.NewScheduler(logf.Log), - EventRecorder: eRecorder, - }).SetupWithManager(k8sManager) - Expect(err).ToNot(HaveOccurred()) - - err = (&NodeReloadReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - API: MockInstAPI, - EventRecorder: eRecorder, - }).SetupWithManager(k8sManager) - Expect(err).ToNot(HaveOccurred()) - - err = (&AWSSecurityGroupFirewallRuleReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - API: MockInstAPI, - Scheduler: scheduler.NewScheduler(logf.Log), - EventRecorder: eRecorder, - }).SetupWithManager(k8sManager) - Expect(err).ToNot(HaveOccurred()) - - err = (&ClusterNetworkFirewallRuleReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - API: MockInstAPI, - Scheduler: scheduler.NewScheduler(logf.Log), - EventRecorder: eRecorder, - }).SetupWithManager(k8sManager) - Expect(err).ToNot(HaveOccurred()) - - err = (&ClusterBackupReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - API: MockInstAPI, - EventRecorder: eRecorder, - }).SetupWithManager(k8sManager) - Expect(err).ToNot(HaveOccurred()) - - err = (&RedisUserReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - API: MockInstAPI, - EventRecorder: eRecorder, - }).SetupWithManager(k8sManager) - Expect(err).ToNot(HaveOccurred()) - - go func() { - defer GinkgoRecover() - err = k8sManager.Start(ctx) - Expect(err).ToNot(HaveOccurred(), "failed to run manager") - }() - }) var _ = AfterSuite(func() { - cancel() By("tearing down the test environment") err := testEnv.Stop() Expect(err).NotTo(HaveOccurred()) diff --git a/go.mod b/go.mod index 22ee4f0fa..147952f32 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/hashicorp/go-version v1.6.0 github.com/onsi/ginkgo/v2 v2.0.0 github.com/onsi/gomega v1.18.1 + go.uber.org/zap v1.19.1 k8s.io/api v0.24.2 k8s.io/apimachinery v0.24.2 k8s.io/client-go v0.24.2 @@ -59,7 +60,6 @@ require ( github.com/spf13/pflag v1.0.5 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect - go.uber.org/zap v1.19.1 // indirect golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect diff --git a/main.go b/main.go index 2b710782a..a10048c62 100644 --- a/main.go +++ b/main.go @@ -445,6 +445,17 @@ func main() { setupLog.Error(err, "unable to create webhook", "webhook", "KafkaUser") os.Exit(1) } + if err = (&clusterresourcescontrollers.OpenSearchEgressRulesReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).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 cccf45589..370765170 100644 --- a/pkg/instaclustr/client.go +++ b/pkg/instaclustr/client.go @@ -2241,3 +2241,53 @@ func (c *Client) UpdateClusterSettings(clusterID string, settings *models.Cluste return nil } + +func (c *Client) CreateOpenSearchEgressRules(clusterID string, spec any) ([]byte, error) { + url := c.serverHostname + fmt.Sprintf(OpenSearchEgressRulesEndpoint, clusterID) + + 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 + OpenSearchEgressRuleDeleteEndpoint + 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 28324af15..d0af13d0e 100644 --- a/pkg/instaclustr/config.go +++ b/pkg/instaclustr/config.go @@ -52,6 +52,8 @@ 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/data-sources/opensearch_cluster/%s/egress-rules/v2/" + OpenSearchEgressRuleDeleteEndpoint = "/cluster-management/v2/resources/applications/opensearch/egress-rules/v2/" ) // constants for API v1 diff --git a/pkg/instaclustr/interfaces.go b/pkg/instaclustr/interfaces.go index 3747de569..ef0af3e88 100644 --- a/pkg/instaclustr/interfaces.go +++ b/pkg/instaclustr/interfaces.go @@ -98,4 +98,6 @@ 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 } diff --git a/pkg/models/errors.go b/pkg/models/errors.go index df31744b1..b14ef4edc 100644 --- a/pkg/models/errors.go +++ b/pkg/models/errors.go @@ -56,4 +56,5 @@ var ( ErrMissingSecretKeys = errors.New("the secret is missing the correct keys for the user") ErrUserStillExist = errors.New("the user is still attached to cluster") ErrOnlyOneEntityTwoFactorDelete = errors.New("currently only one entity of two factor delete can be filled") + ErrImmutableSpec = errors.New("spec is immutable") )