Skip to content

Commit

Permalink
Mysql replica support
Browse files Browse the repository at this point in the history
  • Loading branch information
jananivMS committed Apr 14, 2020
1 parent d874813 commit 25735b1
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 80 deletions.
18 changes: 8 additions & 10 deletions api/v1alpha1/mysqlserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@ import (

// MySQLServerSpec defines the desired state of MySQLServer
type MySQLServerSpec struct {
Location string `json:"location"`
ResourceGroup string `json:"resourceGroup,omitempty"`
Sku AzureDBsSQLSku `json:"sku,omitempty"`
ServerVersion ServerVersion `json:"serverVersion,omitempty"`
SSLEnforcement SslEnforcementEnum `json:"sslEnforcement,omitempty"`
MinimalTLSVersion string `json:"minimalTLSVersion,omitempty"`
InfrastructureEncryption string `json:"infrastructureEncryption,omitempty"`
CreateMode string `json:"createMode,omitempty"`
ReplicaProperties ReplicaProperties `json:"replicaProperties, omitempty"`
KeyVaultToStoreSecrets string `json:"keyVaultToStoreSecrets,omitempty"`
Location string `json:"location"`
ResourceGroup string `json:"resourceGroup,omitempty"`
Sku AzureDBsSQLSku `json:"sku,omitempty"`
ServerVersion ServerVersion `json:"serverVersion,omitempty"`
SSLEnforcement SslEnforcementEnum `json:"sslEnforcement,omitempty"`
CreateMode string `json:"createMode,omitempty"`
ReplicaProperties ReplicaProperties `json:"replicaProperties,omitempty"`
KeyVaultToStoreSecrets string `json:"keyVaultToStoreSecrets,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
6 changes: 3 additions & 3 deletions config/samples/azure_v1alpha1_mysqlserver.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ spec:
infrastructureEncryption: Enabled # Possible values include: Enabled, Disabled
createMode: Default # Possible values include: Default, Replica, PointInTimeRestore (not implemented), GeoRestore (not implemented)
sku:
name: B_Gen5_2
tier: Basic
family: Gen5
name: B_Gen5_2 # tier + family + cores eg. - B_Gen4_1, GP_Gen5_4
tier: Basic # possible values - 'Basic', 'GeneralPurpose', 'MemoryOptimized'
family: Gen5
size: "51200"
capacity: 2

2 changes: 2 additions & 0 deletions config/samples/azure_v1alpha1_mysqlserver_replica.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ metadata:
name: mysqlserver-replica
spec:
location: eastus2
resourceGroup: resourcegroup-azure-operators
createMode: Replica # Possible values include: Default, Replica, PointInTimeRestore (not implemented), GeoRestore (not implemented)
replicaProperties:
# sourceServer tier should be "GeneralPurpose" or higher for replica support
sourceServerId: /subscriptions/{SUBID}/resourceGroups/resourcegroup-azure-operators/providers/Microsoft.DBforMySQL/servers/mysqlserver-sample

1 change: 1 addition & 0 deletions pkg/errhelp/errhelp.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func StripErrorIDs(err error) string {
patterns := []string{
"RequestID=",
"CorrelationId:\\s",
"Tracking ID: ",
}
reg := regexp.MustCompile(fmt.Sprintf(`(%s)\S+`, strings.Join(patterns, "|")))
return reg.ReplaceAllString(err.Error(), "")
Expand Down
2 changes: 2 additions & 0 deletions pkg/errhelp/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ const (
ServiceBusy = "ServiceBusy"
NameNotAvailable = "NameNotAvailable"
PublicIPIdleTimeoutIsOutOfRange = "PublicIPIdleTimeoutIsOutOfRange"
InvalidRequestContent = "InvalidRequestContent"
InternalServerError = "InternalServerError"
)

func NewAzureError(err error) error {
Expand Down
65 changes: 44 additions & 21 deletions pkg/resourcemanager/mysql/server/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ package server

import (
"context"
"strings"

mysql "github.com/Azure/azure-sdk-for-go/services/mysql/mgmt/2017-12-01/mysql"
"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/secrets"
"github.com/Azure/go-autorest/autorest/to"
"k8s.io/apimachinery/pkg/runtime"
)

Expand Down Expand Up @@ -59,34 +61,55 @@ func (m *MySQLServerClient) CheckServerNameAvailability(ctx context.Context, ser

}

func (m *MySQLServerClient) CreateServerIfValid(ctx context.Context, servername string, resourcegroup string, location string, tags map[string]*string, serverversion mysql.ServerVersion, sslenforcement mysql.SslEnforcementEnum, skuInfo mysql.Sku, adminlogin string, adminpassword string) (future mysql.ServersCreateFuture, err error) {
func (m *MySQLServerClient) CreateServerIfValid(ctx context.Context, servername string, resourcegroup string, location string, tags map[string]*string, serverversion mysql.ServerVersion, sslenforcement mysql.SslEnforcementEnum, skuInfo mysql.Sku, adminlogin string, adminpassword string, createmode string, sourceserver string) (server mysql.Server, err error) {

client := getMySQLServersClient()

// Check if name is valid if this is the first create call
valid, err := m.CheckServerNameAvailability(ctx, servername)
if valid == false {
return future, err
if !valid {
return server, err
}

return client.Create(
ctx,
resourcegroup,
servername,
mysql.ServerForCreate{
Location: &location,
Tags: tags,
Properties: &mysql.ServerPropertiesForDefaultCreate{
AdministratorLogin: &adminlogin,
AdministratorLoginPassword: &adminpassword,
Version: serverversion,
SslEnforcement: sslenforcement,
//StorageProfile: &mysql.StorageProfile{},
CreateMode: mysql.CreateModeServerPropertiesForCreate,
var result mysql.ServersCreateFuture
if strings.EqualFold(createmode, "replica") {
result, _ = client.Create(
ctx,
resourcegroup,
servername,
mysql.ServerForCreate{
Location: &location,
Tags: tags,
Properties: &mysql.ServerPropertiesForReplica{
SourceServerID: to.StringPtr(sourceserver),
CreateMode: mysql.CreateModeReplica,
},
},
)

} else {

result, _ = client.Create(
ctx,
resourcegroup,
servername,
mysql.ServerForCreate{
Location: &location,
Tags: tags,
Properties: &mysql.ServerPropertiesForDefaultCreate{
AdministratorLogin: &adminlogin,
AdministratorLoginPassword: &adminpassword,
Version: serverversion,
SslEnforcement: sslenforcement,
//StorageProfile: &mysql.StorageProfile{},
CreateMode: mysql.CreateModeServerPropertiesForCreate,
},
Sku: &skuInfo,
},
Sku: &skuInfo,
},
)
)
}

return result.Result(client)

}

func (m *MySQLServerClient) DeleteServer(ctx context.Context, resourcegroup string, servername string) (status string, err error) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/resourcemanager/mysql/server/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

type MySQLServerManager interface {
CreateServerIfValid(ctx context.Context, servername string, resourcegroup string, location string, tags map[string]*string, serverversion mysql.ServerVersion, sslenforcement mysql.SslEnforcementEnum, skuInfo mysql.Sku, adminlogin string, adminpassword string) (mysql.ServersCreateFuture, error)
CreateServerIfValid(ctx context.Context, servername string, resourcegroup string, location string, tags map[string]*string, serverversion mysql.ServerVersion, sslenforcement mysql.SslEnforcementEnum, skuInfo mysql.Sku, adminlogin string, adminpassword string, createmode string, sourceserver string) (mysql.Server, error)
DeleteServer(ctx context.Context, resourcegroup string, servername string) (string, error)
GetServer(ctx context.Context, resourcegroup string, servername string) (mysql.Server, error)

Expand Down
101 changes: 56 additions & 45 deletions pkg/resourcemanager/mysql/server/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (m *MySQLServerClient) Ensure(ctx context.Context, obj runtime.Object, opts
createmode = instance.Spec.CreateMode
}

// If a replica is requested, return error if source server is not specified
// If a replica is requested, ensure that source server is specified
if strings.EqualFold(createmode, "replica") {
if len(instance.Spec.ReplicaProperties.SourceServerId) == 0 {
instance.Status.Message = "Replica requested but source server unspecified"
Expand All @@ -55,17 +55,8 @@ func (m *MySQLServerClient) Ensure(ctx context.Context, obj runtime.Object, opts
return false, err
}

// Update secret
err = m.AddServerCredsToSecrets(ctx, instance.Name, secret, instance)
if err != nil {
return false, err
}

// convert kube labels to expected tag format
labels := map[string]*string{}
for k, v := range instance.GetLabels() {
labels[k] = &v
}
labels := helpers.LabelsToTags(instance.GetLabels())

// Check if this server already exists and its state if it does. This is required
// to overcome the issue with the lack of idempotence of the Create call
Expand All @@ -77,6 +68,14 @@ func (m *MySQLServerClient) Ensure(ctx context.Context, obj runtime.Object, opts
instance.Status.Provisioning = false
instance.Status.Message = resourcemanager.SuccessMsg
instance.Status.ResourceId = *server.ID
instance.Status.State = string(server.UserVisibleState)

// Update secret - we do this on success as we need the FQ name of the server
err = m.AddServerCredsToSecrets(ctx, instance.Name, secret, instance, *server.FullyQualifiedDomainName)
if err != nil {
return false, err
}

return true, nil
}
return false, nil
Expand All @@ -98,7 +97,7 @@ func (m *MySQLServerClient) Ensure(ctx context.Context, obj runtime.Object, opts
Family: to.StringPtr(instance.Spec.Sku.Family),
}

_, err = m.CreateServerIfValid(
server, err = m.CreateServerIfValid(
ctx,
instance.Name,
instance.Spec.ResourceGroup,
Expand All @@ -109,23 +108,29 @@ func (m *MySQLServerClient) Ensure(ctx context.Context, obj runtime.Object, opts
skuInfo,
adminlogin,
adminpassword,
createmode,
instance.Spec.ReplicaProperties.SourceServerId,
)
if err != nil {
// let the user know what happened
instance.Status.Message = err.Error()
instance.Status.Message = errhelp.StripErrorIDs(err)
instance.Status.Provisioning = false
azerr := errhelp.NewAzureErrorAzureError(err)

switch azerr.Type {
case errhelp.ResourceGroupNotFoundErrorCode, errhelp.ParentNotFoundErrorCode:
// errors we expect might happen that we are ok with waiting for
return false, nil
case errhelp.ProvisioningDisabled, errhelp.LocationNotAvailableForResourceType:
case errhelp.ProvisioningDisabled, errhelp.LocationNotAvailableForResourceType, errhelp.InvalidRequestContent, errhelp.InternalServerError:
// Unrecoverable error, so stop reconcilation
instance.Status.Message = "Reconcilation hit unrecoverable error: " + err.Error()
instance.Status.Message = "Reconcilation hit unrecoverable error: " + errhelp.StripErrorIDs(err)
return true, nil
case errhelp.AsyncOpIncompleteError:
// Creation in progress
instance.Status.Provisioning = true
instance.Status.Message = "Server request submitted to Azure"
return false, nil
}

// reconciliation not done and we don't know what happened
return false, err
}
Expand Down Expand Up @@ -211,12 +216,15 @@ func (m *MySQLServerClient) convert(obj runtime.Object) (*v1alpha1.MySQLServer,
}

// AddServerCredsToSecrets saves the server's admin credentials in the secret store
func (m *MySQLServerClient) AddServerCredsToSecrets(ctx context.Context, secretName string, data map[string][]byte, instance *azurev1alpha1.MySQLServer) error {
func (m *MySQLServerClient) AddServerCredsToSecrets(ctx context.Context, secretName string, data map[string][]byte, instance *azurev1alpha1.MySQLServer, fullservername string) error {
key := types.NamespacedName{
Name: secretName,
Namespace: instance.Namespace,
}

// Update fullyQualifiedServerName from the created server
data["fullyQualifiedServerName"] = []byte(fullservername)

err := m.SecretClient.Upsert(ctx,
key,
data,
Expand All @@ -240,36 +248,39 @@ func (m *MySQLServerClient) GetOrPrepareSecret(ctx context.Context, instance *az

secret := map[string][]byte{}
var key types.NamespacedName
var err error
if strings.EqualFold(createmode, "default") { // new Mysql server creation

key = types.NamespacedName{Name: name, Namespace: instance.Namespace}
if stored, err := m.SecretClient.Get(ctx, key); err == nil {
return stored, nil
}
var Username string
var Password string

randomUsername := helpers.GenerateRandomUsername(10)
randomPassword := helpers.NewPassword()

secret["username"] = []byte(randomUsername)
secret["fullyQualifiedUsername"] = []byte(fmt.Sprintf("%s@%s", randomUsername, name))
secret["password"] = []byte(randomPassword)
secret["mySqlServerName"] = []byte(name)
// TODO: The below may not be right for non Azure public cloud.
secret["fullyQualifiedServerName"] = []byte(name + ".mysql.database.azure.com")
return secret, nil
// See if secret already exists and return if it does
key = types.NamespacedName{Name: name, Namespace: instance.Namespace}
if stored, err := m.SecretClient.Get(ctx, key); err == nil {
return stored, nil
}
// replica creation
sourceServerId := instance.Spec.ReplicaProperties.SourceServerId
if len(sourceServerId) != 0 {
//Parse to get source server name
sourceServerIdSplit := strings.Split(sourceServerId, "/")
sourceserver := sourceServerIdSplit[len(sourceServerIdSplit)-1]
key = types.NamespacedName{Name: sourceserver, Namespace: instance.Namespace}
if stored, err := m.SecretClient.Get(ctx, key); err == nil {
return stored, nil

if strings.EqualFold(createmode, "default") { // new Mysql server creation
// Generate random username password if secret does not exist already
Username = helpers.GenerateRandomUsername(10)
Password = helpers.NewPassword()
} else { // replica
sourceServerId := instance.Spec.ReplicaProperties.SourceServerId
if len(sourceServerId) != 0 {
// Parse to get source server name
sourceServerIdSplit := strings.Split(sourceServerId, "/")
sourceserver := sourceServerIdSplit[len(sourceServerIdSplit)-1]

// Get the username and password from the source server's secret
key = types.NamespacedName{Name: sourceserver, Namespace: instance.Namespace}
if sourcesecret, err := m.SecretClient.Get(ctx, key); err == nil {
Username = string(sourcesecret["username"])
Password = string(sourcesecret["password"])
}
}
// secret for source server does not exist, return error
}
return secret, err

// Populate secret fields
secret["username"] = []byte(Username)
secret["fullyQualifiedUsername"] = []byte(fmt.Sprintf("%s@%s", Username, name))
secret["password"] = []byte(Password)
secret["mySqlServerName"] = []byte(name)
return secret, nil
}

0 comments on commit 25735b1

Please sign in to comment.