Skip to content

Commit

Permalink
Merge branch 'master' into linters
Browse files Browse the repository at this point in the history
  • Loading branch information
Porges authored Jul 11, 2021
2 parents 5f2f91b + 81091cf commit 04a8c97
Show file tree
Hide file tree
Showing 11 changed files with 372 additions and 83 deletions.
9 changes: 9 additions & 0 deletions api/v1alpha2/mysqlserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ type MySQLServerSpec struct {
ReplicaProperties ReplicaProperties `json:"replicaProperties,omitempty"`
StorageProfile *MySQLStorageProfile `json:"storageProfile,omitempty"`
KeyVaultToStoreSecrets string `json:"keyVaultToStoreSecrets,omitempty"`

// +kubebuilder:validation:MinLength=1
// AdminSecret is the name of a Kubernetes secret containing the username and password of the
// MySQLServer administrator account. When specified, the username and password fields of this
// secret will be included in the generated secret associated with this MySQLServer.
// If AdminSecret is specified but a secret with the given name is not found in the same namespace
// as the MySQLServer, then reconciliation will block until the secret is created.
// If this is not specified, a username and password will be automatically generated.
AdminSecret string `json:"adminSecret,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
77 changes: 75 additions & 2 deletions controllers/mysql_combined_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import (
"reflect"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"

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/azure-service-operator/pkg/resourcemanager/mysql"
"github.com/Azure/azure-service-operator/pkg/resourcemanager/mysql/mysqluser"
"github.com/Azure/azure-service-operator/pkg/secrets"
Expand Down Expand Up @@ -113,7 +115,7 @@ func TestMySQLHappyPath(t *testing.T) {
}

func RunMySQLUserHappyPath(ctx context.Context, t *testing.T, mySQLServerName string, mySQLDBName string, rgName string) {
assert := assert.New(t)
assert := require.New(t)

// Create a user in the DB
username := GenerateTestResourceNameWithRandom("user", 10)
Expand Down Expand Up @@ -197,3 +199,74 @@ func RunMySQLUserHappyPath(ctx context.Context, t *testing.T, mySQLServerName st
return true
}, tc.timeout, tc.retry, "waiting for DB user to be updated")
}

func TestMySQLUserSuppliedPassword(t *testing.T) {
t.Parallel()
defer PanicRecover(t)
ctx := context.Background()
assert := require.New(t)

// Add any setup steps that needs to be executed before each test
rgLocation := "westus2"
rgName := tc.resourceGroupName
mySQLServerName := GenerateTestResourceNameWithRandom("mysql-srv", 10)

adminSecretName := GenerateTestResourceNameWithRandom("mysqlsecret", 10)
adminUsername := helpers.GenerateRandomUsername(10)
adminPassword := helpers.NewPassword()
// Create the secret
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: adminSecretName,
Namespace: "default",
},
StringData: map[string]string{
"username": adminUsername,
"password": adminPassword,
},
}
err := tc.k8sClient.Create(ctx, secret)
assert.NoError(err)

// Create the mySQLServer object and expect the Reconcile to be created
mySQLServerInstance := v1alpha2.NewDefaultMySQLServer(mySQLServerName, rgName, rgLocation)
mySQLServerInstance.Spec.AdminSecret = adminSecretName
RequireInstance(ctx, t, tc, mySQLServerInstance)

mySQLDBName := GenerateTestResourceNameWithRandom("mysql-db", 10)
// Create the mySQLDB object and expect the Reconcile to be created
mySQLDBInstance := &azurev1alpha1.MySQLDatabase{
ObjectMeta: metav1.ObjectMeta{
Name: mySQLDBName,
Namespace: "default",
},
Spec: azurev1alpha1.MySQLDatabaseSpec{
Server: mySQLServerName,
ResourceGroup: rgName,
},
}
EnsureInstance(ctx, t, tc, mySQLDBInstance)

// This rule opens access to the public internet, but in this case
// there's literally no data in the database
ruleName := GenerateTestResourceNameWithRandom("mysql-fw", 10)
ruleInstance := &azurev1alpha1.MySQLFirewallRule{
ObjectMeta: metav1.ObjectMeta{
Name: ruleName,
Namespace: "default",
},
Spec: azurev1alpha1.MySQLFirewallRuleSpec{
Server: mySQLServerName,
ResourceGroup: rgName,
StartIPAddress: "0.0.0.0",
EndIPAddress: "255.255.255.255",
},
}
EnsureInstance(ctx, t, tc, ruleInstance)

// Create user and ensure it can be updated
RunMySQLUserHappyPath(ctx, t, mySQLServerName, mySQLDBName, rgName)

EnsureDelete(ctx, t, tc, mySQLDBInstance)
EnsureDelete(ctx, t, tc, mySQLServerInstance)
}
90 changes: 88 additions & 2 deletions controllers/mysqlserver_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import (
"context"
"testing"

"github.com/stretchr/testify/require"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/Azure/azure-service-operator/api/v1alpha2"
"github.com/Azure/azure-service-operator/pkg/errhelp"
)

func TestMySQLServerControllerNoResourceGroup(t *testing.T) {
func TestMySQLServerNoResourceGroup(t *testing.T) {
t.Parallel()
defer PanicRecover(t)
ctx := context.Background()
Expand All @@ -30,7 +34,7 @@ func TestMySQLServerControllerNoResourceGroup(t *testing.T) {
EnsureDelete(ctx, t, tc, mySQLServerInstance)
}

func TestMySQLServerControllerBadLocation(t *testing.T) {
func TestMySQLServerBadLocation(t *testing.T) {
t.Parallel()
defer PanicRecover(t)
ctx := context.Background()
Expand All @@ -46,3 +50,85 @@ func TestMySQLServerControllerBadLocation(t *testing.T) {
EnsureInstanceWithResult(ctx, t, tc, mySQLServerInstance, errhelp.InvalidResourceLocation, false)
EnsureDelete(ctx, t, tc, mySQLServerInstance)
}

func TestMySQLServerMissingUserSpecifiedSecret(t *testing.T) {
t.Parallel()
defer PanicRecover(t)
ctx := context.Background()
assert := require.New(t)

// Add any setup steps that needs to be executed before each test
mySQLServerName := GenerateTestResourceNameWithRandom("mysql-srv", 10)

mySQLServerInstance := v1alpha2.NewDefaultMySQLServer(mySQLServerName, tc.resourceGroupName, tc.resourceGroupLocation)
mySQLServerInstance.Spec.AdminSecret = "doesntexist"

EnsureInstanceWithResult(ctx, t, tc, mySQLServerInstance, "Failed to get AdminSecret", false)

// Confirm that the resource is not done reconciling
assert.Equal(false, mySQLServerInstance.Status.FailedProvisioning)
assert.Equal(false, mySQLServerInstance.Status.Provisioned)
// The fact that provisioning is false is an artifact of where we set provisioning in the reconcile loop
// and also how we're inconsistent with what exactly provisioning means in the context of these resources.
// Changing it is a larger change though so asserting it is false for now.
assert.Equal(false, mySQLServerInstance.Status.Provisioning)

EnsureDelete(ctx, t, tc, mySQLServerInstance)
}

func TestMySQLServerUserSpecifiedSecretMissingPassword(t *testing.T) {
t.Parallel()
defer PanicRecover(t)
ctx := context.Background()
assert := require.New(t)

// Add any setup steps that needs to be executed before each test
mySQLServerName := GenerateTestResourceNameWithRandom("mysql-srv", 10)
secretName := GenerateTestResourceNameWithRandom("mysqlserversecret", 10)
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: "default",
},
StringData: map[string]string{
"username": "testuser",
},
}
err := tc.k8sClient.Create(ctx, secret)
assert.NoError(err)

mySQLServerInstance := v1alpha2.NewDefaultMySQLServer(mySQLServerName, tc.resourceGroupName, tc.resourceGroupLocation)
mySQLServerInstance.Spec.AdminSecret = secretName

EnsureInstanceWithResult(ctx, t, tc, mySQLServerInstance, "is missing required \"password\" field", false)
EnsureDelete(ctx, t, tc, mySQLServerInstance)
}

func TestMySQLServerUserSpecifiedSecretMissingUsername(t *testing.T) {
t.Parallel()
defer PanicRecover(t)
ctx := context.Background()
assert := require.New(t)

// Add any setup steps that needs to be executed before each test
mySQLServerName := GenerateTestResourceNameWithRandom("mysql-srv", 10)
secretName := GenerateTestResourceNameWithRandom("mysqlserversecret", 10)
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: "default",
},
StringData: map[string]string{
"password": "testpassword",
},
}
err := tc.k8sClient.Create(ctx, secret)
assert.NoError(err)

mySQLServerInstance := v1alpha2.NewDefaultMySQLServer(mySQLServerName, tc.resourceGroupName, tc.resourceGroupLocation)
mySQLServerInstance.Spec.AdminSecret = secretName

EnsureInstanceWithResult(ctx, t, tc, mySQLServerInstance, "is missing required \"username\" field", false)
EnsureDelete(ctx, t, tc, mySQLServerInstance)
}

1 change: 1 addition & 0 deletions controllers/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,7 @@ func setup() error {
config.GlobalCredentials(),
secretClient,
k8sManager.GetScheme(),
k8sManager.GetClient(),
),
Telemetry: telemetry.InitializeTelemetryDefault(
"MySQLServer",
Expand Down
37 changes: 30 additions & 7 deletions docs/services/mysql/mysql.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ The MySQL operator suite consists of the following operators.
1. MySQL server - Deploys an `Azure Database for MySQL server` given the Location, Resource group and other properties. This operator also helps creating read replicas for MySQL server.
2. MySQL database - Deploys a database under the given `Azure Database for MySQL server`
3. MySQL firewall rule - Deploys a firewall rule to allow access to the `Azure Database for MySQL server` from the specified IP range
4. MySQL virtual network rule - Deploys a virtual network rule place the `Azure Database for MySQL server` into an Azure Virtual Network Subnet.
4. MySQL administrator - Sets the AAD Administrator of the `Azure Database for MySQL server` to the specified AAD identity.
5. MySQL user - Deploys a user into the `Azure Database for MySQL server`.
6. MySQL AAD User - Deploys an AAD user into the `Azure Database for MySQL server`.

### MySQL server

Expand All @@ -17,15 +21,17 @@ The value for kind, `MySQLServer` is the Custom Resource Definition (CRD) name.

The values under `spec` provide the values for the location where you want to create the server at and the Resource group in which you want to create it under. It also contains other values that are required to create the server like the `serverVersion`, `sslEnforcement` and the `sku` information.

Along with creating the MySQL server, this operator also generates the admin username and password for the MySQL server and stores it in a kube secret or keyvault (based on what is specified) with the same name as the MySQL server.
The `adminSecret` is optional and if provided must point to a Kubernetes secret containing a `username` and `password` field. If not specified, the operator will generate an administrator account `username` and `password`.
Along with creating the MySQL server, this operator also generates the admin `username` and `password` for the MySQL server and stores it in a kube secret or keyvault (based on what is specified). The generated secret is named according to [secrets naming](/docs/secrets.md).

This secret contains the following fields.

- `fullyqualifiedservername` : Fully qualified name of the MySQL server such as mysqlserver.mysql.database.azure.com
- `mysqlservername` : MySQL server name
- `username` : Server admin
- `password` : Password for the server admin
- `fullyqualifiedusername` : Fully qualified user name that is required by some apps such as <username>@<mysqlserver>
| Secret field | Content |
| -------------------------- | --------------------------------------------------------------------------------------------- |
| `fullyQualifiedServerName` | Fully qualified name of the MySQL server. Example: `mysqlserver.mysql.database.azure.com`. |
| `mySqlServerName` | MySQL server name. |
| `username` | Server admin account name. |
| `password` | Server admin account password. |
| `fullyQualifiedUsername` | Fully qualified user name that is required by some apps. Example: `<username>@<mysqlserver>`. |

For more information on where and how secrets are stored, look [here](/docs/secrets.md)

Expand Down Expand Up @@ -65,6 +71,12 @@ The `server` indicates the MySQL server on which you want to configure the new M

*Note*: When using MySQL Virtual Network Rules, the `Basic` SKU is not a valid op

### MySQL administrator

The MySQL administrator operator allows you to add an [AAD administrator](https://docs.microsoft.com/azure/mysql/concepts-azure-ad-authentication) to the MySQL server.

Here is a [sample YAML](/config/samples/azure_v1alpha1_mysqlserveradministrator.yaml).

### MySQL user

The MySQL user operator allows you to add a new user to an existing MySQL database.
Expand All @@ -79,6 +91,17 @@ The operator supports grant specified privileges using the concept of `roles`, a

The username is defined by `username`. The MySQL server admin secret is stored in the secret with name `adminSecret` in the keyvault named `adminSecretKeyVault`.

### MySQL AAD user
The MySQL AAD user operator allows you to add a new AAD user to an existing MySQL database.

Here is a [sample YAML](/config/samples/azure_v1alpha1_mysqlaaduser.yaml).

This controller is only avilable when using [Managed Identity authentication](https://github.com/Azure/azure-service-operator/blob/master/docs/howto/managedidentity.md) with ASO.
Attempting to use it without Managed Identity will result in an authentication error.

The AAD identity the operator is running as must have permissions to create users in the MySQLServer. This is most commonly granted by making the operator managed identity the MySQL Administrator using the
[MySQL administrator](mysql-administrator) operator described above.

## Deploy, view and delete resources

You can follow the steps [here](/docs/howto/resourceprovision.md) to deploy, view and delete resources.
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ func main() {
config.GlobalCredentials(),
secretClient,
mgr.GetScheme(),
mgr.GetClient(),
),
Telemetry: telemetry.InitializeTelemetryDefault(
"MySQLServer",
Expand Down
8 changes: 2 additions & 6 deletions pkg/resourcemanager/mysql/mysqlaaduser/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,8 @@ func (m *MySQLAADUserManager) Delete(ctx context.Context, obj runtime.Object, op

// GetServer retrieves a server
func (m *MySQLAADUserManager) GetServer(ctx context.Context, resourceGroupName, serverName string) (mysqlmgmt.Server, error) {
// We don't need to pass the secret client and scheme because
// they're not used getting the server.
// TODO: This feels a bit dodgy, consider taking secret client and
// scheme just so we can pass them in here.
client := mysqlserver.NewMySQLServerClient(m.Creds, nil, nil)
return client.GetServer(ctx, resourceGroupName, serverName)
client := mysqlserver.MakeMySQLServerAzureClient(m.Creds)
return client.Get(ctx, resourceGroupName, serverName)
}

// GetParents gets the parents of the user
Expand Down
6 changes: 4 additions & 2 deletions pkg/resourcemanager/mysql/mysqluser/mysqluser.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@ func (m *MySqlUserManager) GetDB(ctx context.Context, resourceGroupName string,

// GetServer retrieves a server
func (m *MySqlUserManager) GetServer(ctx context.Context, resourceGroupName, serverName string) (mysqlmgmt.Server, error) {
client := mysqlserver.NewMySQLServerClient(m.Creds, m.SecretClient, m.Scheme)
return client.GetServer(ctx, resourceGroupName, serverName)
// TODO: It's only ok to pass nil for KubeReader here because we know it's not needed to perform GET server.
// TODO: Ideally this would be done via a different struct than the one that also does MySQLServer reconciles
client := mysqlserver.MakeMySQLServerAzureClient(m.Creds)
return client.Get(ctx, resourceGroupName, serverName)
}

// CreateUser creates user with secret credentials
Expand Down
7 changes: 4 additions & 3 deletions pkg/resourcemanager/mysql/mysqluser/mysqluser_reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"reflect"
"strings"

mysqlserver "github.com/Azure/azure-service-operator/pkg/resourcemanager/mysql/server"
_ "github.com/go-sql-driver/mysql" //sql drive link
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -75,9 +76,9 @@ func (s *MySqlUserManager) Ensure(ctx context.Context, obj runtime.Object, opts
return false, err
}

adminUser := string(adminSecret["fullyQualifiedUsername"])
adminPassword := string(adminSecret[MSecretPasswordKey])
fullServerName := string(adminSecret["fullyQualifiedServerName"])
adminUser := string(adminSecret[mysqlserver.FullyQualifiedUsernameSecretKey])
adminPassword := string(adminSecret[mysqlserver.PasswordSecretKey])
fullServerName := string(adminSecret[mysqlserver.FullyQualifiedServerNameSecretKey])

db, err := mysql.ConnectToSqlDB(
ctx,
Expand Down
Loading

0 comments on commit 04a8c97

Please sign in to comment.