diff --git a/PROJECT b/PROJECT index 84c22ce98dd..2a80bcae005 100644 --- a/PROJECT +++ b/PROJECT @@ -53,9 +53,11 @@ resources: - group: azure version: v1alpha1 kind: PostgreSQLFirewallRule +- group: azure + kind: PostgreSQLUser + version: v1alpha1 - group: azure version: v1alpha1 - kind: APIMgmtAPI - group: azure version: v1alpha1 kind: ApimService @@ -130,4 +132,4 @@ resources: version: v1alpha1 - group: azure kind: AzureVirtualMachineExtension - version: v1alpha1 \ No newline at end of file + version: v1alpha1 diff --git a/api/v1alpha1/aso_types.go b/api/v1alpha1/aso_types.go index 14fbfa6b039..fbf3eb6c127 100644 --- a/api/v1alpha1/aso_types.go +++ b/api/v1alpha1/aso_types.go @@ -22,6 +22,7 @@ type ASOStatus struct { CompletedAt *metav1.Time `json:"completed,omitempty"` FailedProvisioning bool `json:"failedProvisioning,omitempty"` FlattenedSecrets bool `json:"flattenedSecrets,omitempty"` + Output string `json:"output,omitempty"` } // GenericSpec is a struct to help get the KeyVaultName from the Spec diff --git a/api/v1alpha1/postgresqluser_types.go b/api/v1alpha1/postgresqluser_types.go new file mode 100644 index 00000000000..aaadba84927 --- /dev/null +++ b/api/v1alpha1/postgresqluser_types.go @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// PostgreSQLUserSpec defines the desired state of PostgreSqlUser +type PostgreSQLUserSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + Server string `json:"server"` + DbName string `json:"dbName"` + ResourceGroup string `json:"resourceGroup,omitempty"` + Roles []string `json:"roles"` + // optional + AdminSecret string `json:"adminSecret,omitempty"` + AdminSecretKeyVault string `json:"adminSecretKeyVault,omitempty"` + Username string `json:"username,omitempty"` + KeyVaultToStoreSecrets string `json:"keyVaultToStoreSecrets,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// PostgreSQLUser is the Schema for the postgresqlusers API +// +kubebuilder:resource:shortName=psqlu,path=psqluser +// +kubebuilder:printcolumn:name="Provisioned",type="string",JSONPath=".status.provisioned" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.message" +type PostgreSQLUser struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PostgreSQLUserSpec `json:"spec,omitempty"` + Status ASOStatus `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 init() { + SchemeBuilder.Register(&PostgreSQLUser{}, &PostgreSQLUserList{}) +} + +// IsSubmitted checks if psqluser is provisioning +func (s *PostgreSQLUser) IsSubmitted() bool { + return s.Status.Provisioning || s.Status.Provisioned +} diff --git a/api/v1alpha1/rediscache_types.go b/api/v1alpha1/rediscache_types.go index 54fd0fe544d..92977cc2b0a 100644 --- a/api/v1alpha1/rediscache_types.go +++ b/api/v1alpha1/rediscache_types.go @@ -30,6 +30,7 @@ type RedisCacheProperties struct { EnableNonSslPort bool `json:"enableNonSslPort,omitempty"` SubnetID string `json:"subnetId,omitempty"` StaticIP string `json:"staticIp,omitempty"` + ShardCount *int32 `json:"shardCount,omitempty"` Configuration map[string]string `json:"configuration,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 4e34ef1c368..bee5efb4fd4 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -2890,6 +2890,85 @@ func (in *PostgreSQLServerSpec) DeepCopy() *PostgreSQLServerSpec { 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.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]string, len(*in)) + copy(*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 *PostgreSQLVNetRule) DeepCopyInto(out *PostgreSQLVNetRule) { *out = *in @@ -3196,6 +3275,11 @@ func (in *RedisCacheList) DeepCopyObject() runtime.Object { func (in *RedisCacheProperties) DeepCopyInto(out *RedisCacheProperties) { *out = *in out.Sku = in.Sku + if in.ShardCount != nil { + in, out := &in.ShardCount, &out.ShardCount + *out = new(int32) + **out = **in + } if in.Configuration != nil { in, out := &in.Configuration, &out.Configuration *out = make(map[string]string, len(*in)) diff --git a/api/v1alpha2/aso_types.go b/api/v1alpha2/aso_types.go index cbd72c135b5..2fee004ebf9 100644 --- a/api/v1alpha2/aso_types.go +++ b/api/v1alpha2/aso_types.go @@ -22,6 +22,7 @@ type ASOStatus struct { CompletedAt *metav1.Time `json:"completed,omitempty"` FailedProvisioning bool `json:"failedProvisioning,omitempty"` FlattenedSecrets bool `json:"flattenedSecrets,omitempty"` + Output string `json:"output,omitempty"` } // GenericSpec is a struct to help get the KeyVaultName from the Spec diff --git a/api/v1beta1/aso_types.go b/api/v1beta1/aso_types.go index b8df721332c..8ff8d9b7f32 100644 --- a/api/v1beta1/aso_types.go +++ b/api/v1beta1/aso_types.go @@ -22,6 +22,7 @@ type ASOStatus struct { CompletedAt *metav1.Time `json:"completed,omitempty"` FailedProvisioning bool `json:"failedProvisioning,omitempty"` FlattenedSecrets bool `json:"flattenedSecrets,omitempty"` + Output string `json:"output,omitempty"` } // GenericSpec is a struct to help get the KeyVaultName from the Spec diff --git a/charts/azure-service-operator-0.1.0.tgz b/charts/azure-service-operator-0.1.0.tgz index ec2639d3022..35192b2ec5b 100644 Binary files a/charts/azure-service-operator-0.1.0.tgz and b/charts/azure-service-operator-0.1.0.tgz differ diff --git a/charts/index.yaml b/charts/index.yaml index 7f94240817a..f5c13bc714d 100644 --- a/charts/index.yaml +++ b/charts/index.yaml @@ -3,14 +3,14 @@ entries: azure-service-operator: - apiVersion: v2 appVersion: 0.1.0 - created: "2020-06-01T12:41:48.174129-06:00" + created: "2020-06-02T09:40:24.98057+08:00" dependencies: - condition: azureUseMI name: aad-pod-identity repository: https://raw.githubusercontent.com/Azure/aad-pod-identity/master/charts version: 1.5.5 description: Deploy components and dependencies of azure-service-operator - digest: ec95181a3e59179bfdb3bd92989613b1469104a39e6054acea90382795b1fe2d + digest: b549a78f07f6dca8a8f761cd65791ca8cad73b3bf90b03891101fa2cd70e3de8 home: https://github.com/Azure/azure-service-operator name: azure-service-operator sources: @@ -18,4 +18,4 @@ entries: urls: - azure-service-operator-0.1.0.tgz version: 0.1.0 -generated: "2020-06-01T12:41:48.169188-06:00" +generated: "2020-06-02T09:40:24.976336+08:00" diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 55988cf27b4..a7eecb03ff2 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -22,6 +22,7 @@ resources: - bases/azure.microsoft.com_postgresqldatabases.yaml - bases/azure.microsoft.com_postgresqlfirewallrules.yaml - bases/azure.microsoft.com_postgresqlvnetrules.yaml +- bases/azure.microsoft.com_postgresqlusers.yaml - bases/azure.microsoft.com_apimservices.yaml - bases/azure.microsoft.com_apimgmtapis.yaml - bases/azure.microsoft.com_virtualnetworks.yaml diff --git a/config/crd/patches/cainjection_in_apimgmtapis.yaml b/config/crd/patches/cainjection_in_apimgmtapis.yaml index 43550f92aa2..7d52451cda7 100644 --- a/config/crd/patches/cainjection_in_apimgmtapis.yaml +++ b/config/crd/patches/cainjection_in_apimgmtapis.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: apimgmtapis.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_apimservices.yaml b/config/crd/patches/cainjection_in_apimservices.yaml index eb508984872..55110528ace 100644 --- a/config/crd/patches/cainjection_in_apimservices.yaml +++ b/config/crd/patches/cainjection_in_apimservices.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: apimservices.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_azureloadbalancers.yaml b/config/crd/patches/cainjection_in_azureloadbalancers.yaml index bd84c6d6109..3797d4629b0 100644 --- a/config/crd/patches/cainjection_in_azureloadbalancers.yaml +++ b/config/crd/patches/cainjection_in_azureloadbalancers.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: azureloadbalancers.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_azurenetworkinterfaces.yaml b/config/crd/patches/cainjection_in_azurenetworkinterfaces.yaml index 6e02e00e8ed..4720648c58c 100644 --- a/config/crd/patches/cainjection_in_azurenetworkinterfaces.yaml +++ b/config/crd/patches/cainjection_in_azurenetworkinterfaces.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: azurenetworkinterfaces.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_azurepublicipaddresses.yaml b/config/crd/patches/cainjection_in_azurepublicipaddresses.yaml index f76317fab53..44c923124cb 100644 --- a/config/crd/patches/cainjection_in_azurepublicipaddresses.yaml +++ b/config/crd/patches/cainjection_in_azurepublicipaddresses.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: azurepublicipaddresses.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_azuresqlmanagedusers.yaml b/config/crd/patches/cainjection_in_azuresqlmanagedusers.yaml index 0629b33a9ae..55622affc75 100644 --- a/config/crd/patches/cainjection_in_azuresqlmanagedusers.yaml +++ b/config/crd/patches/cainjection_in_azuresqlmanagedusers.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: azuresqlmanagedusers.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_azuresqlusers.yaml b/config/crd/patches/cainjection_in_azuresqlusers.yaml index 76c8682a6da..392218000c1 100644 --- a/config/crd/patches/cainjection_in_azuresqlusers.yaml +++ b/config/crd/patches/cainjection_in_azuresqlusers.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: azuresqlusers.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_azuresqlvnetrules.yaml b/config/crd/patches/cainjection_in_azuresqlvnetrules.yaml index 8a8dd327b07..8cd7cf19c11 100644 --- a/config/crd/patches/cainjection_in_azuresqlvnetrules.yaml +++ b/config/crd/patches/cainjection_in_azuresqlvnetrules.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: azuresqlvnetrules.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_azurevirtualmachineextensions.yaml b/config/crd/patches/cainjection_in_azurevirtualmachineextensions.yaml index 9d425ac9e6c..ea622f96d16 100644 --- a/config/crd/patches/cainjection_in_azurevirtualmachineextensions.yaml +++ b/config/crd/patches/cainjection_in_azurevirtualmachineextensions.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: azurevirtualmachineextensions.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_azurevirtualmachines.yaml b/config/crd/patches/cainjection_in_azurevirtualmachines.yaml index bee5073111d..1b45c764e17 100644 --- a/config/crd/patches/cainjection_in_azurevirtualmachines.yaml +++ b/config/crd/patches/cainjection_in_azurevirtualmachines.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: azurevirtualmachines.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_azurevmscalesets.yaml b/config/crd/patches/cainjection_in_azurevmscalesets.yaml index 8bdc131fb03..362029d68d9 100644 --- a/config/crd/patches/cainjection_in_azurevmscalesets.yaml +++ b/config/crd/patches/cainjection_in_azurevmscalesets.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: azurevmscalesets.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_cosmosdbs.yaml b/config/crd/patches/cainjection_in_cosmosdbs.yaml index 37d4fb25a8e..42cc99b3293 100644 --- a/config/crd/patches/cainjection_in_cosmosdbs.yaml +++ b/config/crd/patches/cainjection_in_cosmosdbs.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - ccert-manager.io/inject-ca-from: $(NAMESPACE)/$(CERTIFICATENAME) + cert-manager.io/inject-ca-from: $(NAMESPACE)/$(CERTIFICATENAME) name: cosmosdbs.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_keyvaultkeys.yaml b/config/crd/patches/cainjection_in_keyvaultkeys.yaml index 1a2504f9aff..fda791ec8b9 100644 --- a/config/crd/patches/cainjection_in_keyvaultkeys.yaml +++ b/config/crd/patches/cainjection_in_keyvaultkeys.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: keyvaultkeys.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_mysqldatabases.yaml b/config/crd/patches/cainjection_in_mysqldatabases.yaml index 027bb719321..c45d7a631ab 100644 --- a/config/crd/patches/cainjection_in_mysqldatabases.yaml +++ b/config/crd/patches/cainjection_in_mysqldatabases.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: mysqldatabases.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_mysqlfirewallrules.yaml b/config/crd/patches/cainjection_in_mysqlfirewallrules.yaml index 184d3e3e50f..cbffd911d44 100644 --- a/config/crd/patches/cainjection_in_mysqlfirewallrules.yaml +++ b/config/crd/patches/cainjection_in_mysqlfirewallrules.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: mysqlfirewallrules.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_mysqlvnetrules.yaml b/config/crd/patches/cainjection_in_mysqlvnetrules.yaml index 370c08038db..b540a92f04c 100644 --- a/config/crd/patches/cainjection_in_mysqlvnetrules.yaml +++ b/config/crd/patches/cainjection_in_mysqlvnetrules.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: mysqlvnetrules.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_postgresqlusers.yaml b/config/crd/patches/cainjection_in_postgresqlusers.yaml new file mode 100644 index 00000000000..d2c81e8115c --- /dev/null +++ b/config/crd/patches/cainjection_in_postgresqlusers.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: psqlusers.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_postgresqlvnetrules.yaml b/config/crd/patches/cainjection_in_postgresqlvnetrules.yaml index 13ff16ea354..4ad55a764d1 100644 --- a/config/crd/patches/cainjection_in_postgresqlvnetrules.yaml +++ b/config/crd/patches/cainjection_in_postgresqlvnetrules.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: postgresqlvnetrules.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_rediscacheactions.yaml b/config/crd/patches/cainjection_in_rediscacheactions.yaml index fca6f70d2c3..5c85c498ba8 100644 --- a/config/crd/patches/cainjection_in_rediscacheactions.yaml +++ b/config/crd/patches/cainjection_in_rediscacheactions.yaml @@ -4,5 +4,5 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: - certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: rediscacheactions.azure.microsoft.com diff --git a/config/crd/patches/webhook_in_postgresqlusers.yaml b/config/crd/patches/webhook_in_postgresqlusers.yaml new file mode 100644 index 00000000000..61610ffbb4a --- /dev/null +++ b/config/crd/patches/webhook_in_postgresqlusers.yaml @@ -0,0 +1,17 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: postgresqlusers.azure.microsoft.com +spec: + conversion: + strategy: Webhook + webhookClientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/rbac/postgresqluser_editor_role.yaml b/config/rbac/postgresqluser_editor_role.yaml new file mode 100644 index 00000000000..fcb256f4671 --- /dev/null +++ b/config/rbac/postgresqluser_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions to do edit mysqlusers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: postgresqluser-editor-role +rules: +- apiGroups: + - azure.microsoft.com + resources: + - postgresqlusers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - azure.microsoft.com + resources: + - postgre + sqlusers/status + verbs: + - get + - patch + - update diff --git a/config/rbac/postgresqluser_viewer_role.yaml b/config/rbac/postgresqluser_viewer_role.yaml new file mode 100644 index 00000000000..2cceadb4b0c --- /dev/null +++ b/config/rbac/postgresqluser_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions to do viewer mysqlusers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: postgresqluser-viewer-role +rules: +- apiGroups: + - azure.microsoft.com + resources: + - postgresqlusers + verbs: + - get + - list + - watch +- apiGroups: + - azure.microsoft.com + resources: + - postgresqlusers/status + verbs: + - get diff --git a/config/samples/azure_v1alpha1_postgresqluser.yaml b/config/samples/azure_v1alpha1_postgresqluser.yaml new file mode 100644 index 00000000000..925151ecd47 --- /dev/null +++ b/config/samples/azure_v1alpha1_postgresqluser.yaml @@ -0,0 +1,27 @@ +apiVersion: azure.microsoft.com/v1alpha1 +kind: PostgreSQLUser +metadata: + name: psqluser-sample +spec: + server: postgresqlserver-sample + dbName: postgresqldatabase-sample + resourceGroup: resourcegroup-azure-operators + # The Azure Database for PostgreSQL server is created with the 3 default roles defined. + # azure_pg_admin + # azure_superuser + # your server admin user + roles: + - "azure_pg_admin" + # Specify a specific username for the user + # username: psqluser-sample + # Specify adminSecret and adminSecretKeyVault if you want to + # read the PSQL server admin creds from a specific keyvault secret + # adminSecret: postgresqlserver-sample + # adminSecretKeyVault: asokeyvault + + # Use the field below to optionally specify a different keyvault + # to store the secrets in + # keyVaultToStoreSecrets: asokeyvault + + + diff --git a/config/samples/azure_v1alpha1_rediscache.yaml b/config/samples/azure_v1alpha1_rediscache.yaml index 475e83dec4b..a807be0b7b0 100644 --- a/config/samples/azure_v1alpha1_rediscache.yaml +++ b/config/samples/azure_v1alpha1_rediscache.yaml @@ -21,8 +21,11 @@ spec: capacity: 1 enableNonSslPort: true ## Optional - vnet usage may require a higher tier sku - subnetId: /subscriptions/{SUBID}/resourceGroups/{resourcegroupName}/providers/Microsoft.Network/virtualNetworks/{vnet name}/subnets/{subnet name} - staticIp: 172.22.0.10 + # subnetId: /subscriptions/{SUBID}/resourceGroups/{resourcegroupName}/providers/Microsoft.Network/virtualNetworks/{vnet name}/subnets/{subnet name} + ## If subnetId is set but statucIp is empty, the operator will attempt to pick an ip + # staticIp: 172.22.0.10 + # # Optional Shard count config for premium deployments + # shardCount: 2 # All redis configuration - Few possible keys: rdb-backup-enabled,rdb-storage-connection-string,rdb-backup-frequency,maxmemory-delta, # maxmemory-policy,notify-keyspace-events,maxmemory-samples,slowlog-log-slower-than,slowlog-max-len,list-max-ziplist-entries,list-max-ziplist-value, # hash-max-ziplist-entries,hash-max-ziplist-value,set-max-intset-entries,zset-max-ziplist-entries,zset-max-ziplist-value diff --git a/controllers/postgresql_combined_controller_test.go b/controllers/postgresql_combined_controller_test.go index d4700ba5c07..acb8feed8b4 100644 --- a/controllers/postgresql_combined_controller_test.go +++ b/controllers/postgresql_combined_controller_test.go @@ -61,6 +61,24 @@ func TestPSQLDatabaseController(t *testing.T) { EnsureInstance(ctx, t, tc, postgreSQLServerInstance) + postgreSQLFirewallRuleName := GenerateTestResourceNameWithRandom("psql-fwrule", 10) + + // Create the PostgreSQLFirewallRule object and expect the Reconcile to be created + postgreSQLFirewallRuleInstance := &azurev1alpha1.PostgreSQLFirewallRule{ + ObjectMeta: metav1.ObjectMeta{ + Name: postgreSQLFirewallRuleName, + Namespace: "default", + }, + Spec: azurev1alpha1.PostgreSQLFirewallRuleSpec{ + ResourceGroup: rgName, + Server: postgreSQLServerName, + StartIPAddress: "0.0.0.0", + EndIPAddress: "255.255.255.255", + }, + } + + EnsureInstance(ctx, t, tc, postgreSQLFirewallRuleInstance) + postgreSQLDatabaseName := GenerateTestResourceNameWithRandom("psql-db", 10) // Create the PostgreSQLDatabase object and expect the Reconcile to be created @@ -79,26 +97,6 @@ func TestPSQLDatabaseController(t *testing.T) { EnsureDelete(ctx, t, tc, postgreSQLDatabaseInstance) - // Test firewall rule ------------------------------- - - postgreSQLFirewallRuleName := GenerateTestResourceNameWithRandom("psql-fwrule", 10) - - // Create the PostgreSQLFirewallRule object and expect the Reconcile to be created - postgreSQLFirewallRuleInstance := &azurev1alpha1.PostgreSQLFirewallRule{ - ObjectMeta: metav1.ObjectMeta{ - Name: postgreSQLFirewallRuleName, - Namespace: "default", - }, - Spec: azurev1alpha1.PostgreSQLFirewallRuleSpec{ - ResourceGroup: rgName, - Server: postgreSQLServerName, - StartIPAddress: "0.0.0.0", - EndIPAddress: "0.0.0.0", - }, - } - - EnsureInstance(ctx, t, tc, postgreSQLFirewallRuleInstance) - EnsureDelete(ctx, t, tc, postgreSQLFirewallRuleInstance) EnsureDelete(ctx, t, tc, postgreSQLServerInstance) diff --git a/controllers/postgresqluser_controller.go b/controllers/postgresqluser_controller.go new file mode 100644 index 00000000000..9da8f196564 --- /dev/null +++ b/controllers/postgresqluser_controller.go @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package controllers + +import ( + ctrl "sigs.k8s.io/controller-runtime" + + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" +) + +// PostgreSQLUserReconciler reconciles a PSQLUser object +type PostgreSQLUserReconciler struct { + Reconciler *AsyncReconciler +} + +//Reconcile for postgresqluser +// +kubebuilder:rbac:groups=azure.microsoft.com,resources=postgresqlusers,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=azure.microsoft.com,resources=postgresqlusers/status,verbs=get;update;patch +func (r *PostgreSQLUserReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + return r.Reconciler.Reconcile(req, &azurev1alpha1.PostgreSQLUser{}) +} + +// SetupWithManager runs reconcile loop with manager +func (r *PostgreSQLUserReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&azurev1alpha1.PostgreSQLUser{}). + Complete(r) +} diff --git a/controllers/postgresqluser_controller_test.go b/controllers/postgresqluser_controller_test.go new file mode 100644 index 00000000000..a169efa10b3 --- /dev/null +++ b/controllers/postgresqluser_controller_test.go @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// +build all postgresqluser + +package controllers + +import ( + "context" + "testing" + + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/pkg/errhelp" + "github.com/Azure/azure-service-operator/pkg/helpers" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestPostgreSQLUserControllerNoAdminSecret(t *testing.T) { + t.Parallel() + defer PanicRecover(t) + ctx := context.Background() + + var postgresqlServerName string + var postgresqlDatabaseName string + var postgresqlUser *azurev1alpha1.PostgreSQLUser + + postgresqlServerName = GenerateTestResourceNameWithRandom("psqlserver-test", 10) + postgresqlDatabaseName = GenerateTestResourceNameWithRandom("psqldb-test", 10) + pusername := "psql-test-user" + helpers.RandomString(10) + roles := []string{"azure_pg_admin"} + + postgresqlUser = &azurev1alpha1.PostgreSQLUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: pusername, + Namespace: "default", + }, + Spec: azurev1alpha1.PostgreSQLUserSpec{ + Server: postgresqlServerName, + DbName: postgresqlDatabaseName, + AdminSecret: "", + Roles: roles, + }, + } + + EnsureInstanceWithResult(ctx, t, tc, postgresqlUser, "admin secret", false) + + EnsureDelete(ctx, t, tc, postgresqlUser) +} + +func TestPostgreSQLUserControllerNoResourceGroup(t *testing.T) { + t.Parallel() + defer PanicRecover(t) + ctx := context.Background() + assert := assert.New(t) + var err error + var psqlServerName string + var psqlDatabaseName string + var psqlUser *azurev1alpha1.PostgreSQLUser + + psqlServerName = GenerateTestResourceNameWithRandom("psqlserver-test", 10) + psqlDatabaseName = GenerateTestResourceNameWithRandom("psqldb-test", 10) + pusername := "psql-test-user" + helpers.RandomString(10) + roles := []string{"azure_pg_admin"} + + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: psqlServerName, + Namespace: "default", + }, + // Needed to avoid nil map error + Data: map[string][]byte{ + "username": []byte("username"), + "password": []byte("password"), + }, + Type: "Opaque", + } + + // Create the sqlUser + err = tc.k8sClient.Create(ctx, secret) + assert.Equal(nil, err, "create admin secret in k8s") + + psqlUser = &azurev1alpha1.PostgreSQLUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: pusername, + Namespace: "default", + }, + Spec: azurev1alpha1.PostgreSQLUserSpec{ + Server: psqlServerName, + DbName: psqlDatabaseName, + AdminSecret: "", + Roles: roles, + ResourceGroup: "fakerg" + helpers.RandomString(10), + }, + } + + EnsureInstanceWithResult(ctx, t, tc, psqlUser, errhelp.ResourceGroupNotFoundErrorCode, false) + + EnsureDelete(ctx, t, tc, psqlUser) + +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go index b6459364ee0..44d6325e58a 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -44,6 +44,7 @@ import ( resourcemanagerpip "github.com/Azure/azure-service-operator/pkg/resourcemanager/pip" resourcemanagerpsqldatabase "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/database" resourcemanagerpsqlfirewallrule "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/firewallrule" + resourcemanagerpsqluser "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/psqluser" resourcemanagerpsqlserver "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/server" rediscacheactions "github.com/Azure/azure-service-operator/pkg/resourcemanager/rediscaches/actions" rcfwr "github.com/Azure/azure-service-operator/pkg/resourcemanager/rediscaches/firewallrule" @@ -756,6 +757,22 @@ func setup() error { return err } + err = (&PostgreSQLUserReconciler{ + Reconciler: &AsyncReconciler{ + Client: k8sManager.GetClient(), + AzureClient: resourcemanagerpsqluser.NewPostgreSqlUserManager(secretClient, k8sManager.GetScheme()), + Telemetry: telemetry.InitializeTelemetryDefault( + "PostgreSQLUser", + ctrl.Log.WithName("controllers").WithName("PostgreSQLUser"), + ), + Recorder: k8sManager.GetEventRecorderFor("PostgreSQLUser-controller"), + Scheme: k8sManager.GetScheme(), + }, + }).SetupWithManager(k8sManager) + if err != nil { + return err + } + err = (&StorageAccountReconciler{ Reconciler: &AsyncReconciler{ Client: k8sManager.GetClient(), diff --git a/docs/howto/newoperatorguide.md b/docs/howto/newoperatorguide.md index caa1e2ed48a..a3d9748f446 100644 --- a/docs/howto/newoperatorguide.md +++ b/docs/howto/newoperatorguide.md @@ -321,7 +321,7 @@ go build -o bin/manager main.go make install ``` - Run controller tests using `make test-existing-controllers` and deploy using `make deploy` + Run controller tests using `make test-integration-controllers` and deploy using `make deploy` If you make changes to the operator and want to update the deployment without recreating the cluster (when testing locally), you can use the `make update` to update your Azure Operator pod. If you need to rebuild the docker image without cache, use `make ARGS="--no-cache" update` diff --git a/go.mod b/go.mod index fd5dd58a338..677e09ad206 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.1.1 github.com/hashicorp/go-multierror v1.0.0 + github.com/lib/pq v1.6.0 // indirect github.com/marstr/randname v0.0.0-20181206212954-d5b0f288ab8c github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.3.0 // indirect @@ -34,7 +35,7 @@ require ( github.com/spf13/viper v1.6.3 github.com/stretchr/testify v1.5.1 golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4 - golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 + golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 // indirect gopkg.in/ini.v1 v1.55.0 // indirect k8s.io/api v0.17.2 diff --git a/go.sum b/go.sum index f76b9eb8bff..d76a6b4f9c4 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5Vpd github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -240,6 +241,8 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -259,6 +262,8 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -273,6 +278,18 @@ github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8= +github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.2.0 h1:lzPl/30ZLkTveYsYZPKMcgXc8MbnE6RsTd4F9KgiLtk= +github.com/jcmturner/gokrb5/v8 v8.2.0/go.mod h1:T1hnNppQsBtxW0tCHMHTkAt8n/sABdzZgZdoFrZaZNM= +github.com/jcmturner/rpc/v2 v2.0.2 h1:gMB4IwRXYsWw4Bc6o/az2HJgFUA1ffSh90i26ZJ6Xl0= +github.com/jcmturner/rpc/v2 v2.0.2/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -301,6 +318,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.6.0 h1:I5DPxhYJChW9KYc66se+oKFFQX6VuQrKiprsX6ivRZc= +github.com/lib/pq v1.6.0/go.mod h1:4vXEAYvW1fRQ2/FhZ78H73A60MHw1geSm145z2mdY1g= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -515,6 +534,7 @@ golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaE golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4 h1:QmwruyY+bKbDDL0BaglrbZABEali68eoMFhTZpCjYVA= golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -548,6 +568,8 @@ golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -659,6 +681,11 @@ gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ= gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= +gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= +gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= +gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= +gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= diff --git a/main.go b/main.go index e8e667805c1..3104ff2fcb8 100644 --- a/main.go +++ b/main.go @@ -47,6 +47,7 @@ import ( pip "github.com/Azure/azure-service-operator/pkg/resourcemanager/pip" psqldatabase "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/database" psqlfirewallrule "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/firewallrule" + psqluser "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/psqluser" psqlserver "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/server" psqlvnetrule "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/vnetrule" rediscacheactions "github.com/Azure/azure-service-operator/pkg/resourcemanager/rediscaches/actions" @@ -179,6 +180,10 @@ func main() { psqlserverclient := psqlserver.NewPSQLServerClient(secretClient, mgr.GetScheme()) psqldatabaseclient := psqldatabase.NewPSQLDatabaseClient() psqlfirewallruleclient := psqlfirewallrule.NewPSQLFirewallRuleClient() + psqlusermanager := psqluser.NewPostgreSqlUserManager( + secretClient, + scheme, + ) sqlUserManager := resourcemanagersqluser.NewAzureSqlUserManager( secretClient, scheme, @@ -578,6 +583,22 @@ func main() { os.Exit(1) } + if err = (&controllers.PostgreSQLUserReconciler{ + Reconciler: &controllers.AsyncReconciler{ + Client: mgr.GetClient(), + AzureClient: psqlusermanager, + Telemetry: telemetry.InitializeTelemetryDefault( + "PSQLUser", + ctrl.Log.WithName("controllers").WithName("PostgreSQLUser"), + ), + Recorder: mgr.GetEventRecorderFor("PostgreSQLUser-controller"), + Scheme: scheme, + }, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "PostgreSQLUser") + os.Exit(1) + } + if err = (&controllers.ApimServiceReconciler{ Reconciler: &controllers.AsyncReconciler{ Client: mgr.GetClient(), diff --git a/pkg/helpers/stringhelper.go b/pkg/helpers/stringhelper.go index e223f68948e..aad3705d718 100644 --- a/pkg/helpers/stringhelper.go +++ b/pkg/helpers/stringhelper.go @@ -189,3 +189,21 @@ func FromBase64EncodedString(input string) string { decodedString := string(output) return decodedString } + +// FindBadChars find the bad chars in a postgresql user +func FindBadChars(stack string) error { + badChars := []string{ + "'", + "\"", + ";", + "--", + "/*", + } + + for _, s := range badChars { + if idx := strings.Index(stack, s); idx > -1 { + return fmt.Errorf("potentially dangerous character seqience found: '%s' at pos: %d", s, idx) + } + } + return nil +} diff --git a/pkg/resourcemanager/psql/database/database.go b/pkg/resourcemanager/psql/database/database.go index 5f39c108ae8..18c686f7754 100644 --- a/pkg/resourcemanager/psql/database/database.go +++ b/pkg/resourcemanager/psql/database/database.go @@ -19,7 +19,8 @@ func NewPSQLDatabaseClient() *PSQLDatabaseClient { return &PSQLDatabaseClient{} } -func getPSQLDatabasesClient() (psql.DatabasesClient, error) { +//GetPSQLDatabasesClient retrieves the psqldabase +func GetPSQLDatabasesClient() (psql.DatabasesClient, error) { databasesClient := psql.NewDatabasesClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) a, err := iam.GetResourceManagementAuthorizer() if err != nil { @@ -64,7 +65,7 @@ func (p *PSQLDatabaseClient) CheckDatabaseNameAvailability(ctx context.Context, func (p *PSQLDatabaseClient) CreateDatabaseIfValid(ctx context.Context, databasename string, servername string, resourcegroup string) (*http.Response, error) { - client, err := getPSQLDatabasesClient() + client, err := GetPSQLDatabasesClient() if err != nil { return &http.Response{ StatusCode: 500, @@ -99,7 +100,7 @@ func (p *PSQLDatabaseClient) CreateDatabaseIfValid(ctx context.Context, database func (p *PSQLDatabaseClient) DeleteDatabase(ctx context.Context, databasename string, servername string, resourcegroup string) (status string, err error) { - client, err := getPSQLDatabasesClient() + client, err := GetPSQLDatabasesClient() if err != nil { return "", err } @@ -116,7 +117,7 @@ func (p *PSQLDatabaseClient) DeleteDatabase(ctx context.Context, databasename st func (p *PSQLDatabaseClient) GetDatabase(ctx context.Context, resourcegroup string, servername string, databasename string) (db psql.Database, err error) { - client, err := getPSQLDatabasesClient() + client, err := GetPSQLDatabasesClient() if err != nil { return psql.Database{}, err } diff --git a/pkg/resourcemanager/psql/database/database_reconcile.go b/pkg/resourcemanager/psql/database/database_reconcile.go index 2a94fdb30d0..c28830a431a 100644 --- a/pkg/resourcemanager/psql/database/database_reconcile.go +++ b/pkg/resourcemanager/psql/database/database_reconcile.go @@ -9,6 +9,7 @@ import ( "github.com/Azure/azure-service-operator/api/v1alpha1" azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/api/v1alpha2" "github.com/Azure/azure-service-operator/pkg/errhelp" "github.com/Azure/azure-service-operator/pkg/helpers" "github.com/Azure/azure-service-operator/pkg/resourcemanager" @@ -138,7 +139,7 @@ func (p *PSQLDatabaseClient) GetParents(obj runtime.Object) ([]resourcemanager.K Namespace: instance.Namespace, Name: instance.Spec.Server, }, - Target: &azurev1alpha1.PostgreSQLServer{}, + Target: &v1alpha2.PostgreSQLServer{}, }, { Key: types.NamespacedName{ diff --git a/pkg/resourcemanager/psql/psqluser/psqluser.go b/pkg/resourcemanager/psql/psqluser/psqluser.go new file mode 100644 index 00000000000..8a173f0efa7 --- /dev/null +++ b/pkg/resourcemanager/psql/psqluser/psqluser.go @@ -0,0 +1,223 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package psqluser + +import ( + "context" + "database/sql" + "fmt" + "strings" + + psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" + "github.com/Azure/azure-service-operator/pkg/helpers" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" + psdatabase "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/database" + "github.com/Azure/azure-service-operator/pkg/secrets" + + "github.com/Azure/azure-service-operator/api/v1alpha1" + "k8s.io/apimachinery/pkg/runtime" + + _ "github.com/lib/pq" //the pg lib + "k8s.io/apimachinery/pkg/types" +) + +// PSqlServerPort is the default server port for sql server +const PSqlServerPort = 5432 + +// PDriverName is driver name for psqldb connection +const PDriverName = "postgres" + +// PSecretUsernameKey is the username key in secret +const PSecretUsernameKey = "username" + +// PSecretPasswordKey is the password key in secret +const PSecretPasswordKey = "password" + +//PostgreSqlUserManager for psqluser manager +type PostgreSqlUserManager struct { + SecretClient secrets.SecretClient + Scheme *runtime.Scheme +} + +//NewPostgreSqlUserManager creates a new PostgreSqlUserManager +func NewPostgreSqlUserManager(secretClient secrets.SecretClient, scheme *runtime.Scheme) *PostgreSqlUserManager { + return &PostgreSqlUserManager{ + SecretClient: secretClient, + Scheme: scheme, + } +} + +// GetDB retrieves a database +func (s *PostgreSqlUserManager) GetDB(ctx context.Context, resourceGroupName string, serverName string, databaseName string) (db psql.Database, err error) { + dbClient, err := psdatabase.GetPSQLDatabasesClient() + if err != nil { + return psql.Database{}, err + } + + return dbClient.Get( + ctx, + resourceGroupName, + serverName, + databaseName, + ) +} + +// ConnectToSqlDb connects to the PostgreSQL db using the given credentials +func (s *PostgreSqlUserManager) ConnectToSqlDb(ctx context.Context, drivername string, fullservername string, database string, port int, user string, password string) (*sql.DB, error) { + + connString := fmt.Sprintf("host=%s user=%s password=%s port=%d dbname=%s sslmode=require connect_timeout=30", fullservername, user, password, port, database) + + db, err := sql.Open(drivername, connString) + if err != nil { + return db, err + } + + err = db.PingContext(ctx) + if err != nil { + return db, err + } + + return db, err +} + +// GrantUserRoles grants roles to a user for a given database +func (s *PostgreSqlUserManager) GrantUserRoles(ctx context.Context, user string, roles []string, db *sql.DB) error { + var errorStrings []string + + if err := helpers.FindBadChars(user); err != nil { + return fmt.Errorf("Problem found with username: %v", err) + } + + for _, role := range roles { + tsql := fmt.Sprintf("GRANT %s TO %q", role, user) + + if err := helpers.FindBadChars(role); err != nil { + return fmt.Errorf("Problem found with role: %v", err) + } + + _, err := db.ExecContext(ctx, tsql) + if err != nil { + errorStrings = append(errorStrings, err.Error()) + } + } + + if len(errorStrings) != 0 { + return fmt.Errorf(strings.Join(errorStrings, "\n")) + } + return nil +} + +// CreateUser creates user with secret credentials +func (s *PostgreSqlUserManager) CreateUser(ctx context.Context, secret map[string][]byte, db *sql.DB) (string, error) { + newUser := string(secret[PSecretUsernameKey]) + newPassword := string(secret[PSecretPasswordKey]) + + // make an effort to prevent sql injection + if err := helpers.FindBadChars(newUser); err != nil { + return "", fmt.Errorf("Problem found with username: %v", err) + } + if err := helpers.FindBadChars(newPassword); err != nil { + return "", fmt.Errorf("Problem found with password: %v", err) + } + + tsql := fmt.Sprintf("CREATE USER \"%s\" WITH PASSWORD '%s'", newUser, newPassword) + _, err := db.ExecContext(ctx, tsql) + + if err != nil { + return newUser, err + } + return newUser, nil +} + +// UpdateUser - Updates user password +func (s *PostgreSqlUserManager) UpdateUser(ctx context.Context, secret map[string][]byte, db *sql.DB) error { + user := string(secret[PSecretUsernameKey]) + newPassword := helpers.NewPassword() + + // make an effort to prevent sql injection + if err := helpers.FindBadChars(user); err != nil { + return fmt.Errorf("Problem found with username: %v", err) + } + if err := helpers.FindBadChars(newPassword); err != nil { + return fmt.Errorf("Problem found with password: %v", err) + } + + tsql := fmt.Sprintf("ALTER USER '%s' WITH PASSWORD '%s'", user, newPassword) + _, err := db.ExecContext(ctx, tsql) + + return err +} + +// UserExists checks if db contains user +func (s *PostgreSqlUserManager) UserExists(ctx context.Context, db *sql.DB, username string) (bool, error) { + + res, err := db.ExecContext(ctx, "SELECT * FROM pg_user WHERE usename = $1", username) + if err != nil { + return false, err + } + rows, err := res.RowsAffected() + return rows > 0, err + +} + +// DropUser drops a user from db +func (s *PostgreSqlUserManager) DropUser(ctx context.Context, db *sql.DB, user string) error { + if err := helpers.FindBadChars(user); err != nil { + return fmt.Errorf("Problem found with username: %v", err) + } + + tsql := fmt.Sprintf("DROP USER IF EXISTS %q", user) + _, err := db.ExecContext(ctx, tsql) + return err +} + +// DeleteSecrets deletes the secrets associated with a SQLUser +func (s *PostgreSqlUserManager) DeleteSecrets(ctx context.Context, instance *v1alpha1.PostgreSQLUser, secretClient secrets.SecretClient) (bool, error) { + + secretKey := GetNamespacedName(instance, secretClient) + + // delete standard user secret + err := secretClient.Delete( + ctx, + secretKey, + ) + if err != nil { + instance.Status.Message = "failed to delete secret, err: " + err.Error() + return false, err + } + + return false, nil +} + +// GetOrPrepareSecret gets or creates a secret +func (s *PostgreSqlUserManager) GetOrPrepareSecret(ctx context.Context, instance *v1alpha1.PostgreSQLUser, secretClient secrets.SecretClient) map[string][]byte { + key := GetNamespacedName(instance, secretClient) + + secret, err := secretClient.Get(ctx, key) + + psqldbdnssuffix := "postgres.database.azure.com" + if config.Environment().Name != "AzurePublicCloud" { + psqldbdnssuffix = "postgres." + config.Environment().SQLDatabaseDNSSuffix + } + + if err != nil { + // @todo: find out whether this is an error due to non existing key or failed conn + pw := helpers.NewPassword() + return map[string][]byte{ + "username": []byte(""), + "password": []byte(pw), + "PSqlServerNamespace": []byte(instance.Namespace), + "PSqlServerName": []byte(instance.Spec.Server), + "fullyQualifiedServerName": []byte(instance.Spec.Server + "." + psqldbdnssuffix), + "PSqlDatabaseName": []byte(instance.Spec.DbName), + } + } + + return secret +} + +// GetNamespacedName gets the namespaced-name +func GetNamespacedName(instance *v1alpha1.PostgreSQLUser, secretClient secrets.SecretClient) types.NamespacedName { + return types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace} +} diff --git a/pkg/resourcemanager/psql/psqluser/psqluser_manager.go b/pkg/resourcemanager/psql/psqluser/psqluser_manager.go new file mode 100644 index 00000000000..c39d85743a6 --- /dev/null +++ b/pkg/resourcemanager/psql/psqluser/psqluser_manager.go @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package psqluser + +import ( + "context" + "database/sql" + + psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" + + "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/pkg/resourcemanager" + "github.com/Azure/azure-service-operator/pkg/secrets" +) + +type PSqlUserManager interface { + GetDB(ctx context.Context, resourceGroupName string, serverName string, databaseName string) (psql.Database, error) + ConnectToPostgreSqlDb(ctx context.Context, drivername string, server string, dbname string, port int, username string, password string) (*sql.DB, error) + GrantUserRoles(ctx context.Context, user string, roles []string, db *sql.DB) error + CreateUser(ctx context.Context, secret map[string][]byte, db *sql.DB) (string, error) + UserExists(ctx context.Context, db *sql.DB, username string) (bool, error) + DropUser(ctx context.Context, db *sql.DB, user string) error + DeleteSecrets(ctx context.Context, instance *v1alpha1.PostgreSQLUser, secretClient secrets.SecretClient) (bool, error) + GetOrPrepareSecret(ctx context.Context, instance *v1alpha1.PostgreSQLUser, secretClient secrets.SecretClient) map[string][]byte + + // also embed methods from AsyncClient + resourcemanager.ARMClient +} diff --git a/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go new file mode 100644 index 00000000000..897a662a895 --- /dev/null +++ b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go @@ -0,0 +1,342 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package psqluser + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "strings" + + "github.com/Azure/azure-service-operator/pkg/helpers" + "github.com/Azure/azure-service-operator/pkg/secrets" + + "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/api/v1alpha2" + "github.com/Azure/azure-service-operator/pkg/errhelp" + "github.com/Azure/azure-service-operator/pkg/resourcemanager" + keyvaultSecrets "github.com/Azure/azure-service-operator/pkg/secrets/keyvault" + + _ "github.com/lib/pq" //pglib + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +// Ensure that user exists +func (s *PostgreSqlUserManager) Ensure(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + instance, err := s.convert(obj) + if err != nil { + return false, err + } + + requestedUsername := instance.Spec.Username + if len(requestedUsername) == 0 { + requestedUsername = instance.Name + } + + options := &resourcemanager.Options{} + for _, opt := range opts { + opt(options) + } + + adminSecretClient := s.SecretClient + + adminsecretName := instance.Spec.AdminSecret + if len(instance.Spec.AdminSecret) == 0 { + adminsecretName = instance.Spec.Server + } + + key := types.NamespacedName{Name: adminsecretName, Namespace: instance.Namespace} + + var sqlUserSecretClient secrets.SecretClient + if options.SecretClient != nil { + sqlUserSecretClient = options.SecretClient + } else { + sqlUserSecretClient = s.SecretClient + } + + // if the admin secret keyvault is not specified, fall back to global secretclient + if len(instance.Spec.AdminSecretKeyVault) != 0 { + adminSecretClient = keyvaultSecrets.New(instance.Spec.AdminSecretKeyVault) + if len(instance.Spec.AdminSecret) != 0 { + key = types.NamespacedName{Name: instance.Spec.AdminSecret} + } + } + + // get admin creds for server + adminSecret, err := adminSecretClient.Get(ctx, key) + if err != nil { + instance.Status.Provisioning = false + instance.Status.Message = fmt.Sprintf("admin secret : %s, not found in %s", key.String(), reflect.TypeOf(adminSecretClient).Elem().Name()) + return false, nil + } + + adminUser := string(adminSecret["fullyQualifiedUsername"]) + adminPassword := string(adminSecret[PSecretPasswordKey]) + + _, err = s.GetDB(ctx, instance.Spec.ResourceGroup, instance.Spec.Server, instance.Spec.DbName) + if err != nil { + instance.Status.Message = errhelp.StripErrorIDs(err) + instance.Status.Provisioning = false + + requeuErrors := []string{ + errhelp.ResourceNotFound, + errhelp.ParentNotFoundErrorCode, + errhelp.ResourceGroupNotFoundErrorCode, + } + azerr := errhelp.NewAzureErrorAzureError(err) + if helpers.ContainsString(requeuErrors, azerr.Type) { + return false, nil + } + + // if the database is busy, requeue + errorString := err.Error() + if strings.Contains(errorString, "Please retry the connection later") { + return false, nil + } + + // if this is an unmarshall error - ignore and continue, otherwise report error and requeue + if _, ok := err.(*json.UnmarshalTypeError); ok { + return false, nil + } + } + + fullServerName := string(adminSecret["fullyQualifiedServerName"]) + db, err := s.ConnectToSqlDb(ctx, PDriverName, fullServerName, instance.Spec.DbName, PSqlServerPort, adminUser, adminPassword) + if err != nil { + instance.Status.Message = errhelp.StripErrorIDs(err) + instance.Status.Provisioning = false + + // catch firewall issue - keep cycling until it clears up + if strings.Contains(err.Error(), "no pg_hba.conf entry for host") { + instance.Status.Message = errhelp.StripErrorIDs(err) + "\nThe IP address is not allowed to access the server. Modify the firewall rule to include the IP address." + return false, nil + } + + // if the database is busy, requeue + errorString := err.Error() + if strings.Contains(errorString, "Please retry the connection later") { + return false, nil + } + + return false, err + } + + // determine our key namespace - if we're persisting to kube, we should use the actual instance namespace. + // In keyvault we have to avoid collisions with other secrets so we create a custom namespace with the user's parameters + key = GetNamespacedName(instance, sqlUserSecretClient) + + // create or get new user secret + DBSecret := s.GetOrPrepareSecret(ctx, instance, sqlUserSecretClient) + // reset user from secret in case it was loaded + user := string(DBSecret[PSecretUsernameKey]) + if user == "" { + user = fmt.Sprintf(requestedUsername) + DBSecret[PSecretUsernameKey] = []byte(user) + } + + // Publishing the user secret: + // We do this first so if the keyvault does not have right permissions we will not proceed to creating the user + err = sqlUserSecretClient.Upsert( + ctx, + key, + DBSecret, + secrets.WithOwner(instance), + secrets.WithScheme(s.Scheme), + ) + if err != nil { + instance.Status.Message = "failed to update secret, err: " + err.Error() + return false, err + } + + userExists, err := s.UserExists(ctx, db, string(DBSecret[PSecretUsernameKey])) + if err != nil { + instance.Status.Message = fmt.Sprintf("failed checking for user, err: %v", err) + return false, nil + } + + if !userExists { + user, err = s.CreateUser(ctx, DBSecret, db) + if err != nil { + instance.Status.Message = "failed creating user, err: " + err.Error() + return false, err + } + } + + // apply roles to user + if len(instance.Spec.Roles) == 0 { + instance.Status.Message = "No roles specified for user" + return false, fmt.Errorf("No roles specified for database user") + } + + err = s.GrantUserRoles(ctx, user, instance.Spec.Roles, db) + if err != nil { + instance.Status.Message = "GrantUserRoles failed" + return false, fmt.Errorf("GrantUserRoles failed") + } + + instance.Status.Provisioned = true + instance.Status.State = "Succeeded" + instance.Status.Message = resourcemanager.SuccessMsg + // reconcile done + return true, nil +} + +// Delete deletes a user +func (s *PostgreSqlUserManager) Delete(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + + options := &resourcemanager.Options{} + for _, opt := range opts { + opt(options) + } + + instance, err := s.convert(obj) + if err != nil { + return false, err + } + + adminSecretClient := s.SecretClient + + adminsecretName := instance.Spec.AdminSecret + + if len(instance.Spec.AdminSecret) == 0 { + adminsecretName = instance.Spec.Server + } + key := types.NamespacedName{Name: adminsecretName, Namespace: instance.Namespace} + + var sqlUserSecretClient secrets.SecretClient + if options.SecretClient != nil { + sqlUserSecretClient = options.SecretClient + } else { + sqlUserSecretClient = s.SecretClient + } + + // if the admin secret keyvault is not specified, fall back to global secretclient + if len(instance.Spec.AdminSecretKeyVault) != 0 { + adminSecretClient = keyvaultSecrets.New(instance.Spec.AdminSecretKeyVault) + if len(instance.Spec.AdminSecret) != 0 { + key = types.NamespacedName{Name: instance.Spec.AdminSecret} + } + } + + adminSecret, err := adminSecretClient.Get(ctx, key) + if err != nil { + // assuming if the admin secret is gone the sql server is too + return false, nil + } + + // short circuit connection if database doesn't exist + _, err = s.GetDB(ctx, instance.Spec.ResourceGroup, instance.Spec.Server, instance.Spec.DbName) + if err != nil { + instance.Status.Message = err.Error() + + catch := []string{ + errhelp.ResourceNotFound, + errhelp.ParentNotFoundErrorCode, + errhelp.ResourceGroupNotFoundErrorCode, + } + azerr := errhelp.NewAzureErrorAzureError(err) + if helpers.ContainsString(catch, azerr.Type) { + return false, nil + } + return false, err + } + + user := string(adminSecret["fullyQualifiedUsername"]) + password := string(adminSecret[PSecretPasswordKey]) + fullservername := string(adminSecret["fullyQualifiedServerName"]) + + db, err := s.ConnectToSqlDb(ctx, PDriverName, fullservername, instance.Spec.DbName, PSqlServerPort, user, password) + if err != nil { + instance.Status.Message = errhelp.StripErrorIDs(err) + if strings.Contains(err.Error(), "no pg_hba.conf entry for host") { + // The error indicates the client IP has no access to server. + instance.Status.Message = errhelp.StripErrorIDs(err) + "\nThe IP address is not allowed to access the server. Modify the firewall rule to include the IP address." + //Stop the reconcile and delete the user from service operator side. + return false, nil + } + //stop the reconcile with unkown error + return false, err + } + + requestedusername := instance.Spec.Username + + exists, err := s.UserExists(ctx, db, requestedusername) + if err != nil { + instance.Status.Message = fmt.Sprintf("Delete PostgreSqlUser failed with %s", err.Error()) + return true, err + } + if !exists { + + s.DeleteSecrets(ctx, instance, sqlUserSecretClient) + instance.Status.Message = fmt.Sprintf("The user %s doesn't exist", requestedusername) + //User doesn't exist. Stop the reconcile. + return false, nil + } + + err = s.DropUser(ctx, db, requestedusername) + if err != nil { + instance.Status.Message = fmt.Sprintf("Delete PostgreSqlUser failed with %s", err.Error()) + //stop the reconcile with err + return false, err + } + + // Once the user has been dropped, also delete their secrets. + s.DeleteSecrets(ctx, instance, sqlUserSecretClient) + + instance.Status.Message = fmt.Sprintf("Delete PostgreSqlUser succeeded") + + // no err, no requeue, reconcile will stop + return false, nil +} + +// GetParents gets the parents of the user +func (s *PostgreSqlUserManager) GetParents(obj runtime.Object) ([]resourcemanager.KubeParent, error) { + instance, err := s.convert(obj) + if err != nil { + return nil, err + } + + return []resourcemanager.KubeParent{ + { + Key: types.NamespacedName{ + Namespace: instance.Namespace, + Name: instance.Spec.DbName, + }, + Target: &v1alpha1.PostgreSQLDatabase{}, + }, + { + Key: types.NamespacedName{ + Namespace: instance.Namespace, + Name: instance.Spec.Server, + }, + Target: &v1alpha2.PostgreSQLServer{}, + }, + { + Key: types.NamespacedName{ + Namespace: instance.Namespace, + Name: instance.Spec.ResourceGroup, + }, + Target: &v1alpha1.ResourceGroup{}, + }, + }, nil +} + +// GetStatus gets the status +func (s *PostgreSqlUserManager) GetStatus(obj runtime.Object) (*v1alpha1.ASOStatus, error) { + instance, err := s.convert(obj) + if err != nil { + return nil, err + } + return &instance.Status, nil +} + +func (s *PostgreSqlUserManager) convert(obj runtime.Object) (*v1alpha1.PostgreSQLUser, error) { + local, ok := obj.(*v1alpha1.PostgreSQLUser) + if !ok { + return nil, fmt.Errorf("failed type assertion on kind: %s", obj.GetObjectKind().GroupVersionKind().String()) + } + return local, nil +} diff --git a/pkg/resourcemanager/rediscaches/redis/rediscache_reconcile.go b/pkg/resourcemanager/rediscaches/redis/rediscache_reconcile.go index 740170ac22b..8b1f72ba7f9 100644 --- a/pkg/resourcemanager/rediscaches/redis/rediscache_reconcile.go +++ b/pkg/resourcemanager/rediscaches/redis/rediscache_reconcile.go @@ -49,6 +49,11 @@ func (rc *AzureRedisCacheManager) Ensure(ctx context.Context, obj runtime.Object } instance.Status.Message = resourcemanager.SuccessMsg instance.Status.State = string(newRc.ProvisioningState) + + if newRc.StaticIP != nil { + instance.Status.Output = *newRc.StaticIP + } + instance.Status.ResourceId = *newRc.ID instance.Status.Provisioned = true instance.Status.Provisioning = false diff --git a/pkg/resourcemanager/rediscaches/redis/rediscaches.go b/pkg/resourcemanager/rediscaches/redis/rediscaches.go index a005e2e0057..2d3e8c008da 100644 --- a/pkg/resourcemanager/rediscaches/redis/rediscaches.go +++ b/pkg/resourcemanager/rediscaches/redis/rediscaches.go @@ -6,13 +6,13 @@ package rediscaches import ( "context" "errors" - "fmt" "log" "github.com/Azure/azure-sdk-for-go/services/redis/mgmt/2018-03-01/redis" azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" "github.com/Azure/azure-service-operator/pkg/helpers" "github.com/Azure/azure-service-operator/pkg/resourcemanager/rediscaches" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/vnet" "github.com/Azure/azure-service-operator/pkg/secrets" "github.com/Azure/go-autorest/autorest/to" "k8s.io/apimachinery/pkg/runtime" @@ -81,11 +81,23 @@ func (r *AzureRedisCacheManager) CreateRedisCache( // handle vnet settings if len(props.SubnetID) > 0 { + ip := props.StaticIP if len(props.StaticIP) == 0 { - return nil, fmt.Errorf("subnet id provided but no static ip has been set") + vnetManager := vnet.NewAzureVNetManager() + sid := vnet.ParseSubnetID(props.SubnetID) + + ip, err = vnetManager.GetAvailableIP(ctx, instance.Spec.ResourceGroupName, sid.VNet, sid.Subnet) + if err != nil { + return nil, err + } } + createParams.CreateProperties.SubnetID = &props.SubnetID - createParams.CreateProperties.StaticIP = &props.StaticIP + createParams.CreateProperties.StaticIP = &ip + } + + if redisSku.Name == redis.Premium && props.ShardCount != nil { + createParams.CreateProperties.ShardCount = props.ShardCount } // set redis config if one was provided diff --git a/pkg/resourcemanager/vnet/subnet.go b/pkg/resourcemanager/vnet/subnet.go new file mode 100644 index 00000000000..64a036c5d6f --- /dev/null +++ b/pkg/resourcemanager/vnet/subnet.go @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package vnet + +import ( + "context" + "strings" + + vnetwork "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-09-01/network" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/iam" +) + +// AzureSubnetManager is the struct that the manager functions hang off +type AzureSubnetManager struct{} + +//NewAzureSubnetManager returns a new client for subnets +func NewAzureSubnetManager() *AzureSubnetManager { + return &AzureSubnetManager{} +} + +// getSubnetClient returns a new instance of an subnet client +func getSubnetClient() (vnetwork.SubnetsClient, error) { + client := vnetwork.NewSubnetsClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) + a, err := iam.GetResourceManagementAuthorizer() + if err != nil { + client = vnetwork.SubnetsClient{} + } else { + client.Authorizer = a + client.AddToUserAgent(config.UserAgent()) + } + return client, err +} + +// Get gets a Subnet from Azure +func (v *AzureSubnetManager) Get(ctx context.Context, resourceGroup, vnet, subnet string) (vnetwork.Subnet, error) { + client, err := getSubnetClient() + if err != nil { + return vnetwork.Subnet{}, err + } + + return client.Get(ctx, resourceGroup, vnet, subnet, "") +} + +//SubnetID models the parts of a subnet resource id +type SubnetID struct { + Name string + VNet string + Subnet string + ResourceGroup string + Subscription string +} + +//ParseSubnetID takes a resource id for a subnet and parses it into its parts +func ParseSubnetID(sid string) SubnetID { + parts := strings.Split(sid, "/") + subid := SubnetID{} + + for i, v := range parts { + if i == 0 { + continue + } + switch parts[i-1] { + case "subscriptions": + subid.Subscription = v + case "resourceGroups": + subid.ResourceGroup = v + case "virtualNetworks": + subid.VNet = v + case "subnets": + subid.Subnet = v + } + } + return subid +} diff --git a/pkg/resourcemanager/vnet/vnet.go b/pkg/resourcemanager/vnet/vnet.go index 2116c240f27..eb5b8820988 100644 --- a/pkg/resourcemanager/vnet/vnet.go +++ b/pkg/resourcemanager/vnet/vnet.go @@ -5,12 +5,14 @@ package vnet import ( "context" + "fmt" + "net" vnetwork "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-09-01/network" azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" "github.com/Azure/azure-service-operator/pkg/resourcemanager/iam" - telemetry "github.com/Azure/azure-service-operator/pkg/telemetry" + "github.com/Azure/azure-service-operator/pkg/telemetry" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/to" ) @@ -111,3 +113,38 @@ func (v *AzureVNetManager) GetVNet(ctx context.Context, resourceGroupName string return client.Get(ctx, resourceGroupName, resourceName, "") } + +func (v *AzureVNetManager) GetAvailableIP(ctx context.Context, resourceGroup, vnet, subnet string) (string, error) { + client, err := getVNetClient() + if err != nil { + return "", err + } + + sclient := NewAzureSubnetManager() + + sub, err := sclient.Get(ctx, resourceGroup, vnet, subnet) + if err != nil { + return "", err + } + + if sub.SubnetPropertiesFormat == nil { + return "", fmt.Errorf("could not find subnet '%s'", subnet) + } + prefix := *sub.AddressPrefix + ip, _, err := net.ParseCIDR(prefix) + if err != nil { + return "", err + } + + result, err := client.CheckIPAddressAvailability(ctx, resourceGroup, vnet, ip.String()) + if err != nil { + return "", err + } + + if result.AvailableIPAddresses == nil || len(*result.AvailableIPAddresses) == 0 { + return "", fmt.Errorf("No available IP addresses in vnet %s", vnet) + } + + return (*result.AvailableIPAddresses)[0], nil + +} diff --git a/pkg/resourcemanager/vnet/vnet_manager.go b/pkg/resourcemanager/vnet/vnet_manager.go index 246d4986bfe..2c850e06e75 100644 --- a/pkg/resourcemanager/vnet/vnet_manager.go +++ b/pkg/resourcemanager/vnet/vnet_manager.go @@ -34,6 +34,8 @@ type VNetManager interface { resourceGroupName string, resourceName string) (vnetwork.VirtualNetwork, error) + GetAvailableIP(ctc context.Context, resourceGroup, vnet, subnet string) (string, error) + // also embed async client methods resourcemanager.ARMClient }