From 4e030b4d355bad4672a9b41788c0a459608c6e07 Mon Sep 17 00:00:00 2001 From: doodgeMatvey Date: Thu, 31 Aug 2023 19:16:30 +0300 Subject: [PATCH] PostgreSQL user creation logic was implemented --- PROJECT | 9 + .../v1beta1/postgresqluser_types.go | 88 +++++ .../v1beta1/zz_generated.deepcopy.go | 132 +++++++ apis/clusters/v1beta1/postgresql_types.go | 44 ++- .../clusters/v1beta1/zz_generated.deepcopy.go | 11 + ...urces.instaclustr.com_postgresqlusers.yaml | 82 +++++ .../clusters.instaclustr.com_cadences.yaml | 2 + .../clusters.instaclustr.com_kafkas.yaml | 1 + .../clusters.instaclustr.com_postgresqls.yaml | 12 + config/crd/kustomization.yaml | 3 + ...n_in_clusterresources_postgresqlusers.yaml | 7 + ...k_in_clusterresources_postgresqlusers.yaml | 16 + ...rresources_postgresqluser_editor_role.yaml | 31 ++ ...rresources_postgresqluser_viewer_role.yaml | 27 ++ config/rbac/role.yaml | 34 ++ ...lusterresources_v1beta1_cassandrauser.yaml | 4 +- ...usterresources_v1beta1_postgresqluser.yaml | 23 ++ .../samples/clusters_v1beta1_postgresql.yaml | 9 +- controllers/clusterresources/helpers.go | 18 +- .../postgresqluser_controller.go | 338 ++++++++++++++++++ controllers/clusters/postgresql_controller.go | 245 +++++++++++-- controllers/tests/datatest/secret.yaml | 4 +- go.mod | 15 +- go.sum | 33 +- main.go | 8 + pkg/exposeservice/expose_service.go | 41 ++- pkg/models/errors.go | 2 + pkg/models/operator.go | 32 +- 28 files changed, 1187 insertions(+), 84 deletions(-) create mode 100644 apis/clusterresources/v1beta1/postgresqluser_types.go create mode 100644 config/crd/bases/clusterresources.instaclustr.com_postgresqlusers.yaml create mode 100644 config/crd/patches/cainjection_in_clusterresources_postgresqlusers.yaml create mode 100644 config/crd/patches/webhook_in_clusterresources_postgresqlusers.yaml create mode 100644 config/rbac/clusterresources_postgresqluser_editor_role.yaml create mode 100644 config/rbac/clusterresources_postgresqluser_viewer_role.yaml create mode 100644 config/samples/clusterresources_v1beta1_postgresqluser.yaml create mode 100644 controllers/clusterresources/postgresqluser_controller.go diff --git a/PROJECT b/PROJECT index d79dab3ca..c51e42cd5 100644 --- a/PROJECT +++ b/PROJECT @@ -295,6 +295,15 @@ resources: kind: Topic path: github.com/instaclustr/operator/apis/kafkamanagement/v1beta1 version: v1beta1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: instaclustr.com + group: clusterresources + kind: PostgreSQLUser + path: github.com/instaclustr/operator/apis/clusterresources/v1beta1 + version: v1beta1 webhooks: defaulting: true webhookVersion: v1 diff --git a/apis/clusterresources/v1beta1/postgresqluser_types.go b/apis/clusterresources/v1beta1/postgresqluser_types.go new file mode 100644 index 000000000..4fa352c61 --- /dev/null +++ b/apis/clusterresources/v1beta1/postgresqluser_types.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 ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/instaclustr/operator/pkg/models" +) + +// PostgreSQLUserSpec defines the desired state of PostgreSQLUser +type PostgreSQLUserSpec struct { + SecretRef *SecretReference `json:"secretRef"` +} + +// PostgreSQLUserStatus defines the observed state of PostgreSQLUser +type PostgreSQLUserStatus struct { + // ClustersInfo efficiently stores data about clusters that related to this user. + // The keys of the map represent the cluster IDs, values are cluster info that consists of default secret namespaced name or event. + ClustersInfo map[string]ClusterInfo `json:"clustersInfo,omitempty"` +} + +type ClusterInfo struct { + DefaultSecretNamespacedName NamespacedName `json:"defaultSecretNamespacedName"` + Event string `json:"event,omitempty"` +} + +type NamespacedName struct { + Namespace string `json:"namespace"` + Name string `json:"name"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// PostgreSQLUser is the Schema for the postgresqlusers API +type PostgreSQLUser struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PostgreSQLUserSpec `json:"spec,omitempty"` + Status PostgreSQLUserStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// PostgreSQLUserList contains a list of PostgreSQLUser +type PostgreSQLUserList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PostgreSQLUser `json:"items"` +} + +func (r *PostgreSQLUser) NewPatch() client.Patch { + old := r.DeepCopy() + return client.MergeFrom(old) +} + +func init() { + SchemeBuilder.Register(&PostgreSQLUser{}, &PostgreSQLUserList{}) +} + +func (r *PostgreSQLUser) ToInstAPI(username, password string) *models.InstaUser { + return &models.InstaUser{ + Username: username, + Password: password, + InitialPermission: "standard", + } +} + +func (r *PostgreSQLUser) GetDeletionFinalizer() string { + return models.DeletionFinalizer + "_" + r.Namespace + "_" + r.Name +} diff --git a/apis/clusterresources/v1beta1/zz_generated.deepcopy.go b/apis/clusterresources/v1beta1/zz_generated.deepcopy.go index 27a8b69cc..659cc5245 100644 --- a/apis/clusterresources/v1beta1/zz_generated.deepcopy.go +++ b/apis/clusterresources/v1beta1/zz_generated.deepcopy.go @@ -667,6 +667,22 @@ func (in *ClusterBackupStatus) DeepCopy() *ClusterBackupStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterInfo) DeepCopyInto(out *ClusterInfo) { + *out = *in + out.DefaultSecretNamespacedName = in.DefaultSecretNamespacedName +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterInfo. +func (in *ClusterInfo) DeepCopy() *ClusterInfo { + if in == nil { + return nil + } + out := new(ClusterInfo) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterNetworkFirewallRule) DeepCopyInto(out *ClusterNetworkFirewallRule) { *out = *in @@ -1109,6 +1125,21 @@ func (in *MaintenanceEventsStatus) DeepCopy() *MaintenanceEventsStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespacedName) DeepCopyInto(out *NamespacedName) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedName. +func (in *NamespacedName) DeepCopy() *NamespacedName { + if in == nil { + return nil + } + out := new(NamespacedName) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Node) DeepCopyInto(out *Node) { *out = *in @@ -1381,6 +1412,107 @@ func (in *PeeringStatus) DeepCopy() *PeeringStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PostgreSQLUser) DeepCopyInto(out *PostgreSQLUser) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLUser. +func (in *PostgreSQLUser) DeepCopy() *PostgreSQLUser { + if in == nil { + return nil + } + out := new(PostgreSQLUser) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PostgreSQLUser) 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 *PostgreSQLUserList) DeepCopyInto(out *PostgreSQLUserList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PostgreSQLUser, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLUserList. +func (in *PostgreSQLUserList) DeepCopy() *PostgreSQLUserList { + if in == nil { + return nil + } + out := new(PostgreSQLUserList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PostgreSQLUserList) 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 *PostgreSQLUserSpec) DeepCopyInto(out *PostgreSQLUserSpec) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(SecretReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLUserSpec. +func (in *PostgreSQLUserSpec) DeepCopy() *PostgreSQLUserSpec { + if in == nil { + return nil + } + out := new(PostgreSQLUserSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PostgreSQLUserStatus) DeepCopyInto(out *PostgreSQLUserStatus) { + *out = *in + if in.ClustersInfo != nil { + in, out := &in.ClustersInfo, &out.ClustersInfo + *out = make(map[string]ClusterInfo, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLUserStatus. +func (in *PostgreSQLUserStatus) DeepCopy() *PostgreSQLUserStatus { + if in == nil { + return nil + } + out := new(PostgreSQLUserStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RedisUser) DeepCopyInto(out *RedisUser) { *out = *in diff --git a/apis/clusters/v1beta1/postgresql_types.go b/apis/clusters/v1beta1/postgresql_types.go index 95520269b..35a6ac533 100644 --- a/apis/clusters/v1beta1/postgresql_types.go +++ b/apis/clusters/v1beta1/postgresql_types.go @@ -89,6 +89,7 @@ type PgSpec struct { ClusterConfigurations map[string]string `json:"clusterConfigurations,omitempty"` Description string `json:"description,omitempty"` SynchronousModeStrict bool `json:"synchronousModeStrict,omitempty"` + UserRefs []*UserReference `json:"userRefs,omitempty"` } // PgStatus defines the observed state of PostgreSQL @@ -308,29 +309,6 @@ func (pdc *PgDataCentre) ArePGBouncersEqual(iPGBs []*PgBouncer) bool { return true } -func (pg *PostgreSQL) GetUserPassword(secret *k8sCore.Secret) string { - password := secret.Data[models.Password] - if len(password) == 0 { - return "" - } - - return string(password) -} - -func (pg *PostgreSQL) GetUserSecret(ctx context.Context, k8sClient client.Client) (*k8sCore.Secret, error) { - userSecret := &k8sCore.Secret{} - userSecretNamespacedName := types.NamespacedName{ - Name: fmt.Sprintf(models.DefaultUserSecretNameTemplate, models.DefaultUserSecretPrefix, pg.Name), - Namespace: pg.Namespace, - } - err := k8sClient.Get(ctx, userSecretNamespacedName, userSecret) - if err != nil { - return nil, err - } - - return userSecret, nil -} - func (pg *PostgreSQL) GetUserSecretName(ctx context.Context, k8sClient client.Client) (string, error) { var err error @@ -365,6 +343,7 @@ func (pg *PostgreSQL) NewUserSecret(defaultUserPassword string) *k8sCore.Secret Labels: map[string]string{ models.ControlledByLabel: pg.Name, models.DefaultSecretLabel: "true", + models.ClusterIDLabel: pg.Status.ID, }, }, StringData: map[string]string{ @@ -627,3 +606,22 @@ func (pgs *PgStatus) DCsFromInstAPI(iDCs []*models.PGDataCentre) (dcs []*DataCen } return } + +func GetDefaultPgUserSecret( + ctx context.Context, + name string, + ns string, + k8sClient client.Client, +) (*k8sCore.Secret, error) { + userSecret := &k8sCore.Secret{} + userSecretNamespacedName := types.NamespacedName{ + Name: fmt.Sprintf(models.DefaultUserSecretNameTemplate, models.DefaultUserSecretPrefix, name), + Namespace: ns, + } + err := k8sClient.Get(ctx, userSecretNamespacedName, userSecret) + if err != nil { + return nil, err + } + + return userSecret, nil +} diff --git a/apis/clusters/v1beta1/zz_generated.deepcopy.go b/apis/clusters/v1beta1/zz_generated.deepcopy.go index aeedbf2bb..9687260b8 100644 --- a/apis/clusters/v1beta1/zz_generated.deepcopy.go +++ b/apis/clusters/v1beta1/zz_generated.deepcopy.go @@ -1700,6 +1700,17 @@ func (in *PgSpec) DeepCopyInto(out *PgSpec) { (*out)[key] = val } } + if in.UserRefs != nil { + in, out := &in.UserRefs, &out.UserRefs + *out = make([]*UserReference, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(UserReference) + **out = **in + } + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PgSpec. diff --git a/config/crd/bases/clusterresources.instaclustr.com_postgresqlusers.yaml b/config/crd/bases/clusterresources.instaclustr.com_postgresqlusers.yaml new file mode 100644 index 000000000..4208d0f18 --- /dev/null +++ b/config/crd/bases/clusterresources.instaclustr.com_postgresqlusers.yaml @@ -0,0 +1,82 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: postgresqlusers.clusterresources.instaclustr.com +spec: + group: clusterresources.instaclustr.com + names: + kind: PostgreSQLUser + listKind: PostgreSQLUserList + plural: postgresqlusers + singular: postgresqluser + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: PostgreSQLUser is the Schema for the postgresqlusers API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: PostgreSQLUserSpec defines the desired state of PostgreSQLUser + properties: + secretRef: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + required: + - secretRef + type: object + status: + description: PostgreSQLUserStatus defines the observed state of PostgreSQLUser + properties: + clustersInfo: + additionalProperties: + properties: + defaultSecretNamespacedName: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + event: + type: string + required: + - defaultSecretNamespacedName + type: object + description: ClustersInfo efficiently stores data about clusters that + related to this user. The keys of the map represent the cluster + IDs, values are cluster info that consists of default secret namespaced + name or event. + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/clusters.instaclustr.com_cadences.yaml b/config/crd/bases/clusters.instaclustr.com_cadences.yaml index f87d635d0..67a0a4acb 100644 --- a/config/crd/bases/clusters.instaclustr.com_cadences.yaml +++ b/config/crd/bases/clusters.instaclustr.com_cadences.yaml @@ -104,6 +104,8 @@ spec: - nodesNumber - region type: object + maxItems: 1 + minItems: 1 type: array description: type: string diff --git a/config/crd/bases/clusters.instaclustr.com_kafkas.yaml b/config/crd/bases/clusters.instaclustr.com_kafkas.yaml index 07c923f6c..6b06f04c3 100644 --- a/config/crd/bases/clusters.instaclustr.com_kafkas.yaml +++ b/config/crd/bases/clusters.instaclustr.com_kafkas.yaml @@ -95,6 +95,7 @@ spec: - region type: object maxItems: 1 + minItems: 1 type: array dedicatedZookeeper: description: Provision additional dedicated nodes for Apache Zookeeper diff --git a/config/crd/bases/clusters.instaclustr.com_postgresqls.yaml b/config/crd/bases/clusters.instaclustr.com_postgresqls.yaml index 2d0fa3286..8748fd7ae 100644 --- a/config/crd/bases/clusters.instaclustr.com_postgresqls.yaml +++ b/config/crd/bases/clusters.instaclustr.com_postgresqls.yaml @@ -184,6 +184,18 @@ spec: - email type: object type: array + userRefs: + items: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + type: array version: type: string type: object diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index be06a55d1..0278a542e 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -28,6 +28,7 @@ resources: - bases/clusterresources.instaclustr.com_opensearchusers.yaml - bases/clusterresources.instaclustr.com_awsendpointserviceprincipals.yaml - bases/clusterresources.instaclustr.com_exclusionwindows.yaml +- bases/clusterresources.instaclustr.com_postgresqlusers.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -58,6 +59,7 @@ patchesStrategicMerge: #- patches/webhook_in_clusterbackups.yaml #- patches/webhook_in_awsendpointserviceprincipals.yaml #- patches/webhook_in_exclusionwindows.yaml +#- patches/webhook_in_postgresqlusers.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -91,6 +93,7 @@ patchesStrategicMerge: #- patches/cainjection_in_maintenanceevents.yaml #- patches/cainjection_in_awsendpointserviceprincipals.yaml #- patches/cainjection_in_exclusionwindows.yaml +#- patches/cainjection_in_postgresqlusers.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_postgresqlusers.yaml b/config/crd/patches/cainjection_in_clusterresources_postgresqlusers.yaml new file mode 100644 index 000000000..f2670559f --- /dev/null +++ b/config/crd/patches/cainjection_in_clusterresources_postgresqlusers.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: postgresqlusers.clusterresources.instaclustr.com diff --git a/config/crd/patches/webhook_in_clusterresources_postgresqlusers.yaml b/config/crd/patches/webhook_in_clusterresources_postgresqlusers.yaml new file mode 100644 index 000000000..6bff8450c --- /dev/null +++ b/config/crd/patches/webhook_in_clusterresources_postgresqlusers.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: postgresqlusers.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_postgresqluser_editor_role.yaml b/config/rbac/clusterresources_postgresqluser_editor_role.yaml new file mode 100644 index 000000000..c0af95771 --- /dev/null +++ b/config/rbac/clusterresources_postgresqluser_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit postgresqlusers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: postgresqluser-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: postgresqluser-editor-role +rules: +- apiGroups: + - clusterresources.instaclustr.com + resources: + - postgresqlusers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - clusterresources.instaclustr.com + resources: + - postgresqlusers/status + verbs: + - get diff --git a/config/rbac/clusterresources_postgresqluser_viewer_role.yaml b/config/rbac/clusterresources_postgresqluser_viewer_role.yaml new file mode 100644 index 000000000..da34f9cee --- /dev/null +++ b/config/rbac/clusterresources_postgresqluser_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view postgresqlusers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: postgresqluser-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: postgresqluser-viewer-role +rules: +- apiGroups: + - clusterresources.instaclustr.com + resources: + - postgresqlusers + verbs: + - get + - list + - watch +- apiGroups: + - clusterresources.instaclustr.com + resources: + - postgresqlusers/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index c6477385a..26bf3de63 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -24,6 +24,14 @@ rules: verbs: - create - patch +- apiGroups: + - "" + resources: + - nodes + verbs: + - get + - list + - watch - apiGroups: - "" resources: @@ -387,6 +395,32 @@ rules: - get - patch - update +- apiGroups: + - clusterresources.instaclustr.com + resources: + - postgresqlusers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - clusterresources.instaclustr.com + resources: + - postgresqlusers/finalizers + verbs: + - update +- apiGroups: + - clusterresources.instaclustr.com + resources: + - postgresqlusers/status + verbs: + - get + - patch + - update - apiGroups: - clusterresources.instaclustr.com resources: diff --git a/config/samples/clusterresources_v1beta1_cassandrauser.yaml b/config/samples/clusterresources_v1beta1_cassandrauser.yaml index 442f7c732..f12a6fd0a 100644 --- a/config/samples/clusterresources_v1beta1_cassandrauser.yaml +++ b/config/samples/clusterresources_v1beta1_cassandrauser.yaml @@ -3,8 +3,8 @@ kind: Secret metadata: name: cassandra-user-secret data: - password: NDgxMzU5ODM1NzlmMDU0ZTlhY2I4ZjcxMTMzMzQ1MjM3ZQo= - username: b2xvbG8K + password: NDgxMzU5ODM1NzlmMDU0ZTlhY2I4ZjcxMTMzMzQ1MjM3ZQ== + username: b2xvbG8= --- apiVersion: clusterresources.instaclustr.com/v1beta1 diff --git a/config/samples/clusterresources_v1beta1_postgresqluser.yaml b/config/samples/clusterresources_v1beta1_postgresqluser.yaml new file mode 100644 index 000000000..ddcc2502c --- /dev/null +++ b/config/samples/clusterresources_v1beta1_postgresqluser.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Secret +metadata: + name: postgres-user-secret +data: + password: NDgxMzU5ODM1NzlmMDU0ZTlhY2I4ZjcxMTMzMzQ1MjM3ZQ== + username: b2xvbG8= +--- + +apiVersion: clusterresources.instaclustr.com/v1beta1 +kind: PostgreSQLUser +metadata: + labels: + app.kubernetes.io/name: postgresqluser + app.kubernetes.io/instance: postgresqluser-sample + app.kubernetes.io/part-of: operator + app.kuberentes.io/managed-by: kustomize + app.kubernetes.io/created-by: operator + name: postgresqluser-sample +spec: + secretRef: + name: "postgres-user-secret" + namespace: "default" diff --git a/config/samples/clusters_v1beta1_postgresql.yaml b/config/samples/clusters_v1beta1_postgresql.yaml index b92140b4b..e3c3f05c2 100644 --- a/config/samples/clusters_v1beta1_postgresql.yaml +++ b/config/samples/clusters_v1beta1_postgresql.yaml @@ -1,13 +1,13 @@ apiVersion: clusters.instaclustr.com/v1beta1 kind: PostgreSQL metadata: - name: postgresql-sample + name: postgresql-cluster # TODO https://github.com/instaclustr/operator/issues/472 # annotations: # testAnnotation: test spec: - name: "username-test" - version: "14.6.0" + name: "postgresql-cluster" + version: "14.9.0" dataCentres: - region: "US_WEST_2" network: "10.1.0.0/16" @@ -42,5 +42,8 @@ spec: # - email: "rostyslp@netapp.com" # description: "test 222" slaTier: "NON_PRODUCTION" + userRef: + - namespace: default + name: postgresqluser-sample privateNetworkCluster: false synchronousModeStrict: false diff --git a/controllers/clusterresources/helpers.go b/controllers/clusterresources/helpers.go index 1282d8a69..e97a06c20 100644 --- a/controllers/clusterresources/helpers.go +++ b/controllers/clusterresources/helpers.go @@ -17,6 +17,8 @@ limitations under the License. package clusterresources import ( + "strings" + k8sCore "k8s.io/api/core/v1" "github.com/instaclustr/operator/apis/clusterresources/v1beta1" @@ -66,12 +68,22 @@ func areEncryptionKeyStatusesEqual(a, b *v1beta1.AWSEncryptionKeyStatus) bool { } func getUserCreds(secret *k8sCore.Secret) (username, password string, err error) { - password = string(secret.Data["password"]) - username = string(secret.Data["username"]) + password = string(secret.Data[models.Password]) + username = string(secret.Data[models.Username]) if len(username) == 0 || len(password) == 0 { return "", "", models.ErrMissingSecretKeys } - return username[:len(username)-1], password[:len(password)-1], nil + newLineSuffix := "\n" + + if strings.HasSuffix(username, newLineSuffix) { + username = strings.TrimRight(username, newLineSuffix) + } + + if strings.HasSuffix(password, newLineSuffix) { + password = strings.TrimRight(password, newLineSuffix) + } + + return username, password, nil } diff --git a/controllers/clusterresources/postgresqluser_controller.go b/controllers/clusterresources/postgresqluser_controller.go new file mode 100644 index 000000000..735ed585c --- /dev/null +++ b/controllers/clusterresources/postgresqluser_controller.go @@ -0,0 +1,338 @@ +/* +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" + + "github.com/jackc/pgx/v5" + k8sCore "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + clusterresourcesv1beta1 "github.com/instaclustr/operator/apis/clusterresources/v1beta1" + "github.com/instaclustr/operator/pkg/exposeservice" + "github.com/instaclustr/operator/pkg/models" +) + +// PostgreSQLUserReconciler reconciles a PostgreSQLUser object +type PostgreSQLUserReconciler struct { + client.Client + Scheme *runtime.Scheme + EventRecorder record.EventRecorder +} + +//+kubebuilder:rbac:groups=clusterresources.instaclustr.com,resources=postgresqlusers,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=clusterresources.instaclustr.com,resources=postgresqlusers/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=clusterresources.instaclustr.com,resources=postgresqlusers/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.0/pkg/reconcile +func (r *PostgreSQLUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + l := log.FromContext(ctx) + + u := &clusterresourcesv1beta1.PostgreSQLUser{} + err := r.Get(ctx, req.NamespacedName, u) + if err != nil { + if k8sErrors.IsNotFound(err) { + l.Info("PostgreSQL user resource is not found", "request", req) + + return models.ExitReconcile, nil + } + l.Error(err, "Cannot fetch PostgreSQL user resource", "request", req) + + return models.ReconcileRequeue, nil + } + + s := &k8sCore.Secret{} + err = r.Get(ctx, types.NamespacedName{ + Namespace: u.Spec.SecretRef.Namespace, + Name: u.Spec.SecretRef.Name, + }, s) + if err != nil { + if k8sErrors.IsNotFound(err) { + l.Info("PostgreSQL user secret is not found", "request", req) + r.EventRecorder.Event(u, models.Warning, models.NotFound, + "Secret is not found, please create a new secret or set an actual reference") + return models.ReconcileRequeue, nil + } + + l.Error(err, "Cannot get PostgreSQL user secret", "user", u.Name) + + return models.ReconcileRequeue, nil + } + + newUsername, newPassword, err := getUserCreds(s) + if err != nil { + l.Error(err, "Cannot get the PostgreSQL user credentials from the secret", + "secret name", s.Name, + "secret namespace", s.Namespace) + r.EventRecorder.Eventf(u, models.Warning, models.CreatingEvent, + "Cannot get the PostgreSQL user credentials from the secret. Reason: %v", err) + + return models.ReconcileRequeue, nil + } + + if controllerutil.AddFinalizer(s, u.GetDeletionFinalizer()) { + err = r.Update(ctx, s) + if err != nil { + l.Error(err, "Cannot update PostgreSQL user's secret with deletion finalizer", + "secret name", s.Name, "secret namespace", s.Namespace) + r.EventRecorder.Eventf(u, models.Warning, models.PatchFailed, + "Update secret with deletion finalizer has been failed. Reason: %v", err) + return models.ReconcileRequeue, nil + } + } + + patch := u.NewPatch() + if controllerutil.AddFinalizer(u, u.GetDeletionFinalizer()) { + err = r.Patch(ctx, u, patch) + if err != nil { + l.Error(err, "Cannot patch PostgreSQL user with deletion finalizer") + r.EventRecorder.Eventf(u, models.Warning, models.PatchFailed, + "Patching PostgreSQL user with deletion finalizer has been failed. Reason: %v", err) + return models.ReconcileRequeue, nil + } + } + + for clusterID, clusterInfo := range u.Status.ClustersInfo { + if clusterInfo.Event == models.CreatingEvent { + l.Info("Creating user", "user", u, "cluster ID", clusterID) + + err = r.createUser(ctx, clusterID, newUsername, newPassword, clusterInfo.DefaultSecretNamespacedName) + if err != nil { + if errors.Is(err, models.ErrExposeServiceNotCreatedYet) || + errors.Is(err, models.ErrExposeServiceEndpointsNotCreatedYet) { + l.Info("Expose service or expose service endpoints are not created yet", + "username", newUsername) + + return models.ReconcileRequeue, nil + } + + l.Error(err, "Cannot create a user for the PostgreSQL cluster", + "cluster ID", clusterID, + "username", newUsername) + r.EventRecorder.Eventf(u, models.Warning, models.CreatingEvent, + "Cannot create user. Reason: %v", err) + + return models.ReconcileRequeue, nil + } + + u.Status.ClustersInfo[clusterID] = clusterresourcesv1beta1.ClusterInfo{ + DefaultSecretNamespacedName: clusterInfo.DefaultSecretNamespacedName, + Event: models.Created, + } + + err = r.Status().Patch(ctx, u, patch) + if err != nil { + l.Error(err, "Cannot patch PostgreSQL user status", + "cluster ID", clusterID, + "username", newUsername) + r.EventRecorder.Eventf(u, models.Warning, models.PatchFailed, + "Resource patch is failed. Reason: %v", err) + + return models.ReconcileRequeue, nil + } + + l.Info("User has been created", "username", newUsername) + + r.EventRecorder.Eventf(u, models.Normal, models.Created, + "User has been created for a cluster. Cluster ID: %s, username: %s", + clusterID, newUsername) + + // TODO: Add deletion user finalizers + + continue + } + + // TODO: implement user deletion logic on this event + } + + // TODO: add logic for Deletion case + + return models.ExitReconcile, nil +} + +func (r *PostgreSQLUserReconciler) createUser( + ctx context.Context, + clusterID string, + newUserName string, + newPassword string, + defaultUserSecretNamespacedName clusterresourcesv1beta1.NamespacedName, +) error { + defaultUserSecret := &k8sCore.Secret{} + + namespacedName := types.NamespacedName{ + Namespace: defaultUserSecretNamespacedName.Namespace, + Name: defaultUserSecretNamespacedName.Name, + } + err := r.Get(ctx, namespacedName, defaultUserSecret) + if err != nil { + if k8sErrors.IsNotFound(err) { + return fmt.Errorf("cannot get default PostgreSQL user secret, user reference: %v, err: %w", defaultUserSecretNamespacedName, err) + } + + return err + } + + defaultUsername, defaultPassword, err := getUserCreds(defaultUserSecret) + if err != nil { + return fmt.Errorf("cannot get default PostgreSQL user credentials, user reference: %v, err: %w", defaultUserSecretNamespacedName, err) + } + + clusterName := defaultUserSecret.Labels[models.ControlledByLabel] + exposeServiceList, err := exposeservice.GetExposeService(r.Client, clusterName, defaultUserSecretNamespacedName.Namespace) + if err != nil { + return fmt.Errorf("cannot list expose services for cluster: %s, err: %w", clusterID, err) + } + + if len(exposeServiceList.Items) == 0 { + return models.ErrExposeServiceNotCreatedYet + } + + exposeServiceEndpoints, err := exposeservice.GetExposeServiceEndpoints(r.Client, clusterName, defaultUserSecretNamespacedName.Namespace) + if err != nil { + return fmt.Errorf("cannot list expose service endpoints for cluster: %s, err: %w", clusterID, err) + } + + if len(exposeServiceEndpoints.Items) == 0 { + return models.ErrExposeServiceEndpointsNotCreatedYet + } + + nodeList := &k8sCore.NodeList{} + + err = r.List(ctx, nodeList, &client.ListOptions{}) + if err != nil { + return fmt.Errorf("cannot list nodes, err: %w", err) + } + + // TODO: Handle scenario if there are no nodes with external IP + + for _, node := range nodeList.Items { + for _, nodeAddress := range node.Status.Addresses { + if nodeAddress.Type == k8sCore.NodeExternalIP { + err := r.createPostgreSQLFirewallRule(ctx, node.Name, clusterID, defaultUserSecretNamespacedName.Namespace, nodeAddress.Address) + if err != nil { + return fmt.Errorf("cannot create postgreSQL firewall rule, err: %w", err) + } + } + } + } + + serviceName := fmt.Sprintf(models.ExposeServiceNameTemplate, clusterName) + host := fmt.Sprintf("%s.%s", serviceName, exposeServiceList.Items[0].Namespace) + + dbURL := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?target_session_attrs=read-write", + defaultUsername, defaultPassword, host, models.DefaultPgDbPortValue, models.DefaultPgDbNameValue) + + conn, err := pgx.Connect(ctx, dbURL) + if err != nil { + return fmt.Errorf("cannot establish a connection with a PostgreSQL server, err: %w", err) + } + defer conn.Close(ctx) + + createUserQuery := fmt.Sprintf(`CREATE USER "%s" WITH PASSWORD '%s'`, newUserName, newPassword) + _, err = conn.Exec(ctx, createUserQuery) + if err != nil { + return fmt.Errorf("cannot execute creation user query in postgresql, err: %w", err) + } + + return nil +} + +func (r *PostgreSQLUserReconciler) createPostgreSQLFirewallRule( + ctx context.Context, + nodeName string, + clusterID string, + ns string, + nodeAddress string, +) error { + firewallRuleName := fmt.Sprintf("%s-%s-%s", models.ClusterNetworkFirewallRulePrefix, clusterID, nodeName) + + exists, err := r.firewallRuleExists(ctx, firewallRuleName, ns) + if err != nil { + return err + } + + if exists { + return nil + } + + firewallRule := &clusterresourcesv1beta1.ClusterNetworkFirewallRule{ + TypeMeta: ctrl.TypeMeta{ + Kind: models.ClusterNetworkFirewallRuleKind, + APIVersion: models.ClusterresourcesV1beta1APIVersion, + }, + ObjectMeta: ctrl.ObjectMeta{ + Name: firewallRuleName, + Namespace: ns, + Annotations: map[string]string{models.ResourceStateAnnotation: models.CreatingEvent}, + Labels: map[string]string{models.ClusterIDLabel: clusterID}, + Finalizers: []string{models.DeletionFinalizer}, + }, + Spec: clusterresourcesv1beta1.ClusterNetworkFirewallRuleSpec{ + FirewallRuleSpec: clusterresourcesv1beta1.FirewallRuleSpec{ + ClusterID: clusterID, + Type: models.PgAppType, + }, + Network: fmt.Sprintf("%s/%s", nodeAddress, "32"), + }, + Status: clusterresourcesv1beta1.ClusterNetworkFirewallRuleStatus{}, + } + + err = r.Create(ctx, firewallRule) + if err != nil { + return err + } + + return nil +} + +func (r *PostgreSQLUserReconciler) firewallRuleExists(ctx context.Context, firewallRuleName string, ns string) (bool, error) { + clusterNetworkFirewallRule := &clusterresourcesv1beta1.ClusterNetworkFirewallRule{} + err := r.Get(ctx, types.NamespacedName{ + Namespace: ns, + Name: firewallRuleName, + }, clusterNetworkFirewallRule) + if err != nil { + if k8sErrors.IsNotFound(err) { + return false, nil + } + + return false, err + } + + return true, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *PostgreSQLUserReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&clusterresourcesv1beta1.PostgreSQLUser{}). + Complete(r) +} diff --git a/controllers/clusters/postgresql_controller.go b/controllers/clusters/postgresql_controller.go index 64e538d5e..abaad5f7a 100644 --- a/controllers/clusters/postgresql_controller.go +++ b/controllers/clusters/postgresql_controller.go @@ -62,6 +62,7 @@ type PostgreSQLReconciler struct { //+kubebuilder:rbac:groups=clusterresources.instaclustr.com,resources=clusterbackups,verbs=get;list;create;update;patch;deletecollection;delete //+kubebuilder:rbac:groups="",resources=secrets,verbs=get;watch;create;delete;update //+kubebuilder:rbac:groups="",resources=events,verbs=create;patch +//+kubebuilder:rbac:groups="",resources=nodes,verbs=get;watch;list // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -264,7 +265,7 @@ func (r *PostgreSQLReconciler) handleCreateCluster( "Cluster backups check job is started", ) - err = r.createDefaultPassword(pg, logger) + err = r.createDefaultPassword(ctx, pg, logger) if err != nil { logger.Error(err, "Cannot create default password for PostgreSQL", "cluster name", pg.Spec.Name, @@ -280,6 +281,19 @@ func (r *PostgreSQLReconciler) handleCreateCluster( return models.ReconcileRequeue } + if pg.Spec.UserRefs != nil { + err = r.startUsersCreationJob(pg) + if err != nil { + logger.Error(err, "Failed to start user PostreSQL creation job") + r.EventRecorder.Eventf(pg, models.Warning, models.CreationFailed, + "User creation job is failed. Reason: %v", err) + return models.ReconcileRequeue + } + + r.EventRecorder.Event(pg, models.Normal, models.Created, + "Cluster user creation job is started") + } + return models.ExitReconcile } @@ -468,6 +482,134 @@ func (r *PostgreSQLReconciler) handleUpdateCluster( return models.ExitReconcile } +func (r *PostgreSQLReconciler) createUser( + ctx context.Context, + l logr.Logger, + c *v1beta1.PostgreSQL, + uRef *v1beta1.UserReference, +) error { + req := types.NamespacedName{ + Namespace: uRef.Namespace, + Name: uRef.Name, + } + + u := &clusterresourcesv1beta1.PostgreSQLUser{} + err := r.Get(ctx, req, u) + if err != nil { + if k8serrors.IsNotFound(err) { + l.Error(err, "Cannot create a PostgreSQL user. The resource is not found", "request", req) + r.EventRecorder.Eventf(c, models.Warning, models.NotFound, + "User is not found, create a new one PostgreSQL User or provide correct userRef."+ + "Current provided reference: %v", uRef) + return err + } + + l.Error(err, "Cannot get PostgreSQL user", "user", u.Spec) + r.EventRecorder.Eventf(c, models.Warning, models.CreationFailed, + "Cannot get PostgreSQL user. User reference: %v", uRef) + return err + } + + secret, err := v1beta1.GetDefaultPgUserSecret(ctx, c.Name, c.Namespace, r.Client) + if err != nil && !k8serrors.IsNotFound(err) { + r.EventRecorder.Eventf( + c, models.Warning, models.FetchFailed, + "Default user secret fetch is failed. Reason: %v", + err, + ) + + return err + } + + defaultSecretNamespacedName := types.NamespacedName{ + Namespace: secret.Namespace, + Name: secret.Name, + } + + if _, exist := u.Status.ClustersInfo[c.Status.ID]; exist { + l.Info("User is already existing on the cluster", + "user reference", uRef) + r.EventRecorder.Eventf(c, models.Normal, models.CreationFailed, + "User is already existing on the cluster. User reference: %v", uRef) + + return nil + } + + patch := u.NewPatch() + + if u.Status.ClustersInfo == nil { + u.Status.ClustersInfo = make(map[string]clusterresourcesv1beta1.ClusterInfo) + } + + u.Status.ClustersInfo[c.Status.ID] = clusterresourcesv1beta1.ClusterInfo{ + DefaultSecretNamespacedName: clusterresourcesv1beta1.NamespacedName{ + Namespace: defaultSecretNamespacedName.Namespace, + Name: defaultSecretNamespacedName.Name, + }, + Event: models.CreatingEvent, + } + + err = r.Status().Patch(ctx, u, patch) + if err != nil { + l.Error(err, "Cannot patch the PostgreSQL User status with the CreatingEvent", + "cluster name", c.Spec.Name, "cluster ID", c.Status.ID) + r.EventRecorder.Eventf(c, models.Warning, models.CreationFailed, + "Cannot add PostgreSQL User to the cluster. Reason: %v", err) + return err + } + + return nil +} + +func (r *PostgreSQLReconciler) handleUserEvent( + newObj *v1beta1.PostgreSQL, + oldUsers []*v1beta1.UserReference, +) { + ctx := context.TODO() + l := log.FromContext(ctx) + + for _, newUser := range newObj.Spec.UserRefs { + var exist bool + + for _, oldUser := range oldUsers { + if *newUser == *oldUser { + exist = true + break + } + } + + if exist { + continue + } + + err := r.createUser(ctx, l, newObj, newUser) + if err != nil { + l.Error(err, "Cannot create PostgreSQL user in predicate", "user", newUser) + r.EventRecorder.Eventf(newObj, models.Warning, models.CreatingEvent, + "Cannot create user. Reason: %v", err) + } + + oldUsers = append(oldUsers, newUser) + } + + for _, oldUser := range oldUsers { + var exist bool + + for _, newUser := range newObj.Spec.UserRefs { + if *oldUser == *newUser { + exist = true + break + } + } + + if exist { + continue + } + + // TODO: implement user deletion + } +} + func (r *PostgreSQLReconciler) handleExternalChanges(pg, iPg *v1beta1.PostgreSQL, l logr.Logger) reconcile.Result { if !pg.Spec.IsEqual(iPg.Spec) { l.Info(msgSpecStillNoMatch, @@ -680,7 +822,7 @@ func (r *PostgreSQLReconciler) handleUpdateDefaultUserPassword( ) reconcile.Result { logger = logger.WithName("PostgreSQL default user password updating event") - secret, err := pg.GetUserSecret(ctx, r.Client) + secret, err := v1beta1.GetDefaultPgUserSecret(ctx, pg.Name, pg.Namespace, r.Client) if err != nil { logger.Error(err, "Cannot get the default secret for the PostgreSQL cluster", "cluster name", pg.Spec.Name, @@ -696,8 +838,7 @@ func (r *PostgreSQLReconciler) handleUpdateDefaultUserPassword( return models.ReconcileRequeue } - password := pg.GetUserPassword(secret) - + password := string(secret.Data[models.Password]) isValid := pg.ValidateDefaultUserPassword(password) if !isValid { logger.Error(err, "Default PostgreSQL user password is not valid. This field must be at least 8 characters long. Must contain characters from at least 3 of the following 4 categories: Uppercase, Lowercase, Numbers, Special Characters", @@ -781,6 +922,17 @@ func (r *PostgreSQLReconciler) startClusterBackupsJob(pg *v1beta1.PostgreSQL) er return nil } +func (r *PostgreSQLReconciler) startUsersCreationJob(cluster *v1beta1.PostgreSQL) error { + job := r.newUsersCreationJob(cluster) + + err := r.Scheduler.ScheduleJob(cluster.GetJobID(scheduler.UserCreator), scheduler.UserCreationInterval, job) + if err != nil { + return err + } + + return nil +} + func (r *PostgreSQLReconciler) newWatchStatusJob(pg *v1beta1.PostgreSQL) scheduler.Job { l := log.Log.WithValues("component", "postgreSQLStatusClusterJob") @@ -958,7 +1110,7 @@ func (r *PostgreSQLReconciler) newWatchStatusJob(pg *v1beta1.PostgreSQL) schedul } } -func (r *PostgreSQLReconciler) createDefaultPassword(pg *v1beta1.PostgreSQL, l logr.Logger) error { +func (r *PostgreSQLReconciler) createDefaultPassword(ctx context.Context, pg *v1beta1.PostgreSQL, l logr.Logger) error { iData, err := r.API.GetPostgreSQL(pg.Status.ID) if err != nil { l.Error( @@ -975,23 +1127,12 @@ func (r *PostgreSQLReconciler) createDefaultPassword(pg *v1beta1.PostgreSQL, l l return err } - secret, err := pg.GetUserSecret(context.TODO(), r.Client) - if err != nil && !k8serrors.IsNotFound(err) { - r.EventRecorder.Eventf( - pg, models.Warning, models.FetchFailed, - "Default user secret fetch is failed. Reason: %v", - err, - ) - + defaultSecretExists, err := r.DefaultSecretExists(ctx, pg) + if err != nil { return err } - if secret != nil { - l.Info("Default user secret for PostgreSQL cluster already exists", - "cluster name", pg.Spec.Name, - "clusterID", pg.Status.ID, - ) - + if defaultSecretExists { return nil } @@ -1011,7 +1152,7 @@ func (r *PostgreSQLReconciler) createDefaultPassword(pg *v1beta1.PostgreSQL, l l return err } - secret = pg.NewUserSecret(defaultUserPassword) + secret := pg.NewUserSecret(defaultUserPassword) err = r.Client.Create(context.TODO(), secret) if err != nil { l.Error(err, "Cannot create PostgreSQL default user secret", @@ -1042,6 +1183,19 @@ func (r *PostgreSQLReconciler) createDefaultPassword(pg *v1beta1.PostgreSQL, l l return nil } +func (r *PostgreSQLReconciler) DefaultSecretExists(ctx context.Context, pg *v1beta1.PostgreSQL) (bool, error) { + _, err := v1beta1.GetDefaultPgUserSecret(ctx, pg.Name, pg.Namespace, r.Client) + if err != nil { + if k8serrors.IsNotFound(err) { + return false, nil + } + + return false, err + } + + return true, nil +} + func (r *PostgreSQLReconciler) newWatchBackupsJob(pg *v1beta1.PostgreSQL) scheduler.Job { l := log.Log.WithValues("component", "postgreSQLBackupsClusterJob") @@ -1144,6 +1298,49 @@ func (r *PostgreSQLReconciler) newWatchBackupsJob(pg *v1beta1.PostgreSQL) schedu } } +func (r *PostgreSQLReconciler) newUsersCreationJob(c *v1beta1.PostgreSQL) scheduler.Job { + l := log.Log.WithValues("component", "postgresqlUsersCreationJob") + + return func() error { + ctx := context.Background() + + err := r.Get(ctx, types.NamespacedName{ + Namespace: c.Namespace, + Name: c.Name, + }, c) + if err != nil { + if k8serrors.IsNotFound(err) { + return nil + } + return err + } + + if c.Status.State != models.RunningStatus { + l.Info("User creation job is scheduled") + r.EventRecorder.Event(c, models.Normal, models.CreationFailed, + "User creation job is scheduled, cluster is not in the running state") + return nil + } + + for _, ref := range c.Spec.UserRefs { + err = r.createUser(ctx, l, c, ref) + if err != nil { + l.Error(err, "Failed to create a user for the cluster", "user ref", ref) + r.EventRecorder.Eventf(c, models.Warning, models.CreationFailed, + "Failed to create a user for the cluster. Reason: %v", err) + return err + } + } + + l.Info("User creation job successfully finished", "resource name", c.Name) + r.EventRecorder.Eventf(c, models.Normal, models.Created, "User creation job successfully finished") + + go r.Scheduler.RemoveJob(c.GetJobID(scheduler.UserCreator)) + + return nil + } +} + func (r *PostgreSQLReconciler) listClusterBackups(ctx context.Context, clusterID, namespace string) (*clusterresourcesv1beta1.ClusterBackupList, error) { backupsList := &clusterresourcesv1beta1.ClusterBackupList{} listOpts := []client.ListOption{ @@ -1190,8 +1387,8 @@ func (r *PostgreSQLReconciler) deleteBackups(ctx context.Context, clusterID, nam return nil } -func (r *PostgreSQLReconciler) deleteSecret(ctx context.Context, pgCluster *v1beta1.PostgreSQL) error { - secret, err := pgCluster.GetUserSecret(ctx, r.Client) +func (r *PostgreSQLReconciler) deleteSecret(ctx context.Context, pg *v1beta1.PostgreSQL) error { + secret, err := v1beta1.GetDefaultPgUserSecret(ctx, pg.Name, pg.Namespace, r.Client) if err != nil { return err } @@ -1370,6 +1567,10 @@ func (r *PostgreSQLReconciler) SetupWithManager(mgr ctrl.Manager) error { return false } + oldObj := event.ObjectOld.(*v1beta1.PostgreSQL) + + r.handleUserEvent(newObj, oldObj.Spec.UserRefs) + event.ObjectNew.GetAnnotations()[models.ResourceStateAnnotation] = models.UpdatingEvent return true }, diff --git a/controllers/tests/datatest/secret.yaml b/controllers/tests/datatest/secret.yaml index d1e40b125..8f20f2a6c 100644 --- a/controllers/tests/datatest/secret.yaml +++ b/controllers/tests/datatest/secret.yaml @@ -4,5 +4,5 @@ metadata: name: secret-sample namespace: default data: - password: NDgxMzU5ODM1NzlmMDU0ZTlhY2I4ZjcxMTMzMzQ1MjM3ZQo= - username: b2xvbG8K + password: NDgxMzU5ODM1NzlmMDU0ZTlhY2I4ZjcxMTMzMzQ1MjM3ZQ== + username: b2xvbG8= diff --git a/go.mod b/go.mod index 147952f32..1cb9a4a96 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ require ( github.com/go-logr/logr v1.2.0 github.com/gorilla/mux v1.8.0 github.com/hashicorp/go-version v1.6.0 + github.com/jackc/pgx/v5 v5.4.3 + github.com/lib/pq v1.10.9 github.com/onsi/ginkgo/v2 v2.0.0 github.com/onsi/gomega v1.18.1 go.uber.org/zap v1.19.1 @@ -45,6 +47,8 @@ require ( github.com/google/gofuzz v1.1.0 // indirect github.com/google/uuid v1.1.2 // indirect github.com/imdario/mergo v0.3.12 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.6 // indirect @@ -57,15 +61,16 @@ require ( github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/spf13/pflag v1.0.5 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect - golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect - golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/net v0.10.0 // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/term v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index 0af4ef4f3..66a6c251a 100644 --- a/go.sum +++ b/go.sum @@ -294,6 +294,12 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= +github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= @@ -320,10 +326,13 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -359,7 +368,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -422,6 +430,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -456,8 +466,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -523,8 +533,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -610,8 +621,8 @@ golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -703,11 +714,12 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -716,8 +728,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -905,8 +918,8 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= diff --git a/main.go b/main.go index d1015472b..0836739aa 100644 --- a/main.go +++ b/main.go @@ -421,6 +421,14 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "OpenSearchUser") os.Exit(1) } + if err = (&clusterresourcescontrollers.PostgreSQLUserReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: eventRecorder, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "PostgreSQLUser") + os.Exit(1) + } if err = (&clusterresourcesv1beta1.OpenSearchUser{}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "OpenSearchUser") os.Exit(1) diff --git a/pkg/exposeservice/expose_service.go b/pkg/exposeservice/expose_service.go index 6318be4ac..ea02de72f 100644 --- a/pkg/exposeservice/expose_service.go +++ b/pkg/exposeservice/expose_service.go @@ -37,7 +37,7 @@ func Create( nodes []*v1beta1.Node, targetPort int32, ) error { - svcName := fmt.Sprintf("%s-service", clusterName) + svcName := fmt.Sprintf(models.ExposeServiceNameTemplate, clusterName) service := &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: svcName, @@ -73,6 +73,7 @@ func Create( ObjectMeta: metav1.ObjectMeta{ Name: svcName, Namespace: ns, + Labels: map[string]string{models.ClusterNameLabel: clusterName}, }, Subsets: []v1.EndpointSubset{ { @@ -135,3 +136,41 @@ func Delete( return nil } + +func GetExposeService( + c client.Client, + clusterName string, + ns string, +) (*v1.ServiceList, error) { + serviceList := &v1.ServiceList{} + listOpts := []client.ListOption{ + client.InNamespace(ns), + client.MatchingLabels{models.ClusterNameLabel: clusterName}, + } + + err := c.List(context.Background(), serviceList, listOpts...) + if err != nil { + return nil, err + } + + return serviceList, nil +} + +func GetExposeServiceEndpoints( + c client.Client, + clusterName string, + ns string, +) (*v1.EndpointsList, error) { + endpointsList := &v1.EndpointsList{} + listOpts := []client.ListOption{ + client.InNamespace(ns), + client.MatchingLabels{models.ClusterNameLabel: clusterName}, + } + + err := c.List(context.Background(), endpointsList, listOpts...) + if err != nil { + return nil, err + } + + return endpointsList, nil +} diff --git a/pkg/models/errors.go b/pkg/models/errors.go index 515fa02eb..5d77432a2 100644 --- a/pkg/models/errors.go +++ b/pkg/models/errors.go @@ -61,4 +61,6 @@ var ( ErrPrivateLinkSupportedOnlyForSingleDC = errors.New("private link is only supported for a single data centre") ErrPrivateLinkSupportedOnlyForAWS = errors.New("private link is supported only for an AWS cloud provider") ErrImmutableSpec = errors.New("resource specification is immutable") + ErrExposeServiceNotCreatedYet = errors.New("expose service is not created yet") + ErrExposeServiceEndpointsNotCreatedYet = errors.New("expose service endpoints is not created yet") ) diff --git a/pkg/models/operator.go b/pkg/models/operator.go index 8fb93e50a..3c37f907c 100644 --- a/pkg/models/operator.go +++ b/pkg/models/operator.go @@ -37,6 +37,7 @@ const ( ClustersV1beta1APIVersion = "clusters.instaclustr.com/v1beta1" ClusterresourcesV1beta1APIVersion = "clusterresources.instaclustr.com/v1beta1" RedisUserNamespaceLabel = "instaclustr.com/redisUserNamespace" + PostgreSQLUserNamespaceLabel = "instaclustr.com/postgresqlUserNamespace" OpenSearchUserNamespaceLabel = "instaclustr.com/openSearchUserNamespace" CassandraKind = "Cassandra" @@ -59,19 +60,22 @@ const ( Triggered = "triggered" - ClusterBackupKind = "ClusterBackup" - PgClusterKind = "PostgreSQL" - RedisClusterKind = "Redis" - OsClusterKind = "OpenSearch" - CassandraClusterKind = "Cassandra" - ZookeeperClusterKind = "Zookeeper" - SecretKind = "Secret" - PgBackupEventType = "postgresql-backup" - SnapshotUploadEventType = "snapshot-upload" - PgBackupPrefix = "postgresql-backup-" - SnapshotUploadPrefix = "snapshot-upload-" - DefaultUserSecretPrefix = "default-user-password" - DefaultUserSecretNameTemplate = "%s-%s" + ClusterBackupKind = "ClusterBackup" + PgClusterKind = "PostgreSQL" + RedisClusterKind = "Redis" + OsClusterKind = "OpenSearch" + CassandraClusterKind = "Cassandra" + ZookeeperClusterKind = "Zookeeper" + ClusterNetworkFirewallRuleKind = "ClusterNetworkFirewallRule" + SecretKind = "Secret" + PgBackupEventType = "postgresql-backup" + SnapshotUploadEventType = "snapshot-upload" + PgBackupPrefix = "postgresql-backup-" + SnapshotUploadPrefix = "snapshot-upload-" + ClusterNetworkFirewallRulePrefix = "firewall-rule" + DefaultUserSecretPrefix = "default-user-password" + DefaultUserSecretNameTemplate = "%s-%s" + ExposeServiceNameTemplate = "%s-service" CassandraConnectionPort = 9042 CadenceConnectionPort = 7933 @@ -102,6 +106,8 @@ const ( SparkAppType = "SPARK" DefaultPgUsernameValue = "icpostgresql" + DefaultPgDbNameValue = "postgres" + DefaultPgDbPortValue = 5432 ) const (