From b18ee6715b9c8ede385574ec600c3ae8ef01ee73 Mon Sep 17 00:00:00 2001 From: hobu <37413937+buhongw7583c@users.noreply.github.com> Date: Mon, 18 May 2020 11:59:01 +0800 Subject: [PATCH 01/24] add user support for pgresql --- api/v1alpha1/postgresqluser_types.go | 61 +++ .../cainjection_in_postgresqlusers.yaml | 8 + controllers/postgresqluser_controller.go | 44 ++ main.go | 21 + pkg/resourcemanager/psql/psqluser/psqluser.go | 279 +++++++++++ .../psql/psqluser/psqluser_manager.go | 29 ++ .../psql/psqluser/psqluser_reconcile.go | 435 ++++++++++++++++++ 7 files changed, 877 insertions(+) create mode 100644 api/v1alpha1/postgresqluser_types.go create mode 100644 config/crd/patches/cainjection_in_postgresqlusers.yaml create mode 100644 controllers/postgresqluser_controller.go create mode 100644 pkg/resourcemanager/psql/psqluser/psqluser.go create mode 100644 pkg/resourcemanager/psql/psqluser/psqluser_manager.go create mode 100644 pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go diff --git a/api/v1alpha1/postgresqluser_types.go b/api/v1alpha1/postgresqluser_types.go new file mode 100644 index 00000000000..993b6789364 --- /dev/null +++ b/api/v1alpha1/postgresqluser_types.go @@ -0,0 +1,61 @@ +// 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"` + KeyVaultSecretPrefix string `json:"keyVaultSecretPrefix,omitempty"` + KeyVaultSecretFormats []string `json:"keyVaultSecretFormats,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 sqluser is provisioning +func (s *PostgreSQLUser) IsSubmitted() bool { + return s.Status.Provisioning || s.Status.Provisioned +} diff --git a/config/crd/patches/cainjection_in_postgresqlusers.yaml b/config/crd/patches/cainjection_in_postgresqlusers.yaml new file mode 100644 index 00000000000..363d1ca1467 --- /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: + certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: psqlusers.azure.microsoft.com diff --git a/controllers/postgresqluser_controller.go b/controllers/postgresqluser_controller.go new file mode 100644 index 00000000000..4fbea859bee --- /dev/null +++ b/controllers/postgresqluser_controller.go @@ -0,0 +1,44 @@ +// 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" +) + +// PSqlServerPort is the default server port for psql server +const PSqlServerPort = 5432 + +// DriverName is driver name for db connection +const PDriverName = "psqlserver" + +// SecretUsernameKey is the username key in secret +const PSecretUsernameKey = "username" + +// SecretPasswordKey is the password key in secret +const PSecretPasswordKey = "password" + +// PSQLUserFinalizerName is the name of the finalizer +const PSQLUserFinalizerName = "psqluser.finalizers.azure.com" + +// PostgreSQLUserReconciler reconciles a PSQLUser object +type PostgreSQLUserReconciler struct { + Reconciler *AsyncReconciler +} + +// +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/main.go b/main.go index 1027341fc36..492f8f7b5ef 100644 --- a/main.go +++ b/main.go @@ -44,6 +44,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" resourcemanagerrediscache "github.com/Azure/azure-service-operator/pkg/resourcemanager/rediscaches" @@ -160,6 +161,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, @@ -514,6 +519,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("PSQLUser"), + ), + Recorder: mgr.GetEventRecorderFor("PSQLUser-controller"), + Scheme: scheme, + }, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "PSQLUser") + os.Exit(1) + } + if err = (&controllers.ApimServiceReconciler{ Reconciler: &controllers.AsyncReconciler{ Client: mgr.GetClient(), diff --git a/pkg/resourcemanager/psql/psqluser/psqluser.go b/pkg/resourcemanager/psql/psqluser/psqluser.go new file mode 100644 index 00000000000..e98986fa268 --- /dev/null +++ b/pkg/resourcemanager/psql/psqluser/psqluser.go @@ -0,0 +1,279 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package psqluser + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "strings" + + psql "github.com/Azure/azure-sdk-for-go/services/preview/sql/mgmt/2015-05-01-preview/sql" + "github.com/Azure/azure-service-operator/pkg/helpers" + azuresqlshared "github.com/Azure/azure-service-operator/pkg/resourcemanager/azuresql/azuresqlshared" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" + "github.com/Azure/azure-service-operator/pkg/secrets" + + "github.com/Azure/azure-service-operator/api/v1alpha1" + "k8s.io/apimachinery/pkg/runtime" + + _ "github.com/denisenkom/go-mssqldb" + "k8s.io/apimachinery/pkg/types" +) + +// PSqlServerPort is the default server port for sql server +const PSqlServerPort = 5432 + +// DriverName is driver name for db connection +const DriverName = "sqlserver" + +// SecretUsernameKey is the username key in secret +const SecretUsernameKey = "username" + +// SecretPasswordKey is the password key in secret +const SecretPasswordKey = "password" + +type PostgreSqlUserManager struct { + SecretClient secrets.SecretClient + Scheme *runtime.Scheme +} + +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) (psql.Database, error) { + dbClient, err := azuresqlshared.GetGoDbClient() + if err != nil { + return psql.Database{}, err + } + + return dbClient.Get( + ctx, + resourceGroupName, + serverName, + databaseName, + "serviceTierAdvisors, transparentDataEncryption", + ) +} + +// ConnectToSqlDb connects to the SQL db using the given credentials +func (s *PostgreSqlUserManager) ConnectToSqlDb(ctx context.Context, drivername string, server string, database string, port int, user string, password string) (*sql.DB, error) { + + fullServerAddress := fmt.Sprintf("%s."+config.Environment().SQLDatabaseDNSSuffix, server) + connString := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;database=%s;Persist Security Info=False;Pooling=False;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30", fullServerAddress, 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 + for _, role := range roles { + tsql := "sp_addrolemember @role, @user" + + _, err := db.ExecContext( + ctx, tsql, + sql.Named("role", role), + sql.Named("user", user), + ) + 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[SecretUsernameKey]) + newPassword := string(secret[SecretPasswordKey]) + + // make an effort to prevent sql injection + if err := findBadChars(newUser); err != nil { + return "", fmt.Errorf("Problem found with username: %v", err) + } + if err := 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) + + // TODO: Have db lib do string interpolation + //tsql := fmt.Sprintf(`CREATE USER @User WITH PASSWORD='@Password'`) + //_, err := db.ExecContext(ctx, tsql, sql.Named("User", newUser), sql.Named("Password", newPassword)) + + 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[SecretUsernameKey]) + newPassword := helpers.NewPassword() + + // make an effort to prevent sql injection + if err := findBadChars(user); err != nil { + return fmt.Errorf("Problem found with username: %v", err) + } + if err := 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 sysusers WHERE NAME=@user", + sql.Named("user", 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 { + tsql := "DROP USER @user" + _, err := db.ExecContext(ctx, tsql, sql.Named("user", user)) + 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) { + // determine our key namespace - if we're persisting to kube, we should use the actual instance namespace. + // In keyvault we have some creative freedom to allow more flexibility + 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 + } + + // delete all the custom formatted secrets if keyvault is in use + keyVaultEnabled := reflect.TypeOf(secretClient).Elem().Name() == "KeyvaultSecretClient" + if keyVaultEnabled { + customFormatNames := []string{ + "adonet", + "adonet-urlonly", + "jdbc", + "jdbc-urlonly", + "odbc", + "odbc-urlonly", + "server", + "database", + "username", + "password", + } + + for _, formatName := range customFormatNames { + key := types.NamespacedName{Namespace: secretKey.Namespace, Name: instance.Name + "-" + formatName} + + err = secretClient.Delete( + ctx, + key, + ) + 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) + 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 + "." + config.Environment().SQLDatabaseDNSSuffix), + "PSqlDatabaseName": []byte(instance.Spec.DbName), + } + } + return secret +} + +// GetNamespacedName gets the namespaced-name +func GetNamespacedName(instance *v1alpha1.PostgreSQLUser, secretClient secrets.SecretClient) types.NamespacedName { + var namespacedName types.NamespacedName + keyVaultEnabled := reflect.TypeOf(secretClient).Elem().Name() == "KeyvaultSecretClient" + + if keyVaultEnabled { + // For a keyvault secret store, check for supplied namespace parameters + var dbUserCustomNamespace string + if instance.Spec.KeyVaultSecretPrefix != "" { + dbUserCustomNamespace = instance.Spec.KeyVaultSecretPrefix + } else { + dbUserCustomNamespace = "psqluser-" + instance.Spec.Server + "-" + instance.Spec.DbName + } + + namespacedName = types.NamespacedName{Namespace: dbUserCustomNamespace, Name: instance.Name} + } else { + namespacedName = types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace} + } + + return namespacedName +} + +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/psqluser/psqluser_manager.go b/pkg/resourcemanager/psql/psqluser/psqluser_manager.go new file mode 100644 index 00000000000..fd61e5140a9 --- /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/preview/postgresql/mgmt/2017-12-01-preview/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..4f72c938623 --- /dev/null +++ b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go @@ -0,0 +1,435 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package psqluser + +import ( + "context" + "fmt" + "reflect" + "strings" + + "github.com/Azure/azure-service-operator/pkg/helpers" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" + "github.com/Azure/azure-service-operator/pkg/secrets" + + "github.com/Azure/azure-service-operator/api/v1alpha1" + "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/google/uuid" + "k8s.io/apimachinery/pkg/runtime" + + _ "github.com/denisenkom/go-mssqldb" + "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[SecretUsernameKey]) + adminPassword := string(adminSecret[SecretPasswordKey]) + + _, 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 - igmore and continue, otherwise report error and requeue + if !strings.Contains(errorString, "cannot unmarshal array into Go struct field serviceError2.details") { + return false, err + } + } + + db, err := s.ConnectToSqlDb(ctx, DriverName, instance.Spec.Server, 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(), "create a firewall rule for this 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[SecretUsernameKey]) + if user == "" { + user = fmt.Sprintf("%s-%s", requestedUsername, uuid.New()) + DBSecret[SecretUsernameKey] = []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 + } + + // Preformatted special formats are only available through keyvault as they require separated secrets + keyVaultEnabled := reflect.TypeOf(sqlUserSecretClient).Elem().Name() == "KeyvaultSecretClient" + if keyVaultEnabled { + // Instantiate a map of all formats and flip the bool to true for any that have been requested in the spec. + // Formats that were not requested will be explicitly deleted. + requestedFormats := map[string]bool{ + "adonet": false, + "adonet-urlonly": false, + "jdbc": false, + "jdbc-urlonly": false, + "odbc": false, + "odbc-urlonly": false, + "server": false, + "database": false, + "username": false, + "password": false, + } + for _, format := range instance.Spec.KeyVaultSecretFormats { + requestedFormats[format] = true + } + + // Deleted items will be processed immediately but secrets that need to be added will be created in this array and persisted in one pass at the end + formattedSecrets := make(map[string][]byte) + + for formatName, requested := range requestedFormats { + // Add the format to the output map if it has been requested otherwise call for its deletion from the secret store + if requested { + switch formatName { + case "adonet": + formattedSecrets["adonet"] = []byte(fmt.Sprintf( + "Server=tcp:%v,1433;Initial Catalog=%v;Persist Security Info=False;User ID=%v;Password=%v;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;", + string(DBSecret["fullyQualifiedServerName"]), + instance.Spec.DbName, + user, + string(DBSecret["password"]), + )) + + case "adonet-urlonly": + formattedSecrets["adonet-urlonly"] = []byte(fmt.Sprintf( + "Server=tcp:%v,1433;Initial Catalog=%v;Persist Security Info=False; MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout", + string(DBSecret["fullyQualifiedServerName"]), + instance.Spec.DbName, + )) + + case "jdbc": + formattedSecrets["jdbc"] = []byte(fmt.Sprintf( + "jdbc:sqlserver://%v:1433;database=%v;user=%v@%v;password=%v;encrypt=true;trustServerCertificate=false;hostNameInCertificate=*."+config.Environment().SQLDatabaseDNSSuffix+";loginTimeout=30;", + string(DBSecret["fullyQualifiedServerName"]), + instance.Spec.DbName, + user, + instance.Spec.Server, + string(DBSecret["password"]), + )) + case "jdbc-urlonly": + formattedSecrets["jdbc-urlonly"] = []byte(fmt.Sprintf( + "jdbc:sqlserver://%v:1433;database=%v;encrypt=true;trustServerCertificate=false;hostNameInCertificate=*."+config.Environment().SQLDatabaseDNSSuffix+";loginTimeout=30;", + string(DBSecret["fullyQualifiedServerName"]), + instance.Spec.DbName, + )) + + case "odbc": + formattedSecrets["odbc"] = []byte(fmt.Sprintf( + "Server=tcp:%v,1433;Initial Catalog=%v;Persist Security Info=False;User ID=%v;Password=%v;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;", + string(DBSecret["fullyQualifiedServerName"]), + instance.Spec.DbName, + user, + string(DBSecret["password"]), + )) + case "odbc-urlonly": + formattedSecrets["odbc-urlonly"] = []byte(fmt.Sprintf( + "Driver={ODBC Driver 13 for SQL Server};Server=tcp:%v,1433;Database=%v; Encrypt=yes;TrustServerCertificate=no;Connection Timeout=30;", + string(DBSecret["fullyQualifiedServerName"]), + instance.Spec.DbName, + )) + case "server": + formattedSecrets["server"] = DBSecret["fullyQualifiedServerName"] + + case "database": + formattedSecrets["database"] = []byte(instance.Spec.DbName) + + case "username": + formattedSecrets["username"] = []byte(user) + + case "password": + formattedSecrets["password"] = DBSecret["password"] + } + } else { + err = sqlUserSecretClient.Delete( + ctx, + types.NamespacedName{Namespace: key.Namespace, Name: instance.Name + "-" + formatName}, + ) + } + } + + err = sqlUserSecretClient.Upsert( + ctx, + types.NamespacedName{Namespace: key.Namespace, Name: instance.Name}, + formattedSecrets, + secrets.WithOwner(instance), + secrets.WithScheme(s.Scheme), + secrets.Flatten(true), + ) + if err != nil { + return false, err + } + } + + userExists, err := s.UserExists(ctx, db, string(DBSecret[SecretUsernameKey])) + 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 + + 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 + } + + var user = string(adminSecret[SecretUsernameKey]) + var password = string(adminSecret[SecretPasswordKey]) + + db, err := s.ConnectToSqlDb(ctx, DriverName, instance.Spec.Server, instance.Spec.DbName, PSqlServerPort, user, password) + if err != nil { + instance.Status.Message = errhelp.StripErrorIDs(err) + if strings.Contains(err.Error(), "create a firewall rule for this IP address") { + + // there is nothing much we can do here - cycle forever + return true, err + } + return false, err + } + + exists, err := s.UserExists(ctx, db, user) + if err != nil { + return true, err + } + if !exists { + s.DeleteSecrets(ctx, instance, sqlUserSecretClient) + return false, nil + } + + err = s.DropUser(ctx, db, user) + if err != nil { + instance.Status.Message = fmt.Sprintf("Delete AzureSqlUser failed with %s", err.Error()) + return false, err + } + + // Once the user has been dropped, also delete their secrets. + s.DeleteSecrets(ctx, instance, sqlUserSecretClient) + + instance.Status.Message = fmt.Sprintf("Delete AzureSqlUser succeeded") + + return true, 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: &v1alpha1.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 +} From 9995bf78357ac9923ae15ef6525399c394a49fbf Mon Sep 17 00:00:00 2001 From: hobu <37413937+buhongw7583c@users.noreply.github.com> Date: Wed, 20 May 2020 08:10:57 +0800 Subject: [PATCH 02/24] change the connection method --- controllers/postgresqluser_controller.go | 2 +- pkg/resourcemanager/psql/database/database.go | 9 +++-- pkg/resourcemanager/psql/psqluser/psqluser.go | 37 ++++++++++--------- .../psql/psqluser/psqluser_manager.go | 2 +- .../psql/psqluser/psqluser_reconcile.go | 20 +++++----- 5 files changed, 37 insertions(+), 33 deletions(-) diff --git a/controllers/postgresqluser_controller.go b/controllers/postgresqluser_controller.go index 4fbea859bee..1279a9e7886 100644 --- a/controllers/postgresqluser_controller.go +++ b/controllers/postgresqluser_controller.go @@ -13,7 +13,7 @@ import ( const PSqlServerPort = 5432 // DriverName is driver name for db connection -const PDriverName = "psqlserver" +const PDriverName = "postgres" // SecretUsernameKey is the username key in secret const PSecretUsernameKey = "username" 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/psqluser/psqluser.go b/pkg/resourcemanager/psql/psqluser/psqluser.go index e98986fa268..d554ba51977 100644 --- a/pkg/resourcemanager/psql/psqluser/psqluser.go +++ b/pkg/resourcemanager/psql/psqluser/psqluser.go @@ -10,36 +10,38 @@ import ( "reflect" "strings" - psql "github.com/Azure/azure-sdk-for-go/services/preview/sql/mgmt/2015-05-01-preview/sql" + psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" "github.com/Azure/azure-service-operator/pkg/helpers" - azuresqlshared "github.com/Azure/azure-service-operator/pkg/resourcemanager/azuresql/azuresqlshared" "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/denisenkom/go-mssqldb" + _ "github.com/lib/pq" //the pg lib "k8s.io/apimachinery/pkg/types" ) // PSqlServerPort is the default server port for sql server const PSqlServerPort = 5432 -// DriverName is driver name for db connection -const DriverName = "sqlserver" +// PDriverName is driver name for psqldb connection +const PDriverName = "postgres" -// SecretUsernameKey is the username key in secret -const SecretUsernameKey = "username" +// PSecretUsernameKey is the username key in secret +const PSecretUsernameKey = "username" -// SecretPasswordKey is the password key in secret -const SecretPasswordKey = "password" +// 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, @@ -48,8 +50,8 @@ func NewPostgreSqlUserManager(secretClient secrets.SecretClient, scheme *runtime } // GetDB retrieves a database -func (s *PostgreSqlUserManager) GetDB(ctx context.Context, resourceGroupName string, serverName string, databaseName string) (psql.Database, error) { - dbClient, err := azuresqlshared.GetGoDbClient() +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 } @@ -59,15 +61,16 @@ func (s *PostgreSqlUserManager) GetDB(ctx context.Context, resourceGroupName str resourceGroupName, serverName, databaseName, - "serviceTierAdvisors, transparentDataEncryption", ) } // ConnectToSqlDb connects to the SQL db using the given credentials func (s *PostgreSqlUserManager) ConnectToSqlDb(ctx context.Context, drivername string, server string, database string, port int, user string, password string) (*sql.DB, error) { - fullServerAddress := fmt.Sprintf("%s."+config.Environment().SQLDatabaseDNSSuffix, server) - connString := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;database=%s;Persist Security Info=False;Pooling=False;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30", fullServerAddress, user, password, port, database) + //fullServerAddress := fmt.Sprintf("%s."+config.Environment().SQLDatabaseDNSSuffix, server) + fullServerAddress := fmt.Sprintf("%s."+"postgres.database.azure.com", server) + + connString := fmt.Sprintf("host=%s user=%s password=%s port=%d dbname=%s sslmode=require connect_timeout=30", fullServerAddress, user, password, port, database) db, err := sql.Open(drivername, connString) if err != nil { @@ -106,8 +109,8 @@ func (s *PostgreSqlUserManager) GrantUserRoles(ctx context.Context, user string, // 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[SecretUsernameKey]) - newPassword := string(secret[SecretPasswordKey]) + newUser := string(secret[PSecretUsernameKey]) + newPassword := string(secret[PSecretPasswordKey]) // make an effort to prevent sql injection if err := findBadChars(newUser); err != nil { @@ -132,7 +135,7 @@ func (s *PostgreSqlUserManager) CreateUser(ctx context.Context, secret map[strin // UpdateUser - Updates user password func (s *PostgreSqlUserManager) UpdateUser(ctx context.Context, secret map[string][]byte, db *sql.DB) error { - user := string(secret[SecretUsernameKey]) + user := string(secret[PSecretUsernameKey]) newPassword := helpers.NewPassword() // make an effort to prevent sql injection diff --git a/pkg/resourcemanager/psql/psqluser/psqluser_manager.go b/pkg/resourcemanager/psql/psqluser/psqluser_manager.go index fd61e5140a9..c39d85743a6 100644 --- a/pkg/resourcemanager/psql/psqluser/psqluser_manager.go +++ b/pkg/resourcemanager/psql/psqluser/psqluser_manager.go @@ -7,7 +7,7 @@ import ( "context" "database/sql" - psql "github.com/Azure/azure-sdk-for-go/services/preview/postgresql/mgmt/2017-12-01-preview/postgresql" + 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" diff --git a/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go index 4f72c938623..8685deb6359 100644 --- a/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go +++ b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go @@ -20,7 +20,7 @@ import ( "github.com/google/uuid" "k8s.io/apimachinery/pkg/runtime" - _ "github.com/denisenkom/go-mssqldb" + _ "github.com/lib/pq" "k8s.io/apimachinery/pkg/types" ) @@ -73,8 +73,8 @@ func (s *PostgreSqlUserManager) Ensure(ctx context.Context, obj runtime.Object, return false, nil } - adminUser := string(adminSecret[SecretUsernameKey]) - adminPassword := string(adminSecret[SecretPasswordKey]) + adminUser := string(adminSecret[PSecretUsernameKey]) + adminPassword := string(adminSecret[PSecretPasswordKey]) _, err = s.GetDB(ctx, instance.Spec.ResourceGroup, instance.Spec.Server, instance.Spec.DbName) if err != nil { @@ -103,7 +103,7 @@ func (s *PostgreSqlUserManager) Ensure(ctx context.Context, obj runtime.Object, } } - db, err := s.ConnectToSqlDb(ctx, DriverName, instance.Spec.Server, instance.Spec.DbName, PSqlServerPort, adminUser, adminPassword) + db, err := s.ConnectToSqlDb(ctx, PDriverName, instance.Spec.Server, instance.Spec.DbName, PSqlServerPort, adminUser, adminPassword) if err != nil { instance.Status.Message = errhelp.StripErrorIDs(err) instance.Status.Provisioning = false @@ -129,10 +129,10 @@ func (s *PostgreSqlUserManager) Ensure(ctx context.Context, obj runtime.Object, // create or get new user secret DBSecret := s.GetOrPrepareSecret(ctx, instance, sqlUserSecretClient) // reset user from secret in case it was loaded - user := string(DBSecret[SecretUsernameKey]) + user := string(DBSecret[PSecretUsernameKey]) if user == "" { user = fmt.Sprintf("%s-%s", requestedUsername, uuid.New()) - DBSecret[SecretUsernameKey] = []byte(user) + DBSecret[PSecretUsernameKey] = []byte(user) } // Publishing the user secret: @@ -256,7 +256,7 @@ func (s *PostgreSqlUserManager) Ensure(ctx context.Context, obj runtime.Object, } } - userExists, err := s.UserExists(ctx, db, string(DBSecret[SecretUsernameKey])) + 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 @@ -348,10 +348,10 @@ func (s *PostgreSqlUserManager) Delete(ctx context.Context, obj runtime.Object, return false, err } - var user = string(adminSecret[SecretUsernameKey]) - var password = string(adminSecret[SecretPasswordKey]) + var user = string(adminSecret[PSecretUsernameKey]) + var password = string(adminSecret[PSecretPasswordKey]) - db, err := s.ConnectToSqlDb(ctx, DriverName, instance.Spec.Server, instance.Spec.DbName, PSqlServerPort, user, password) + db, err := s.ConnectToSqlDb(ctx, PDriverName, instance.Spec.Server, instance.Spec.DbName, PSqlServerPort, user, password) if err != nil { instance.Status.Message = errhelp.StripErrorIDs(err) if strings.Contains(err.Error(), "create a firewall rule for this IP address") { From 6594b5c8c7333109b567edc0131d94963a603b28 Mon Sep 17 00:00:00 2001 From: hobu <37413937+buhongw7583c@users.noreply.github.com> Date: Thu, 21 May 2020 12:03:27 +0800 Subject: [PATCH 03/24] add user for postgreSQL --- .../azure_v1alpha1_postgresqluser.yaml | 33 +++++++++++++++++++ pkg/resourcemanager/psql/psqluser/psqluser.go | 25 ++++++-------- .../psql/psqluser/psqluser_reconcile.go | 16 ++++----- 3 files changed, 51 insertions(+), 23 deletions(-) create mode 100644 config/samples/azure_v1alpha1_postgresqluser.yaml diff --git a/config/samples/azure_v1alpha1_postgresqluser.yaml b/config/samples/azure_v1alpha1_postgresqluser.yaml new file mode 100644 index 00000000000..8167993b40d --- /dev/null +++ b/config/samples/azure_v1alpha1_postgresqluser.yaml @@ -0,0 +1,33 @@ +apiVersion: azure.microsoft.com/v1alpha1 +kind: PostgreSQLUser +metadata: + name: psqluser-sample2 +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: hong + # Specify adminSecret and adminSecretKeyVault if you want to + # read the PSQL server admin creds from a specific keyvault secret + adminSecret: default-postgresqlserver-sample + adminSecretKeyVault: asokeyvault + + # Use the field below to optionally specify a different keyvault + # to store the secrets in + # keyVaultToStoreSecrets: asokeyvault + + # Below are optional fields that allow customizing the secrets you need + # keyVaultSecretPrefix: sqlServer-sqlDatabase + # valid secret formats + # adonet, adonet-urlonly, jdbc, jdbc-urlonly, odbc, odbc-urlonly, server, database, username, password + #keyVaultSecretFormats: + # - "adonet" + + diff --git a/pkg/resourcemanager/psql/psqluser/psqluser.go b/pkg/resourcemanager/psql/psqluser/psqluser.go index d554ba51977..3098673d3d1 100644 --- a/pkg/resourcemanager/psql/psqluser/psqluser.go +++ b/pkg/resourcemanager/psql/psqluser/psqluser.go @@ -89,13 +89,9 @@ func (s *PostgreSqlUserManager) ConnectToSqlDb(ctx context.Context, drivername s func (s *PostgreSqlUserManager) GrantUserRoles(ctx context.Context, user string, roles []string, db *sql.DB) error { var errorStrings []string for _, role := range roles { - tsql := "sp_addrolemember @role, @user" + tsql := fmt.Sprintf("GRANT %s TO %q", role, user) - _, err := db.ExecContext( - ctx, tsql, - sql.Named("role", role), - sql.Named("user", user), - ) + _, err := db.ExecContext(ctx, tsql) if err != nil { errorStrings = append(errorStrings, err.Error()) } @@ -120,7 +116,7 @@ func (s *PostgreSqlUserManager) CreateUser(ctx context.Context, secret map[strin return "", fmt.Errorf("Problem found with password: %v", err) } - tsql := fmt.Sprintf("CREATE USER \"%s\" WITH PASSWORD='%s'", newUser, newPassword) + tsql := fmt.Sprintf("CREATE USER \"%s\" WITH PASSWORD '%s'", newUser, newPassword) _, err := db.ExecContext(ctx, tsql) // TODO: Have db lib do string interpolation @@ -146,7 +142,7 @@ func (s *PostgreSqlUserManager) UpdateUser(ctx context.Context, secret map[strin return fmt.Errorf("Problem found with password: %v", err) } - tsql := fmt.Sprintf("ALTER USER \"%s\" WITH PASSWORD='%s'", user, newPassword) + tsql := fmt.Sprintf("ALTER USER '%s' WITH PASSWORD '%s'", user, newPassword) _, err := db.ExecContext(ctx, tsql) return err @@ -154,22 +150,21 @@ func (s *PostgreSqlUserManager) UpdateUser(ctx context.Context, secret map[strin // 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 sysusers WHERE NAME=@user", - sql.Named("user", username), - ) + + tsql := fmt.Sprintf("SELECT * FROM pg_user WHERE usename = '%s'", username) + res, err := db.ExecContext(ctx, tsql) 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 { - tsql := "DROP USER @user" - _, err := db.ExecContext(ctx, tsql, sql.Named("user", user)) + tsql := fmt.Sprintf("DROP USER '%s'", user) + _, err := db.ExecContext(ctx, tsql) return err } diff --git a/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go index 8685deb6359..650f43999e7 100644 --- a/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go +++ b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go @@ -17,10 +17,9 @@ import ( "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/google/uuid" - "k8s.io/apimachinery/pkg/runtime" - _ "github.com/lib/pq" + _ "github.com/lib/pq" //pglib + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ) @@ -73,7 +72,7 @@ func (s *PostgreSqlUserManager) Ensure(ctx context.Context, obj runtime.Object, return false, nil } - adminUser := string(adminSecret[PSecretUsernameKey]) + adminUser := string(adminSecret[PSecretUsernameKey]) + "@" + string(instance.Spec.Server) adminPassword := string(adminSecret[PSecretPasswordKey]) _, err = s.GetDB(ctx, instance.Spec.ResourceGroup, instance.Spec.Server, instance.Spec.DbName) @@ -131,7 +130,7 @@ func (s *PostgreSqlUserManager) Ensure(ctx context.Context, obj runtime.Object, // reset user from secret in case it was loaded user := string(DBSecret[PSecretUsernameKey]) if user == "" { - user = fmt.Sprintf("%s-%s", requestedUsername, uuid.New()) + user = fmt.Sprintf(requestedUsername) DBSecret[PSecretUsernameKey] = []byte(user) } @@ -305,6 +304,7 @@ func (s *PostgreSqlUserManager) Delete(ctx context.Context, obj runtime.Object, adminSecretClient := s.SecretClient adminsecretName := instance.Spec.AdminSecret + if len(instance.Spec.AdminSecret) == 0 { adminsecretName = instance.Spec.Server } @@ -348,7 +348,7 @@ func (s *PostgreSqlUserManager) Delete(ctx context.Context, obj runtime.Object, return false, err } - var user = string(adminSecret[PSecretUsernameKey]) + user := string(adminSecret[PSecretUsernameKey]) + "@" + string(instance.Spec.Server) var password = string(adminSecret[PSecretPasswordKey]) db, err := s.ConnectToSqlDb(ctx, PDriverName, instance.Spec.Server, instance.Spec.DbName, PSqlServerPort, user, password) @@ -373,14 +373,14 @@ func (s *PostgreSqlUserManager) Delete(ctx context.Context, obj runtime.Object, err = s.DropUser(ctx, db, user) if err != nil { - instance.Status.Message = fmt.Sprintf("Delete AzureSqlUser failed with %s", err.Error()) + instance.Status.Message = fmt.Sprintf("Delete PostgreSqlUser failed with %s", err.Error()) return false, err } // Once the user has been dropped, also delete their secrets. s.DeleteSecrets(ctx, instance, sqlUserSecretClient) - instance.Status.Message = fmt.Sprintf("Delete AzureSqlUser succeeded") + instance.Status.Message = fmt.Sprintf("Delete PostgreSqlUser succeeded") return true, nil } From c24735dc33fbe10fde84ab742fa8310fac27bd32 Mon Sep 17 00:00:00 2001 From: hobu <37413937+buhongw7583c@users.noreply.github.com> Date: Tue, 26 May 2020 15:09:20 +0800 Subject: [PATCH 04/24] change the DNS suffix to the correct one base on config environment --- pkg/resourcemanager/psql/psqluser/psqluser.go | 14 +++++++++++--- .../psql/psqluser/psqluser_reconcile.go | 10 ++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/pkg/resourcemanager/psql/psqluser/psqluser.go b/pkg/resourcemanager/psql/psqluser/psqluser.go index 3098673d3d1..0e5223a9c51 100644 --- a/pkg/resourcemanager/psql/psqluser/psqluser.go +++ b/pkg/resourcemanager/psql/psqluser/psqluser.go @@ -67,8 +67,16 @@ func (s *PostgreSqlUserManager) GetDB(ctx context.Context, resourceGroupName str // ConnectToSqlDb connects to the SQL db using the given credentials func (s *PostgreSqlUserManager) ConnectToSqlDb(ctx context.Context, drivername string, server string, database string, port int, user string, password string) (*sql.DB, error) { - //fullServerAddress := fmt.Sprintf("%s."+config.Environment().SQLDatabaseDNSSuffix, server) - fullServerAddress := fmt.Sprintf("%s."+"postgres.database.azure.com", server) + psqldbdnssuffix := "database.azure.com" + if config.Environment().Name != "AzurePublicCloud" { + psqldbdnssuffix = config.Environment().SQLDatabaseDNSSuffix + } + //the host or fullserveraddress should be: + //for public cloud .mysql.database.azure.com + //for China cloud .mysql.database.chinacloudapi.cn + //for German cloud .mysql.database.cloudapi.de + //for US government .mysql.database.usgovcloudapi.net + fullServerAddress := fmt.Sprintf("%s.mysql."+psqldbdnssuffix, server) connString := fmt.Sprintf("host=%s user=%s password=%s port=%d dbname=%s sslmode=require connect_timeout=30", fullServerAddress, user, password, port, database) @@ -163,7 +171,7 @@ func (s *PostgreSqlUserManager) UserExists(ctx context.Context, db *sql.DB, user // DropUser drops a user from db func (s *PostgreSqlUserManager) DropUser(ctx context.Context, db *sql.DB, user string) error { - tsql := fmt.Sprintf("DROP USER '%s'", user) + tsql := fmt.Sprintf("DROP USER %s", user) _, err := db.ExecContext(ctx, tsql) return err } diff --git a/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go index 650f43999e7..0889d120099 100644 --- a/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go +++ b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go @@ -349,7 +349,7 @@ func (s *PostgreSqlUserManager) Delete(ctx context.Context, obj runtime.Object, } user := string(adminSecret[PSecretUsernameKey]) + "@" + string(instance.Spec.Server) - var password = string(adminSecret[PSecretPasswordKey]) + password := string(adminSecret[PSecretPasswordKey]) db, err := s.ConnectToSqlDb(ctx, PDriverName, instance.Spec.Server, instance.Spec.DbName, PSqlServerPort, user, password) if err != nil { @@ -362,7 +362,9 @@ func (s *PostgreSqlUserManager) Delete(ctx context.Context, obj runtime.Object, return false, err } - exists, err := s.UserExists(ctx, db, user) + requestedusername := instance.Spec.Username + + exists, err := s.UserExists(ctx, db, requestedusername) if err != nil { return true, err } @@ -371,7 +373,7 @@ func (s *PostgreSqlUserManager) Delete(ctx context.Context, obj runtime.Object, return false, nil } - err = s.DropUser(ctx, db, user) + err = s.DropUser(ctx, db, requestedusername) if err != nil { instance.Status.Message = fmt.Sprintf("Delete PostgreSqlUser failed with %s", err.Error()) return false, err @@ -382,7 +384,7 @@ func (s *PostgreSqlUserManager) Delete(ctx context.Context, obj runtime.Object, instance.Status.Message = fmt.Sprintf("Delete PostgreSqlUser succeeded") - return true, nil + return false, nil } // GetParents gets the parents of the user From 838465b13c390da5897fa3d7566ccf6b3350bcd6 Mon Sep 17 00:00:00 2001 From: hobu <37413937+buhongw7583c@users.noreply.github.com> Date: Tue, 26 May 2020 17:44:57 +0800 Subject: [PATCH 05/24] correct the fullserveraddress when connect to mysql server --- pkg/resourcemanager/psql/psqluser/psqluser.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/resourcemanager/psql/psqluser/psqluser.go b/pkg/resourcemanager/psql/psqluser/psqluser.go index 0e5223a9c51..ba85bcf45ac 100644 --- a/pkg/resourcemanager/psql/psqluser/psqluser.go +++ b/pkg/resourcemanager/psql/psqluser/psqluser.go @@ -64,7 +64,7 @@ func (s *PostgreSqlUserManager) GetDB(ctx context.Context, resourceGroupName str ) } -// ConnectToSqlDb connects to the SQL db using the given credentials +// ConnectToSqlDb connects to the PostgreSQL db using the given credentials func (s *PostgreSqlUserManager) ConnectToSqlDb(ctx context.Context, drivername string, server string, database string, port int, user string, password string) (*sql.DB, error) { psqldbdnssuffix := "database.azure.com" @@ -72,11 +72,11 @@ func (s *PostgreSqlUserManager) ConnectToSqlDb(ctx context.Context, drivername s psqldbdnssuffix = config.Environment().SQLDatabaseDNSSuffix } //the host or fullserveraddress should be: - //for public cloud .mysql.database.azure.com - //for China cloud .mysql.database.chinacloudapi.cn - //for German cloud .mysql.database.cloudapi.de - //for US government .mysql.database.usgovcloudapi.net - fullServerAddress := fmt.Sprintf("%s.mysql."+psqldbdnssuffix, server) + //for public cloud .postgres.database.azure.com + //for China cloud .postgres.database.chinacloudapi.cn + //for German cloud .postgres.database.cloudapi.de + //for US government .postgres.database.usgovcloudapi.net + fullServerAddress := fmt.Sprintf("%s.postgres."+psqldbdnssuffix, server) connString := fmt.Sprintf("host=%s user=%s password=%s port=%d dbname=%s sslmode=require connect_timeout=30", fullServerAddress, user, password, port, database) @@ -171,7 +171,7 @@ func (s *PostgreSqlUserManager) UserExists(ctx context.Context, db *sql.DB, user // DropUser drops a user from db func (s *PostgreSqlUserManager) DropUser(ctx context.Context, db *sql.DB, user string) error { - tsql := fmt.Sprintf("DROP USER %s", user) + tsql := fmt.Sprintf("DROP USER IF EXISTS %q", user) _, err := db.ExecContext(ctx, tsql) return err } From 4ed44885b225927dc99f32c8be5f9b73516c1a97 Mon Sep 17 00:00:00 2001 From: hobu <37413937+buhongw7583c@users.noreply.github.com> Date: Thu, 28 May 2020 20:35:09 +0800 Subject: [PATCH 06/24] Update per comments: modify the crd name to align with the convention; add controller test for postgresqluser; add handling for firewall access erro. --- PROJECT | 3 + api/v1alpha1/postgresqluser_types.go | 2 +- charts/azure-service-operator-0.1.0.tgz | Bin 26010 -> 25075 bytes charts/index.yaml | 6 +- config/crd/kustomization.yaml | 1 + .../patches/webhook_in_postgresqlusers.yaml | 17 +++ .../azure_v1alpha1_postgresqluser.yaml | 8 +- .../postgresql_combined_controller_test.go | 49 ++++++--- controllers/postgresqluser_controller.go | 14 +-- controllers/postgresqluser_controller_test.go | 102 ++++++++++++++++++ controllers/suite_test.go | 17 +++ docs/howto/newoperatorguide.md | 2 +- main.go | 6 +- .../psql/psqluser/psqluser_reconcile.go | 14 +-- 14 files changed, 202 insertions(+), 39 deletions(-) create mode 100644 config/crd/patches/webhook_in_postgresqlusers.yaml create mode 100644 controllers/postgresqluser_controller_test.go diff --git a/PROJECT b/PROJECT index 9dd2285a807..b866b613997 100644 --- a/PROJECT +++ b/PROJECT @@ -52,6 +52,9 @@ resources: - group: azure kind: PostgreSQLFirewallRule version: v1alpha1 +- group: azure + kind: PostgreSQLUser + version: v1alpha1 - group: azure kind: APIMgmtAPI version: v1alpha1 diff --git a/api/v1alpha1/postgresqluser_types.go b/api/v1alpha1/postgresqluser_types.go index 993b6789364..3c664d3c931 100644 --- a/api/v1alpha1/postgresqluser_types.go +++ b/api/v1alpha1/postgresqluser_types.go @@ -55,7 +55,7 @@ func init() { SchemeBuilder.Register(&PostgreSQLUser{}, &PostgreSQLUserList{}) } -// IsSubmitted checks if sqluser is provisioning +// IsSubmitted checks if psqluser is provisioning func (s *PostgreSQLUser) IsSubmitted() bool { return s.Status.Provisioning || s.Status.Provisioned } diff --git a/charts/azure-service-operator-0.1.0.tgz b/charts/azure-service-operator-0.1.0.tgz index 1d4cf361965a5b4a52e1a9687c92332dcf0b10f7..e18ddaba4528396b75c808c5af1c731409e8f518 100644 GIT binary patch literal 25075 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 Q0ssL2|7-k29srI60QyOnFaQ7m literal 26010 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 Date: Thu, 28 May 2020 21:25:31 +0800 Subject: [PATCH 07/24] modify the TestPostgreSQLUserControllerNoResourceGroup in postgresqluser_controller_test.go --- controllers/postgresqluser_controller_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/postgresqluser_controller_test.go b/controllers/postgresqluser_controller_test.go index 78b55fbacad..a169efa10b3 100644 --- a/controllers/postgresqluser_controller_test.go +++ b/controllers/postgresqluser_controller_test.go @@ -49,7 +49,7 @@ func TestPostgreSQLUserControllerNoAdminSecret(t *testing.T) { EnsureDelete(ctx, t, tc, postgresqlUser) } -func TestAzureSQLUserControllerNoResourceGroup(t *testing.T) { +func TestPostgreSQLUserControllerNoResourceGroup(t *testing.T) { t.Parallel() defer PanicRecover(t) ctx := context.Background() From 475eba2f28c14fd565cb2dbe82943d6e8bdb8060 Mon Sep 17 00:00:00 2001 From: hobu <37413937+buhongw7583c@users.noreply.github.com> Date: Fri, 29 May 2020 00:11:05 +0800 Subject: [PATCH 08/24] postgresql_combined_test change to make it work --- .../postgresql_combined_controller_test.go | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/controllers/postgresql_combined_controller_test.go b/controllers/postgresql_combined_controller_test.go index 66d8f31ed83..acb8feed8b4 100644 --- a/controllers/postgresql_combined_controller_test.go +++ b/controllers/postgresql_combined_controller_test.go @@ -11,7 +11,6 @@ import ( azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" "github.com/Azure/azure-service-operator/api/v1alpha2" - "github.com/Azure/azure-service-operator/pkg/helpers" "github.com/Azure/go-autorest/autorest/to" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -96,30 +95,6 @@ func TestPSQLDatabaseController(t *testing.T) { EnsureInstance(ctx, t, tc, postgreSQLDatabaseInstance) - var postgresqlUser *azurev1alpha1.PostgreSQLUser - - // create a psql user and verify it provisions - pusername := "psql-test-user" + helpers.RandomString(10) - roles := []string{"azure_pg_admin"} - keyVaultSecretFormats := []string{"adonet"} - - postgresqlUser = &azurev1alpha1.PostgreSQLUser{ - ObjectMeta: metav1.ObjectMeta{ - Name: pusername, - Namespace: "default", - }, - Spec: azurev1alpha1.PostgreSQLUserSpec{ - Server: postgreSQLServerName, - DbName: postgreSQLDatabaseName, - ResourceGroup: rgName, - Roles: roles, - KeyVaultSecretFormats: keyVaultSecretFormats, - }, - } - - EnsureInstance(ctx, t, tc, postgresqlUser) - EnsureDelete(ctx, t, tc, postgresqlUser) - EnsureDelete(ctx, t, tc, postgreSQLDatabaseInstance) EnsureDelete(ctx, t, tc, postgreSQLFirewallRuleInstance) From b10042408aeffaa5323f813f2cb6ace8559e9e58 Mon Sep 17 00:00:00 2001 From: hobu <37413937+buhongw7583c@users.noreply.github.com> Date: Fri, 29 May 2020 11:56:44 +0800 Subject: [PATCH 09/24] Handle the "no pg_hba.conf entry" in a graceful way. Add comments to make code more clear. --- .../psql/psqluser/psqluser_reconcile.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go index 54db5527e90..623aab7892b 100644 --- a/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go +++ b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go @@ -110,7 +110,7 @@ func (s *PostgreSqlUserManager) Ensure(ctx context.Context, obj runtime.Object, // 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 true, nil + return false, nil } // if the database is busy, requeue @@ -285,8 +285,8 @@ func (s *PostgreSqlUserManager) Ensure(ctx context.Context, obj runtime.Object, instance.Status.Provisioned = true instance.Status.State = "Succeeded" instance.Status.Message = resourcemanager.SuccessMsg - - return false, nil + // reconcile done + return true, nil } // Delete deletes a user @@ -356,9 +356,12 @@ func (s *PostgreSqlUserManager) Delete(ctx context.Context, obj runtime.Object, 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." + //Requeue the requests (return true) till the issue solves. return true, nil } + //stop the reconcile with unkown error return false, err } @@ -366,16 +369,21 @@ func (s *PostgreSqlUserManager) Delete(ctx context.Context, obj runtime.Object, 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 } @@ -384,6 +392,7 @@ func (s *PostgreSqlUserManager) Delete(ctx context.Context, obj runtime.Object, instance.Status.Message = fmt.Sprintf("Delete PostgreSqlUser succeeded") + // no err, no requeue, reconcile will stop return false, nil } From b1f8456caa3e191510c4b3aa25df4ca2b2b1f4bf Mon Sep 17 00:00:00 2001 From: hobu <37413937+buhongw7583c@users.noreply.github.com> Date: Fri, 29 May 2020 14:00:05 +0800 Subject: [PATCH 10/24] add the postgresqluser RBAC files and modify the sample yaml file for postgresqluser. --- config/rbac/postgresqluser_editor_role.yaml | 27 +++++++++++++++++++ config/rbac/postgresqluser_viewer_role.yaml | 20 ++++++++++++++ .../azure_v1alpha1_postgresqluser.yaml | 4 +-- 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 config/rbac/postgresqluser_editor_role.yaml create mode 100644 config/rbac/postgresqluser_viewer_role.yaml 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 index c8df6b424d3..1864c827241 100644 --- a/config/samples/azure_v1alpha1_postgresqluser.yaml +++ b/config/samples/azure_v1alpha1_postgresqluser.yaml @@ -13,10 +13,10 @@ spec: roles: - "azure_pg_admin" # Specify a specific username for the user - username: psqluser-sample + # 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 + # adminSecret: postgresqlserver-sample # adminSecretKeyVault: asokeyvault # Use the field below to optionally specify a different keyvault From fa0966e9328c76ace5730b6f0413a0041221b654 Mon Sep 17 00:00:00 2001 From: Erin Corson Date: Thu, 28 May 2020 17:28:29 -0600 Subject: [PATCH 11/24] add a client for subnet --- pkg/resourcemanager/vnet/subnet.go | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 pkg/resourcemanager/vnet/subnet.go 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 +} From e73d71832384c7a76829fb52d8234d60d82cff49 Mon Sep 17 00:00:00 2001 From: Erin Corson Date: Thu, 28 May 2020 18:29:36 -0600 Subject: [PATCH 12/24] allow auto ip assigning, set chosen ip in status.ouput, allow setting shard count --- api/v1alpha1/aso_types.go | 1 + api/v1alpha1/rediscache_types.go | 1 + api/v1alpha1/zz_generated.deepcopy.go | 5 +++ api/v1alpha2/aso_types.go | 1 + api/v1beta1/aso_types.go | 1 + config/samples/azure_v1alpha1_rediscache.yaml | 7 ++-- .../rediscaches/redis/rediscache_reconcile.go | 1 + .../rediscaches/redis/rediscaches.go | 18 ++++++++-- pkg/resourcemanager/vnet/vnet.go | 36 ++++++++++++++++++- pkg/resourcemanager/vnet/vnet_manager.go | 2 ++ 10 files changed, 67 insertions(+), 6 deletions(-) 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/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 cbda21e97d5..aae05e86f69 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -3117,6 +3117,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/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/pkg/resourcemanager/rediscaches/redis/rediscache_reconcile.go b/pkg/resourcemanager/rediscaches/redis/rediscache_reconcile.go index 72a5128cfdf..b9dfb611388 100644 --- a/pkg/resourcemanager/rediscaches/redis/rediscache_reconcile.go +++ b/pkg/resourcemanager/rediscaches/redis/rediscache_reconcile.go @@ -54,6 +54,7 @@ func (rc *AzureRedisCacheManager) Ensure(ctx context.Context, obj runtime.Object } instance.Status.Message = resourcemanager.SuccessMsg instance.Status.State = string(newRc.ProvisioningState) + 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 e7fd22bb484..86a2c99a133 100644 --- a/pkg/resourcemanager/rediscaches/redis/rediscaches.go +++ b/pkg/resourcemanager/rediscaches/redis/rediscaches.go @@ -6,7 +6,6 @@ package rediscaches import ( "context" "errors" - "fmt" "log" "github.com/Azure/azure-sdk-for-go/services/redis/mgmt/2018-03-01/redis" @@ -15,6 +14,7 @@ import ( "github.com/Azure/azure-service-operator/pkg/helpers" "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" "github.com/Azure/azure-service-operator/pkg/resourcemanager/iam" + "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" @@ -95,11 +95,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/vnet.go b/pkg/resourcemanager/vnet/vnet.go index 2116c240f27..176bf16ec08 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,35 @@ 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 + } + + 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 } From 17ccf63d13043816a2dbe1f508aa774128d37c5c Mon Sep 17 00:00:00 2001 From: Erin Corson Date: Thu, 28 May 2020 20:58:26 -0600 Subject: [PATCH 13/24] catch panic when subnet id doesn't have a subnet name --- pkg/resourcemanager/vnet/vnet.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/resourcemanager/vnet/vnet.go b/pkg/resourcemanager/vnet/vnet.go index 176bf16ec08..eb5b8820988 100644 --- a/pkg/resourcemanager/vnet/vnet.go +++ b/pkg/resourcemanager/vnet/vnet.go @@ -127,6 +127,9 @@ func (v *AzureVNetManager) GetAvailableIP(ctx context.Context, resourceGroup, vn 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 { From 5e4f10a008440216dab2d237170bcd111348704a Mon Sep 17 00:00:00 2001 From: hobu <37413937+buhongw7583c@users.noreply.github.com> Date: Mon, 1 Jun 2020 11:25:45 +0800 Subject: [PATCH 14/24] delete the postgresql user when the IP address no access to server anyway --- pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go index 623aab7892b..1d92c63f8c8 100644 --- a/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go +++ b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go @@ -358,8 +358,8 @@ func (s *PostgreSqlUserManager) Delete(ctx context.Context, obj runtime.Object, 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." - //Requeue the requests (return true) till the issue solves. - return true, nil + //Stop the reconcile and delete the user from service operator side. + return false, nil } //stop the reconcile with unkown error return false, err From 09c5edcacfe9b3515837e8661e1b893af4df7918 Mon Sep 17 00:00:00 2001 From: Erin Corson Date: Mon, 1 Jun 2020 11:41:58 -0600 Subject: [PATCH 15/24] these imports are no longer needed after changes that happened in master --- pkg/resourcemanager/rediscaches/redis/rediscaches.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/resourcemanager/rediscaches/redis/rediscaches.go b/pkg/resourcemanager/rediscaches/redis/rediscaches.go index 8f917066914..2d3e8c008da 100644 --- a/pkg/resourcemanager/rediscaches/redis/rediscaches.go +++ b/pkg/resourcemanager/rediscaches/redis/rediscaches.go @@ -11,10 +11,8 @@ import ( "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/config" - "github.com/Azure/azure-service-operator/pkg/resourcemanager/iam" - "github.com/Azure/azure-service-operator/pkg/resourcemanager/vnet" "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" From ecb716f6faf6ec2ccd83d2082909245c94ce002c Mon Sep 17 00:00:00 2001 From: Erin Corson Date: Mon, 1 Jun 2020 16:40:01 -0600 Subject: [PATCH 16/24] Update PROJECT this is superfluous --- PROJECT | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PROJECT b/PROJECT index 816c765c6e5..16250e6e372 100644 --- a/PROJECT +++ b/PROJECT @@ -57,7 +57,6 @@ resources: kind: PostgreSQLUser version: v1alpha1 - group: azure - kind: APIMgmtAPI version: v1alpha1 kind: APIMgmtAPI - group: azure @@ -134,4 +133,4 @@ resources: version: v1alpha1 - group: azure kind: AzureVirtualMachineExtension - version: v1alpha1 \ No newline at end of file + version: v1alpha1 From ec4fc91eebe6fd20568d909eaddae6c05c076a47 Mon Sep 17 00:00:00 2001 From: jananivMS Date: Mon, 1 Jun 2020 16:54:44 -0600 Subject: [PATCH 17/24] Update cert manager annotations --- config/crd/patches/cainjection_in_apimgmtapis.yaml | 2 +- config/crd/patches/cainjection_in_azureloadbalancers.yaml | 2 +- config/crd/patches/cainjection_in_azurenetworkinterfaces.yaml | 2 +- config/crd/patches/cainjection_in_azurepublicipaddresses.yaml | 2 +- config/crd/patches/cainjection_in_azuresqlusers.yaml | 2 +- config/crd/patches/cainjection_in_azuresqlvnetrules.yaml | 2 +- .../patches/cainjection_in_azurevirtualmachineextensions.yaml | 2 +- config/crd/patches/cainjection_in_azurevirtualmachines.yaml | 2 +- config/crd/patches/cainjection_in_azurevmscalesets.yaml | 2 +- config/crd/patches/cainjection_in_cosmosdbs.yaml | 2 +- config/crd/patches/cainjection_in_keyvaultkeys.yaml | 2 +- config/crd/patches/cainjection_in_mysqldatabases.yaml | 2 +- config/crd/patches/cainjection_in_mysqlfirewallrules.yaml | 2 +- config/crd/patches/cainjection_in_mysqlvnetrules.yaml | 2 +- config/crd/patches/cainjection_in_postgresqlvnetrules.yaml | 2 +- config/crd/patches/cainjection_in_rediscacheactions.yaml | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) 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_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_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_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 From 525dfe825bfcac99966085bd988ad8090b3fca9c Mon Sep 17 00:00:00 2001 From: Erin Corson Date: Mon, 1 Jun 2020 18:18:13 -0600 Subject: [PATCH 18/24] avoid nil pointer dereferencing --- .../rediscaches/redis/rediscache_reconcile.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/resourcemanager/rediscaches/redis/rediscache_reconcile.go b/pkg/resourcemanager/rediscaches/redis/rediscache_reconcile.go index f6c79bdbb69..8b1f72ba7f9 100644 --- a/pkg/resourcemanager/rediscaches/redis/rediscache_reconcile.go +++ b/pkg/resourcemanager/rediscaches/redis/rediscache_reconcile.go @@ -49,7 +49,11 @@ func (rc *AzureRedisCacheManager) Ensure(ctx context.Context, obj runtime.Object } instance.Status.Message = resourcemanager.SuccessMsg instance.Status.State = string(newRc.ProvisioningState) - instance.Status.Output = *newRc.StaticIP + + if newRc.StaticIP != nil { + instance.Status.Output = *newRc.StaticIP + } + instance.Status.ResourceId = *newRc.ID instance.Status.Provisioned = true instance.Status.Provisioning = false From 5a53399356d3c2acf08a6e6f45c1c7bb3fa8fb31 Mon Sep 17 00:00:00 2001 From: hobu <37413937+buhongw7583c@users.noreply.github.com> Date: Tue, 2 Jun 2020 17:54:19 +0800 Subject: [PATCH 19/24] modify based on comments: update cainjection, use the secret to retrieve username and full server name, move the common function to helpers, change to v2 version for postgresqlserver in controllers. --- PROJECT | 1 - .../cainjection_in_postgresqlusers.yaml | 2 +- controllers/postgresqluser_controller.go | 15 ----- pkg/helpers/stringhelper.go | 18 ++++++ pkg/resourcemanager/psql/psqluser/psqluser.go | 59 +++++-------------- .../psql/psqluser/psqluser_reconcile.go | 22 +++---- 6 files changed, 46 insertions(+), 71 deletions(-) diff --git a/PROJECT b/PROJECT index 816c765c6e5..f42d8d5b3a8 100644 --- a/PROJECT +++ b/PROJECT @@ -59,7 +59,6 @@ resources: - group: azure kind: APIMgmtAPI version: v1alpha1 - kind: APIMgmtAPI - group: azure version: v1alpha1 kind: ApimService diff --git a/config/crd/patches/cainjection_in_postgresqlusers.yaml b/config/crd/patches/cainjection_in_postgresqlusers.yaml index 363d1ca1467..d2c81e8115c 100644 --- a/config/crd/patches/cainjection_in_postgresqlusers.yaml +++ b/config/crd/patches/cainjection_in_postgresqlusers.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: psqlusers.azure.microsoft.com diff --git a/controllers/postgresqluser_controller.go b/controllers/postgresqluser_controller.go index 3c99e61f2e3..9da8f196564 100644 --- a/controllers/postgresqluser_controller.go +++ b/controllers/postgresqluser_controller.go @@ -9,21 +9,6 @@ import ( azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" ) -// PSqlServerPort is the default server port for psql server -const PSqlServerPort = 5432 - -// PDriverName is driver name for db connection -const PDriverName = "postgres" - -// PSecretUsernameKey is the username key in secret -const PSecretUsernameKey = "username" - -// PSecretPasswordKey is the password key in secret -const PSecretPasswordKey = "password" - -// PSQLUserFinalizerName is the name of the finalizer -const PSQLUserFinalizerName = "postgresqluser.finalizers.azure.com" - // PostgreSQLUserReconciler reconciles a PSQLUser object type PostgreSQLUserReconciler struct { Reconciler *AsyncReconciler 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/psqluser/psqluser.go b/pkg/resourcemanager/psql/psqluser/psqluser.go index ba85bcf45ac..0f3e860dc6d 100644 --- a/pkg/resourcemanager/psql/psqluser/psqluser.go +++ b/pkg/resourcemanager/psql/psqluser/psqluser.go @@ -65,20 +65,9 @@ func (s *PostgreSqlUserManager) GetDB(ctx context.Context, resourceGroupName str } // ConnectToSqlDb connects to the PostgreSQL db using the given credentials -func (s *PostgreSqlUserManager) ConnectToSqlDb(ctx context.Context, drivername string, server string, database string, port int, user string, password string) (*sql.DB, error) { +func (s *PostgreSqlUserManager) ConnectToSqlDb(ctx context.Context, drivername string, fullservername string, database string, port int, user string, password string) (*sql.DB, error) { - psqldbdnssuffix := "database.azure.com" - if config.Environment().Name != "AzurePublicCloud" { - psqldbdnssuffix = config.Environment().SQLDatabaseDNSSuffix - } - //the host or fullserveraddress should be: - //for public cloud .postgres.database.azure.com - //for China cloud .postgres.database.chinacloudapi.cn - //for German cloud .postgres.database.cloudapi.de - //for US government .postgres.database.usgovcloudapi.net - fullServerAddress := fmt.Sprintf("%s.postgres."+psqldbdnssuffix, server) - - connString := fmt.Sprintf("host=%s user=%s password=%s port=%d dbname=%s sslmode=require connect_timeout=30", fullServerAddress, user, password, port, database) + 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 { @@ -117,20 +106,16 @@ func (s *PostgreSqlUserManager) CreateUser(ctx context.Context, secret map[strin newPassword := string(secret[PSecretPasswordKey]) // make an effort to prevent sql injection - if err := findBadChars(newUser); err != nil { + if err := helpers.FindBadChars(newUser); err != nil { return "", fmt.Errorf("Problem found with username: %v", err) } - if err := findBadChars(newPassword); err != nil { + 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) - // TODO: Have db lib do string interpolation - //tsql := fmt.Sprintf(`CREATE USER @User WITH PASSWORD='@Password'`) - //_, err := db.ExecContext(ctx, tsql, sql.Named("User", newUser), sql.Named("Password", newPassword)) - if err != nil { return newUser, err } @@ -143,10 +128,10 @@ func (s *PostgreSqlUserManager) UpdateUser(ctx context.Context, secret map[strin newPassword := helpers.NewPassword() // make an effort to prevent sql injection - if err := findBadChars(user); err != nil { + if err := helpers.FindBadChars(user); err != nil { return fmt.Errorf("Problem found with username: %v", err) } - if err := findBadChars(newPassword); err != nil { + if err := helpers.FindBadChars(newPassword); err != nil { return fmt.Errorf("Problem found with password: %v", err) } @@ -159,8 +144,7 @@ func (s *PostgreSqlUserManager) UpdateUser(ctx context.Context, secret map[strin // UserExists checks if db contains user func (s *PostgreSqlUserManager) UserExists(ctx context.Context, db *sql.DB, username string) (bool, error) { - tsql := fmt.Sprintf("SELECT * FROM pg_user WHERE usename = '%s'", username) - res, err := db.ExecContext(ctx, tsql) + res, err := db.ExecContext(ctx, "SELECT * FROM pg_user WHERE usename = $1", username) if err != nil { return false, err } @@ -178,8 +162,7 @@ func (s *PostgreSqlUserManager) DropUser(ctx context.Context, db *sql.DB, user s // DeleteSecrets deletes the secrets associated with a SQLUser func (s *PostgreSqlUserManager) DeleteSecrets(ctx context.Context, instance *v1alpha1.PostgreSQLUser, secretClient secrets.SecretClient) (bool, error) { - // determine our key namespace - if we're persisting to kube, we should use the actual instance namespace. - // In keyvault we have some creative freedom to allow more flexibility + secretKey := GetNamespacedName(instance, secretClient) // delete standard user secret @@ -191,7 +174,6 @@ func (s *PostgreSqlUserManager) DeleteSecrets(ctx context.Context, instance *v1a instance.Status.Message = "failed to delete secret, err: " + err.Error() return false, err } - // delete all the custom formatted secrets if keyvault is in use keyVaultEnabled := reflect.TypeOf(secretClient).Elem().Name() == "KeyvaultSecretClient" if keyVaultEnabled { @@ -230,6 +212,12 @@ func (s *PostgreSqlUserManager) GetOrPrepareSecret(ctx context.Context, instance 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() @@ -238,7 +226,7 @@ func (s *PostgreSqlUserManager) GetOrPrepareSecret(ctx context.Context, instance "password": []byte(pw), "PSqlServerNamespace": []byte(instance.Namespace), "PSqlServerName": []byte(instance.Spec.Server), - "fullyQualifiedServerName": []byte(instance.Spec.Server + "." + config.Environment().SQLDatabaseDNSSuffix), + "fullyQualifiedServerName": []byte(instance.Spec.Server + "." + psqldbdnssuffix), "PSqlDatabaseName": []byte(instance.Spec.DbName), } } @@ -266,20 +254,3 @@ func GetNamespacedName(instance *v1alpha1.PostgreSQLUser, secretClient secrets.S return namespacedName } - -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/psqluser/psqluser_reconcile.go b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go index 1d92c63f8c8..432ae51f56f 100644 --- a/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go +++ b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go @@ -5,6 +5,7 @@ package psqluser import ( "context" + "encoding/json" "fmt" "reflect" "strings" @@ -14,6 +15,7 @@ import ( "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" @@ -72,7 +74,7 @@ func (s *PostgreSqlUserManager) Ensure(ctx context.Context, obj runtime.Object, return false, nil } - adminUser := string(adminSecret[PSecretUsernameKey]) + "@" + string(instance.Spec.Server) + adminUser := string(adminSecret["fullyQualifiedUsername"]) adminPassword := string(adminSecret[PSecretPasswordKey]) _, err = s.GetDB(ctx, instance.Spec.ResourceGroup, instance.Spec.Server, instance.Spec.DbName) @@ -96,13 +98,13 @@ func (s *PostgreSqlUserManager) Ensure(ctx context.Context, obj runtime.Object, return false, nil } - // if this is an unmarshall error - igmore and continue, otherwise report error and requeue - if !strings.Contains(errorString, "cannot unmarshal array into Go struct field serviceError2.details") { - return false, err + // if this is an unmarshall error - ignore and continue, otherwise report error and requeue + if _, ok := err.(*json.UnmarshalTypeError); ok { + return false, nil } } - - db, err := s.ConnectToSqlDb(ctx, PDriverName, instance.Spec.Server, instance.Spec.DbName, PSqlServerPort, adminUser, adminPassword) + 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 @@ -148,7 +150,6 @@ func (s *PostgreSqlUserManager) Ensure(ctx context.Context, obj runtime.Object, instance.Status.Message = "failed to update secret, err: " + err.Error() return false, err } - // Preformatted special formats are only available through keyvault as they require separated secrets keyVaultEnabled := reflect.TypeOf(sqlUserSecretClient).Elem().Name() == "KeyvaultSecretClient" if keyVaultEnabled { @@ -349,10 +350,11 @@ func (s *PostgreSqlUserManager) Delete(ctx context.Context, obj runtime.Object, return false, err } - user := string(adminSecret[PSecretUsernameKey]) + "@" + string(instance.Spec.Server) + user := string(adminSecret["fullyQualifiedUsername"]) password := string(adminSecret[PSecretPasswordKey]) + fullservername := string(adminSecret["fullyQualifiedServerName"]) - db, err := s.ConnectToSqlDb(ctx, PDriverName, instance.Spec.Server, instance.Spec.DbName, PSqlServerPort, user, password) + 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") { @@ -416,7 +418,7 @@ func (s *PostgreSqlUserManager) GetParents(obj runtime.Object) ([]resourcemanage Namespace: instance.Namespace, Name: instance.Spec.Server, }, - Target: &v1alpha1.PostgreSQLServer{}, + Target: &v1alpha2.PostgreSQLServer{}, }, { Key: types.NamespacedName{ From 634b8746ac4c0734132ecf26127a4aeccd8e1a7b Mon Sep 17 00:00:00 2001 From: Erin Corson Date: Mon, 1 Jun 2020 20:54:18 -0600 Subject: [PATCH 20/24] setting psql db parent to correct version --- go.mod | 3 ++- go.sum | 27 +++++++++++++++++++ .../psql/database/database_reconcile.go | 3 ++- 3 files changed, 31 insertions(+), 2 deletions(-) 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/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{ From 3791abd7dd636e49d783d59b4599d8af7d5468fb Mon Sep 17 00:00:00 2001 From: Erin Corson Date: Tue, 2 Jun 2020 13:25:41 -0600 Subject: [PATCH 21/24] updates to go.mod and sum, also deepcopy gen update --- api/v1alpha1/zz_generated.deepcopy.go | 84 +++++++++++++++++++++++++++ go.mod | 3 +- go.sum | 27 +++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index a84cebabf62..cc3a204359a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -2890,6 +2890,90 @@ 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) + } + if in.KeyVaultSecretFormats != nil { + in, out := &in.KeyVaultSecretFormats, &out.KeyVaultSecretFormats + *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 diff --git a/go.mod b/go.mod index fd5dd58a338..dd342db2c6b 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 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= From 9a8d6bc5c2ae0bc1378bf78fb05dd6e254516daa Mon Sep 17 00:00:00 2001 From: Erin Corson Date: Tue, 2 Jun 2020 15:18:29 -0600 Subject: [PATCH 22/24] remove conn string formats, harden against sql injection --- pkg/resourcemanager/psql/psqluser/psqluser.go | 63 +++------- .../psql/psqluser/psqluser_reconcile.go | 108 +----------------- 2 files changed, 16 insertions(+), 155 deletions(-) diff --git a/pkg/resourcemanager/psql/psqluser/psqluser.go b/pkg/resourcemanager/psql/psqluser/psqluser.go index 0f3e860dc6d..8a173f0efa7 100644 --- a/pkg/resourcemanager/psql/psqluser/psqluser.go +++ b/pkg/resourcemanager/psql/psqluser/psqluser.go @@ -7,7 +7,6 @@ import ( "context" "database/sql" "fmt" - "reflect" "strings" psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" @@ -85,9 +84,18 @@ func (s *PostgreSqlUserManager) ConnectToSqlDb(ctx context.Context, drivername s // 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()) @@ -155,6 +163,10 @@ func (s *PostgreSqlUserManager) UserExists(ctx context.Context, db *sql.DB, user // 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 @@ -174,35 +186,6 @@ func (s *PostgreSqlUserManager) DeleteSecrets(ctx context.Context, instance *v1a instance.Status.Message = "failed to delete secret, err: " + err.Error() return false, err } - // delete all the custom formatted secrets if keyvault is in use - keyVaultEnabled := reflect.TypeOf(secretClient).Elem().Name() == "KeyvaultSecretClient" - if keyVaultEnabled { - customFormatNames := []string{ - "adonet", - "adonet-urlonly", - "jdbc", - "jdbc-urlonly", - "odbc", - "odbc-urlonly", - "server", - "database", - "username", - "password", - } - - for _, formatName := range customFormatNames { - key := types.NamespacedName{Namespace: secretKey.Namespace, Name: instance.Name + "-" + formatName} - - err = secretClient.Delete( - ctx, - key, - ) - if err != nil { - instance.Status.Message = "failed to delete secret, err: " + err.Error() - return false, err - } - } - } return false, nil } @@ -230,27 +213,11 @@ func (s *PostgreSqlUserManager) GetOrPrepareSecret(ctx context.Context, instance "PSqlDatabaseName": []byte(instance.Spec.DbName), } } + return secret } // GetNamespacedName gets the namespaced-name func GetNamespacedName(instance *v1alpha1.PostgreSQLUser, secretClient secrets.SecretClient) types.NamespacedName { - var namespacedName types.NamespacedName - keyVaultEnabled := reflect.TypeOf(secretClient).Elem().Name() == "KeyvaultSecretClient" - - if keyVaultEnabled { - // For a keyvault secret store, check for supplied namespace parameters - var dbUserCustomNamespace string - if instance.Spec.KeyVaultSecretPrefix != "" { - dbUserCustomNamespace = instance.Spec.KeyVaultSecretPrefix - } else { - dbUserCustomNamespace = "psqluser-" + instance.Spec.Server + "-" + instance.Spec.DbName - } - - namespacedName = types.NamespacedName{Namespace: dbUserCustomNamespace, Name: instance.Name} - } else { - namespacedName = types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace} - } - - return namespacedName + return types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace} } diff --git a/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go index 432ae51f56f..897a662a895 100644 --- a/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go +++ b/pkg/resourcemanager/psql/psqluser/psqluser_reconcile.go @@ -11,7 +11,6 @@ import ( "strings" "github.com/Azure/azure-service-operator/pkg/helpers" - "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" "github.com/Azure/azure-service-operator/pkg/secrets" "github.com/Azure/azure-service-operator/api/v1alpha1" @@ -103,6 +102,7 @@ func (s *PostgreSqlUserManager) Ensure(ctx context.Context, obj runtime.Object, return false, nil } } + fullServerName := string(adminSecret["fullyQualifiedServerName"]) db, err := s.ConnectToSqlDb(ctx, PDriverName, fullServerName, instance.Spec.DbName, PSqlServerPort, adminUser, adminPassword) if err != nil { @@ -150,112 +150,6 @@ func (s *PostgreSqlUserManager) Ensure(ctx context.Context, obj runtime.Object, instance.Status.Message = "failed to update secret, err: " + err.Error() return false, err } - // Preformatted special formats are only available through keyvault as they require separated secrets - keyVaultEnabled := reflect.TypeOf(sqlUserSecretClient).Elem().Name() == "KeyvaultSecretClient" - if keyVaultEnabled { - // Instantiate a map of all formats and flip the bool to true for any that have been requested in the spec. - // Formats that were not requested will be explicitly deleted. - requestedFormats := map[string]bool{ - "adonet": false, - "adonet-urlonly": false, - "jdbc": false, - "jdbc-urlonly": false, - "odbc": false, - "odbc-urlonly": false, - "server": false, - "database": false, - "username": false, - "password": false, - } - for _, format := range instance.Spec.KeyVaultSecretFormats { - requestedFormats[format] = true - } - - // Deleted items will be processed immediately but secrets that need to be added will be created in this array and persisted in one pass at the end - formattedSecrets := make(map[string][]byte) - - for formatName, requested := range requestedFormats { - // Add the format to the output map if it has been requested otherwise call for its deletion from the secret store - if requested { - switch formatName { - case "adonet": - formattedSecrets["adonet"] = []byte(fmt.Sprintf( - "Server=tcp:%v,1433;Initial Catalog=%v;Persist Security Info=False;User ID=%v;Password=%v;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;", - string(DBSecret["fullyQualifiedServerName"]), - instance.Spec.DbName, - user, - string(DBSecret["password"]), - )) - - case "adonet-urlonly": - formattedSecrets["adonet-urlonly"] = []byte(fmt.Sprintf( - "Server=tcp:%v,1433;Initial Catalog=%v;Persist Security Info=False; MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout", - string(DBSecret["fullyQualifiedServerName"]), - instance.Spec.DbName, - )) - - case "jdbc": - formattedSecrets["jdbc"] = []byte(fmt.Sprintf( - "jdbc:sqlserver://%v:1433;database=%v;user=%v@%v;password=%v;encrypt=true;trustServerCertificate=false;hostNameInCertificate=*."+config.Environment().SQLDatabaseDNSSuffix+";loginTimeout=30;", - string(DBSecret["fullyQualifiedServerName"]), - instance.Spec.DbName, - user, - instance.Spec.Server, - string(DBSecret["password"]), - )) - case "jdbc-urlonly": - formattedSecrets["jdbc-urlonly"] = []byte(fmt.Sprintf( - "jdbc:sqlserver://%v:1433;database=%v;encrypt=true;trustServerCertificate=false;hostNameInCertificate=*."+config.Environment().SQLDatabaseDNSSuffix+";loginTimeout=30;", - string(DBSecret["fullyQualifiedServerName"]), - instance.Spec.DbName, - )) - - case "odbc": - formattedSecrets["odbc"] = []byte(fmt.Sprintf( - "Server=tcp:%v,1433;Initial Catalog=%v;Persist Security Info=False;User ID=%v;Password=%v;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;", - string(DBSecret["fullyQualifiedServerName"]), - instance.Spec.DbName, - user, - string(DBSecret["password"]), - )) - case "odbc-urlonly": - formattedSecrets["odbc-urlonly"] = []byte(fmt.Sprintf( - "Driver={ODBC Driver 13 for SQL Server};Server=tcp:%v,1433;Database=%v; Encrypt=yes;TrustServerCertificate=no;Connection Timeout=30;", - string(DBSecret["fullyQualifiedServerName"]), - instance.Spec.DbName, - )) - case "server": - formattedSecrets["server"] = DBSecret["fullyQualifiedServerName"] - - case "database": - formattedSecrets["database"] = []byte(instance.Spec.DbName) - - case "username": - formattedSecrets["username"] = []byte(user) - - case "password": - formattedSecrets["password"] = DBSecret["password"] - } - } else { - err = sqlUserSecretClient.Delete( - ctx, - types.NamespacedName{Namespace: key.Namespace, Name: instance.Name + "-" + formatName}, - ) - } - } - - err = sqlUserSecretClient.Upsert( - ctx, - types.NamespacedName{Namespace: key.Namespace, Name: instance.Name}, - formattedSecrets, - secrets.WithOwner(instance), - secrets.WithScheme(s.Scheme), - secrets.Flatten(true), - ) - if err != nil { - return false, err - } - } userExists, err := s.UserExists(ctx, db, string(DBSecret[PSecretUsernameKey])) if err != nil { From 4ea7ae7d51fe29623a24fbc7f3c19d1affd65873 Mon Sep 17 00:00:00 2001 From: Erin Corson Date: Tue, 2 Jun 2020 15:48:19 -0600 Subject: [PATCH 23/24] remove unused fields from user spec --- api/v1alpha1/postgresqluser_types.go | 10 ++++------ api/v1alpha1/zz_generated.deepcopy.go | 5 ----- config/samples/azure_v1alpha1_postgresqluser.yaml | 8 +------- 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/api/v1alpha1/postgresqluser_types.go b/api/v1alpha1/postgresqluser_types.go index 3c664d3c931..aaadba84927 100644 --- a/api/v1alpha1/postgresqluser_types.go +++ b/api/v1alpha1/postgresqluser_types.go @@ -19,12 +19,10 @@ type PostgreSQLUserSpec struct { 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"` - KeyVaultSecretPrefix string `json:"keyVaultSecretPrefix,omitempty"` - KeyVaultSecretFormats []string `json:"keyVaultSecretFormats,omitempty"` + AdminSecret string `json:"adminSecret,omitempty"` + AdminSecretKeyVault string `json:"adminSecretKeyVault,omitempty"` + Username string `json:"username,omitempty"` + KeyVaultToStoreSecrets string `json:"keyVaultToStoreSecrets,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index cc3a204359a..d84e7ae3aca 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -2957,11 +2957,6 @@ func (in *PostgreSQLUserSpec) DeepCopyInto(out *PostgreSQLUserSpec) { *out = make([]string, len(*in)) copy(*out, *in) } - if in.KeyVaultSecretFormats != nil { - in, out := &in.KeyVaultSecretFormats, &out.KeyVaultSecretFormats - *out = make([]string, len(*in)) - copy(*out, *in) - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLUserSpec. diff --git a/config/samples/azure_v1alpha1_postgresqluser.yaml b/config/samples/azure_v1alpha1_postgresqluser.yaml index 1864c827241..925151ecd47 100644 --- a/config/samples/azure_v1alpha1_postgresqluser.yaml +++ b/config/samples/azure_v1alpha1_postgresqluser.yaml @@ -22,12 +22,6 @@ spec: # Use the field below to optionally specify a different keyvault # to store the secrets in # keyVaultToStoreSecrets: asokeyvault - - # Below are optional fields that allow customizing the secrets you need - # keyVaultSecretPrefix: sqlServer-sqlDatabase - # valid secret formats - # adonet, adonet-urlonly, jdbc, jdbc-urlonly, odbc, odbc-urlonly, server, database, username, password - #keyVaultSecretFormats: - # - "adonet" + From f2a86ec4bec463246bfe583da70e2dc808e5c7c7 Mon Sep 17 00:00:00 2001 From: jananivMS Date: Wed, 3 Jun 2020 11:45:24 -0600 Subject: [PATCH 24/24] missed couple files --- config/crd/patches/cainjection_in_apimservices.yaml | 2 +- config/crd/patches/cainjection_in_azuresqlmanagedusers.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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_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