diff --git a/Makefile b/Makefile index d8c9b45876f..eeac70eb5f3 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,9 @@ test-existing-managers: generate fmt vet manifests ./pkg/resourcemanager/psql/firewallrule/... \ ./pkg/resourcemanager/appinsights/... \ ./pkg/resourcemanager/vnet/... \ - ./pkg/resourcemanager/apim/apimgmt... + ./pkg/resourcemanager/apim/apimgmt... \ + ./pkg/secrets/... + # Cleanup resource groups azure created by tests using pattern matching 't-rg-' test-cleanup-azure-resources: diff --git a/README.md b/README.md index 94841dbdd92..79c97963b85 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,17 @@ This project maintains [releases of the Azure Service Operator](https://github.c 1. [Resource Group](/docs/resourcegroup/resourcegroup.md) 2. [EventHub](/docs/eventhub/eventhub.md) 3. [Azure SQL](/docs/azuresql/azuresql.md) - -For more information on troubleshooting resources, refer to [this](/docs/viewresources.md) link +4. [Azure Keyvault](/docs/keyvault/keyvault.md) +5. [Azure Rediscache](/docs/rediscache/rediscache.md) +6. [Storage Account](/docs/storage/storageaccount.md) +7. [Blob container](/docs/storage/blobcontainer.md) +8. [Azure Database for PostgreSQL](/docs/postgresql/postgresql.md) +9. [Virtual Network](/docs/virtualnetwork/virtualnetwork.md) +10.[Application Insights](/docs/appinsights/appinsights.md) +11.[API Management](/docs/apimgmt/apimgmt.md) +12.[Cosmos DB](/docs/cosmosdb/cosmosdb.md) + +For more information on deploying, troubleshooting & deleting resources, refer to [this](/docs/customresource.md) link ## Building the operators diff --git a/api/v1alpha1/keyvault_types.go b/api/v1alpha1/keyvault_types.go index cc2185af49b..3b21a2c1f67 100644 --- a/api/v1alpha1/keyvault_types.go +++ b/api/v1alpha1/keyvault_types.go @@ -14,6 +14,7 @@ type KeyVaultSpec struct { EnableSoftDelete bool `json:"enableSoftDelete,omitempty"` NetworkPolicies *NetworkRuleSet `json:"networkPolicies,omitempty"` AccessPolicies *[]AccessPolicyEntry `json:"accessPolicies,omitempty"` + Sku KeyVaultSku `json:"sku,omitempty"` } type NetworkRuleSet struct { @@ -38,6 +39,13 @@ type AccessPolicyEntry struct { Permissions *Permissions `json:"permissions,omitempty"` } +// KeyVaultSku the SKU of the Key Vault +type KeyVaultSku struct { + // Name - The SKU name. Required for account creation; optional for update. + // Possible values include: 'Premium', `Standard` + Name string `json:"name,omitempty"` +} + type Permissions struct { Keys *[]string `json:"keys,omitempty"` Secrets *[]string `json:"secrets,omitempty"` diff --git a/config/samples/azure_v1alpha1_keyvault.yaml b/config/samples/azure_v1alpha1_keyvault.yaml index 0a45688bbe4..cc482ebacf9 100644 --- a/config/samples/azure_v1alpha1_keyvault.yaml +++ b/config/samples/azure_v1alpha1_keyvault.yaml @@ -9,6 +9,9 @@ spec: resourceGroup: resourcegroup-azure-operators location: westus enableSoftDelete: false + # possible values for sku.Name are "Standard" or "Premium" + sku: + name: standard networkPolicies: bypass: AzureServices # AzureServices or None defaultAction: Allow # Allow or Deny diff --git a/config/samples/azure_v1alpha1_keyvault_simple.yaml b/config/samples/azure_v1alpha1_keyvault_simple.yaml index 6cc965923fd..23c256f9ae1 100644 --- a/config/samples/azure_v1alpha1_keyvault_simple.yaml +++ b/config/samples/azure_v1alpha1_keyvault_simple.yaml @@ -9,3 +9,6 @@ spec: resourceGroup: resourcegroup-azure-operators location: westus enableSoftDelete: false + # Optional: possible values for sku.Name are "Standard" or "Premium". Default is "Standard" + #sku: + # name: standard diff --git a/config/samples/azure_v1alpha1_postgresql_everything.yaml b/config/samples/azure_v1alpha1_postgresql_everything.yaml new file mode 100644 index 00000000000..41db022619d --- /dev/null +++ b/config/samples/azure_v1alpha1_postgresql_everything.yaml @@ -0,0 +1,36 @@ +apiVersion: azure.microsoft.com/v1alpha1 +kind: PostgreSQLServer +metadata: + name: postgresqlserver-sample +spec: + location: southcentralus + resourceGroup: resourcegroup-azure-operators + serverVersion: "10" + sslEnforcement: Enabled + sku: + name: B_Gen5_2 + tier: Basic + family: Gen5 + size: "51200" + capacity: 2 + # Use the field below to optionally specify a different keyvault + # to store the server admin credential secrets in + #keyVaultToStoreSecrets: asoSecretKeyVault +--- +apiVersion: azure.microsoft.com/v1alpha1 +kind: PostgreSQLDatabase +metadata: + name: postgresqldatabase-sample +spec: + resourceGroup: resourcegroup-azure-operators + server: postgresqlserver-sample +--- +apiVersion: azure.microsoft.com/v1alpha1 +kind: PostgreSQLFirewallRule +metadata: + name: postgresqlfirewallrule-sample +spec: + resourceGroup: resourcegroup-azure-operators + server: postgresqlserver-sample + startIpAddress: 0.0.0.0 + endIpAddress: 0.0.0.0 diff --git a/controllers/azuresql_combined_test.go b/controllers/azuresql_combined_test.go index 5afcf7ea3af..a92e1db2bcf 100644 --- a/controllers/azuresql_combined_test.go +++ b/controllers/azuresql_combined_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/assert" helpers "github.com/Azure/azure-service-operator/pkg/helpers" + kvsecrets "github.com/Azure/azure-service-operator/pkg/secrets/keyvault" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -148,6 +149,8 @@ func TestAzureSqlServerCombinedHappyPath(t *testing.T) { }) var sqlUser *azurev1alpha1.AzureSQLUser + var kvSqlUser1 *azurev1alpha1.AzureSQLUser + var kvSqlUser2 *azurev1alpha1.AzureSQLUser // run sub tests that require 2 servers or have to be run after rollcreds test ------------------ t.Run("group2", func(t *testing.T) { @@ -158,6 +161,7 @@ func TestAzureSqlServerCombinedHappyPath(t *testing.T) { // create a sql user and verify it provisions username := "sql-test-user" + helpers.RandomString(10) roles := []string{"db_owner"} + keyVaultSecretFormats := []string{"adonet"} sqlUser = &azurev1alpha1.AzureSQLUser{ ObjectMeta: metav1.ObjectMeta{ @@ -165,16 +169,111 @@ func TestAzureSqlServerCombinedHappyPath(t *testing.T) { Namespace: "default", }, Spec: azurev1alpha1.AzureSQLUserSpec{ - Server: sqlServerName, - DbName: sqlDatabaseName, - ResourceGroup: rgName, - Roles: roles, + Server: sqlServerName, + DbName: sqlDatabaseName, + ResourceGroup: rgName, + Roles: roles, + KeyVaultSecretFormats: keyVaultSecretFormats, }, } EnsureInstance(ctx, t, tc, sqlUser) + + // verify user's secret has been created + // this test suite defaults to Kube Secrets. They do not support keyvault-specific config but the spec is passed anyway + // to verify that passing them does not break the service + assert.Eventually(func() bool { + key := types.NamespacedName{Name: sqlUser.ObjectMeta.Name, Namespace: sqlUser.ObjectMeta.Namespace} + var secrets, _ = tc.secretClient.Get(ctx, key) + + return strings.Contains(string(secrets["azureSqlDatabaseName"]), sqlDatabaseName) + }, tc.timeoutFast, tc.retry, "wait for secret store to show azure sql user credentials") + t.Log(sqlUser.Status) }) + + t.Run("set up user in first db with custom keyvault", func(t *testing.T) { + t.Parallel() + + // create a sql user and verify it provisions + username := "sql-test-user" + helpers.RandomString(10) + roles := []string{"db_owner"} + + // This test will attempt to persist secrets to the KV that was instantiated as part of the test suite + keyVaultName := tc.keyvaultName + + kvSqlUser1 = &azurev1alpha1.AzureSQLUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: username, + Namespace: "default", + }, + Spec: azurev1alpha1.AzureSQLUserSpec{ + Server: sqlServerName, + DbName: sqlDatabaseName, + ResourceGroup: rgName, + Roles: roles, + KeyVaultToStoreSecrets: keyVaultName, + }, + } + + EnsureInstance(ctx, t, tc, kvSqlUser1) + + // Check that the user's secret is in the keyvault + keyVaultSecretClient := kvsecrets.New(keyVaultName) + + assert.Eventually(func() bool { + keyNamespace := "azuresqluser-" + sqlServerName + "-" + sqlDatabaseName + key := types.NamespacedName{Name: kvSqlUser1.ObjectMeta.Name, Namespace: keyNamespace} + var secrets, _ = keyVaultSecretClient.Get(ctx, key) + + return strings.Contains(string(secrets["azureSqlDatabaseName"]), sqlDatabaseName) + }, tc.timeoutFast, tc.retry, "wait for keyvault to show azure sql user credentials") + + t.Log(kvSqlUser1.Status) + }) + + t.Run("set up user in first db with custom keyvault and custom formatting", func(t *testing.T) { + t.Parallel() + + // create a sql user and verify it provisions + username := "sql-test-user" + helpers.RandomString(10) + roles := []string{"db_owner"} + formats := []string{"adonet"} + + // This test will attempt to persist secrets to the KV that was instantiated as part of the test suite + keyVaultName := tc.keyvaultName + + kvSqlUser2 = &azurev1alpha1.AzureSQLUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: username, + Namespace: "default", + }, + Spec: azurev1alpha1.AzureSQLUserSpec{ + Server: sqlServerName, + DbName: sqlDatabaseName, + ResourceGroup: rgName, + Roles: roles, + KeyVaultToStoreSecrets: keyVaultName, + KeyVaultSecretFormats: formats, + }, + } + + EnsureInstance(ctx, t, tc, kvSqlUser2) + + // Check that the user's secret is in the keyvault + keyVaultSecretClient := kvsecrets.New(keyVaultName) + + assert.Eventually(func() bool { + keyNamespace := "azuresqluser-" + sqlServerName + "-" + sqlDatabaseName + keyName := kvSqlUser2.ObjectMeta.Name + "-adonet" + key := types.NamespacedName{Name: keyName, Namespace: keyNamespace} + var secrets, _ = keyVaultSecretClient.Get(ctx, key) + + return len(string(secrets[keyNamespace+"-"+keyName])) > 0 + }, tc.timeoutFast, tc.retry, "wait for keyvault to show azure sql user credentials with custom formats") + + t.Log(kvSqlUser2.Status) + }) }) var sqlFailoverGroupInstance *azurev1alpha1.AzureSqlFailoverGroup @@ -183,8 +282,40 @@ func TestAzureSqlServerCombinedHappyPath(t *testing.T) { sqlFailoverGroupNamespacedName := types.NamespacedName{Name: sqlFailoverGroupName, Namespace: "default"} t.Run("group3", func(t *testing.T) { - t.Run("delete db user", func(t *testing.T) { + t.Run("delete db users and ensure that their secrets have been cleaned up", func(t *testing.T) { EnsureDelete(ctx, t, tc, sqlUser) + EnsureDelete(ctx, t, tc, kvSqlUser1) + EnsureDelete(ctx, t, tc, kvSqlUser2) + + // Check that the user's secret is in the keyvault + keyVaultSecretClient := kvsecrets.New(tc.keyvaultName) + + assert.Eventually(func() bool { + key := types.NamespacedName{Name: sqlUser.ObjectMeta.Name, Namespace: sqlUser.ObjectMeta.Namespace} + var _, err = tc.secretClient.Get(ctx, key) + + // Once the secret is gone, the Kube secret client will return an error + return err != nil && strings.Contains(err.Error(), "not found") + }, tc.timeoutFast, tc.retry, "wait for the azuresqluser kube secret to be deleted") + + assert.Eventually(func() bool { + keyNamespace := "azuresqluser-" + sqlServerName + "-" + sqlDatabaseName + key := types.NamespacedName{Name: kvSqlUser1.ObjectMeta.Name, Namespace: keyNamespace} + var _, err = keyVaultSecretClient.Get(ctx, key) + + // Once the secret is gone, the KV secret client will return an err + return err != nil && strings.Contains(err.Error(), "secret does not exist") + }, tc.timeoutFast, tc.retry, "wait for the azuresqluser keyvault secret to be deleted") + + assert.Eventually(func() bool { + keyNamespace := "azuresqluser-" + sqlServerName + "-" + sqlDatabaseName + keyName := kvSqlUser2.ObjectMeta.Name + "-adonet" + key := types.NamespacedName{Name: keyName, Namespace: keyNamespace} + var _, err = keyVaultSecretClient.Get(ctx, key) + + // Once the secret is gone, the KV secret client will return an err + return err != nil && strings.Contains(err.Error(), "secret does not exist") + }, tc.timeoutFast, tc.retry, "wait for the azuresqluser custom formatted keyvault secret to be deleted") }) t.Run("delete local firewallrule", func(t *testing.T) { diff --git a/controllers/eventhub_controller_test.go b/controllers/eventhub_controller_test.go index 6bab0ffee36..3faa18626bd 100644 --- a/controllers/eventhub_controller_test.go +++ b/controllers/eventhub_controller_test.go @@ -13,8 +13,6 @@ import ( azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" "github.com/Azure/azure-service-operator/pkg/errhelp" - helpers "github.com/Azure/azure-service-operator/pkg/helpers" - "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" kvhelper "github.com/Azure/azure-service-operator/pkg/resourcemanager/keyvaults" kvsecrets "github.com/Azure/azure-service-operator/pkg/secrets/keyvault" @@ -192,13 +190,10 @@ func TestEventHubControllerCreateAndDeleteCustomKeyVault(t *testing.T) { rgLocation := tc.resourceGroupLocation ehnName := tc.eventhubNamespaceName eventhubName := GenerateTestResourceNameWithRandom("ev", 10) - keyVaultNameForSecrets := helpers.FillWithRandom(GenerateTestResourceName("ev-kv"), 24) - userID := config.ClientID() + keyVaultNameForSecrets := tc.keyvaultName - // Create KeyVault with access policies - _, err := kvhelper.AzureKeyVaultManager.CreateVaultWithAccessPolicies(ctx, rgName, keyVaultNameForSecrets, rgLocation, userID) - - _, err = kvhelper.AzureKeyVaultManager.GetVault(ctx, rgName, keyVaultNameForSecrets) + // Instantiate a KV client for the Keyvault that was created during test suite setup + _, err := kvhelper.AzureKeyVaultManager.GetVault(ctx, rgName, keyVaultNameForSecrets) assert.Equal(nil, err, "wait for keyvault to be available") // Create the EventHub object and expect the Reconcile to be created diff --git a/controllers/helpers.go b/controllers/helpers.go index 46609e44263..cb0d76fcbb1 100644 --- a/controllers/helpers.go +++ b/controllers/helpers.go @@ -52,6 +52,7 @@ type TestContext struct { namespaceLocation string storageAccountName string blobContainerName string + keyvaultName string resourceGroupManager resourcegroupsresourcemanager.ResourceGroupManager redisCacheManager resourcemanagerrediscaches.RedisCacheManager eventHubManagers resourcemanagereventhub.EventHubManagers diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 114c8fe7c02..55b093abbdb 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -76,6 +76,8 @@ func setup() error { blobContainerName := GenerateTestResourceName("blob-prime") containerAccessLevel := s.PublicAccessContainer + keyvaultName := GenerateAlphaNumTestResourceName("kv-prime") + var timeout time.Duration testEnv = &envtest.Environment{ @@ -171,7 +173,6 @@ func setup() error { scheme.Scheme, ) redisCacheManager := resourcemanagerrediscaches.NewAzureRedisCacheManager( - ctrl.Log.WithName("rediscachemanager").WithName("RedisCache"), secretClient, scheme.Scheme, ) @@ -189,7 +190,7 @@ func setup() error { ) sqlActionManager = resourcemanagersqlaction.NewAzureSqlActionManager(secretClient, scheme.Scheme) - timeout = time.Second * 720 + timeout = time.Second * 780 err = (&KeyVaultReconciler{ Reconciler: &AsyncReconciler{ @@ -607,6 +608,7 @@ func setup() error { namespaceLocation: namespaceLocation, storageAccountName: storageAccountName, blobContainerName: blobContainerName, + keyvaultName: keyvaultName, eventHubManagers: eventHubManagers, eventhubClient: eventhubClient, resourceGroupManager: resourceGroupManager, @@ -673,6 +675,12 @@ func setup() error { return err } + log.Println("Creating KV:", keyvaultName) + _, err = resourcemanagerkeyvaults.AzureKeyVaultManager.CreateVaultWithAccessPolicies(context.Background(), resourceGroupName, keyvaultName, resourcegroupLocation, resourcemanagerconfig.ClientID()) + if err != nil { + return err + } + log.Println(fmt.Sprintf("finished common controller test setup")) return nil diff --git a/devops/azure-pipelines.yaml b/devops/azure-pipelines.yaml index 1d909b87c62..77a307db075 100644 --- a/devops/azure-pipelines.yaml +++ b/devops/azure-pipelines.yaml @@ -150,7 +150,7 @@ steps: env: BUILD_ID: $(Build.BuildId) inputs: - azureSubscription: 'Az Internal Consumption' + azureSubscription: 'CSE AmWest Sub - ASO' scriptType: bash scriptLocation: inlineScript workingDirectory: '$(MODULE_PATH)' diff --git a/docs/apimgmt/apimgmt.md b/docs/apimgmt/apimgmt.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/appinsights/appinsights.md b/docs/appinsights/appinsights.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/azuredatalakegen2/azuredatalakegen2.md b/docs/azuredatalakegen2/azuredatalakegen2.md deleted file mode 100644 index e45e2e158c1..00000000000 --- a/docs/azuredatalakegen2/azuredatalakegen2.md +++ /dev/null @@ -1,148 +0,0 @@ -# Azure Data Lake Gen2 Operator -## Resources Supported -The Azure Data Lake Gen2 operator can be used to provision the following resources -1. Azure Data Lake Gen2 enabled storage account - Deploys a data lake gen2 enabled storage account using the storage operator -2. Azure Data Lake Gen2 Filesystem - Deploys a filesystem inside of a data lake gen2 enabled storage account - -## Deploying Data Lake Resources - -You can follow the steps [here](/docs/development.md) to either run the operator locally or in a real Kubernetes cluster. - -You can use the YAML files in the `config/samples` folder to create the resources. - -**Note** Don't forget to set the Service Principal ID, Service Principal secret, Tenant ID and Subscription ID as environment variables - -### Azure Data Lake Gen 2 Enabled Storage Account -This is the sample YAML for the Data Lake Gen2 enabled storage account - -```yaml - apiVersion: azure.microsoft.com/v1alpha1 - kind: Storage - metadata: - name: adlsaccountsample - spec: - location: westus - resourceGroup: resourcegroup-azure-operators - sku: - name: Standard_LRS - kind: StorageV2 - accessTier: Hot - supportsHttpsTrafficOnly: true - dataLakeEnabled: true -``` -Important Values: -- Kind: the Custom Resource Definition (CRD) name -- Metadata.Name: the name of the data lake enabled storage account that will be created -- Spec.Location: Azure region where you want to create the data lake enabled storage account -- Spec.ResourceGroup: Name of the resource group under which you want to create the data lake enabled storage account -- Spec.Kind: The kind of storage account that you want to create. This value must be `StorageV2` in order for it to be a data lake -- Spec.AccessTier - The access tier for the storage account. Choose "Hot" access tier for frequently accessed data and "Cool" for infrequently accessed data -- Spec.SupportsHttpsTrafficOnly - When "enabled", requires connection requests to the storage account to be only over HTTPS -- Spec.DataLakeEnabled: If set to `true`, the storage account will have hierarchical namespace enabled and will, therefore, be a data lake enabled storage account - -To create an instance of the Data Lake Enabled Storage Account: - -```shell -kubectl apply -f config/samples/azure_v1_azuredatalakegen2storage.yaml -``` -### Azure Data Lake Gen 2 FileSystem -This is the sample YAML for the Data Lake Gen2 FileSystem - -```yaml - apiVersion: azure.microsoft.com/v1alpha1 - kind: AzureDataLakeGen2FileSystem - metadata: - name: adls-filesystem-sample - spec: - storageAccountName: adlsaccountsample - resourceGroup: resourcegroup-azure-operators -``` -Important Values: -- Kind: the Custom Resource Definition (CRD) name -- Metadata.Name: the name of the data lake enabled storage account that will be created -- Spec.StorageAccountName: Name of the data lake enabled storage account in which you would like to create the filesystem -- Spec.ResourceGroup: Name of the resource group under which the storage account lives - -To create an instance of the Azure Data Lake Gen2 FileSystem: - -```shell -kubectl apply -f config/samples/azure_v1_azuredatalakegen2filesystem.yaml -``` - -## View and Troubleshoot ADLS Gen2 Resources - -To view your created data lake resources run the following command: - -```shell -kubectl get AzureDataLakeGen2FileSystem -``` -You should see the AzureDataLakeGen2FileSystem instances as below - -```shell -NAME AGE -adls-filesystem-sample 5s -``` - -If you want to see more details about a particular resource instance such as the `Status` or `Events`, you can use the below command - -```shell -kubectl describe -``` - -For instance, the below command is used to get more details about the `adls-filesystem-sample` instance - -```shell -kubectl describe AzureDataLakeGen2FileSystem adls-filesystem-sample -``` - -The expected output should look like this: - -```shell -Name: adls-filesystem-sample -Namespace: default -Labels: -Annotations: kubectl.kubernetes.io/last-applied-configuration: - {"apiVersion":"azure.microsoft.com/v1alpha1","kind":"AzureDataLakeGen2FileSystem","metadata":{"annotations":{},"name":"adls-filesystem", - "namespace":"default"}... -API Version: azure.microsoft.com/v1alpha1 -Kind: AzureDataLakeGen2FileSystem -Metadata: - Creation Timestamp: 2019-10-24T23:59:22Z - Finalizers: - filesystem.finalizers.azure.com - Generation: 2 - Resource Version: 1708 - Self Link: /apis/azure.microsoft.com/v1alpha1/namespaces/default/azuredatalakegen2filesystems/adls-filesystem - UID: 3d75a97b-1969-4d00-bf75-85516c27f43c -Output: -Spec: - Resource Group: resourcegroup-azure-operators - Storage Account Name: adlsaccountsample -Events: - Type Reason Age From Message - ---- ------ ---- ---- ------- - Normal Updated 41s AzureDataLakeGen2FileSystem-controller finalizer filesystem.finalizers.azure.com added - Warning Failed 40s AzureDataLakeGen2FileSystem-controller Couldn't create resource in azure - Normal Updated 8s AzureDataLakeGen2FileSystem-controller adls-filesystem-sample provisioned -``` -The `Events` have a chronological record of what occurred through the process of provisioning the resource. - -## Delete an ADLSGen2 Resource - -To delete an existing resource from Kubernetes and Azure, use the following command. - -```shell -kubectl delete   -``` - -For instance, deleting the above FileSystem instance would look like this. - -```shell -kubectl delete AzureDataLakeGen2FileSystem adls-filesystem-sample -``` - -The output should look like this: - -```shell -azuredatalakegen2filesystem.azure.microsoft.com "adls-filesystem-sample" deleted -``` \ No newline at end of file diff --git a/docs/azuresql/azuresql.md b/docs/azuresql/azuresql.md index 731074ad611..1cf8494da69 100644 --- a/docs/azuresql/azuresql.md +++ b/docs/azuresql/azuresql.md @@ -12,14 +12,6 @@ The Azure SQL operator suite consists of the following operators. 5. Azure SQL failover group - Deploys a failover group on a specified Azure SQL server given the secondary server and the databases to failover 6. Azure SQL User - Creates an user on the specified Azure SQL database and stores the username/password as secrets -## Deploying SQL Resources - -First, you need to have the Azure Service Operator deployed in your cluster. - -You can follow the steps from the [project root](../../README.md) to deploy a release or build a [development version locally](/docs/development.md). - -You can use the YAML files in the `config/samples` folder to create the resources. - ### Azure SQL server Here is a [sample YAML](/config/samples/azure_v1alpha1_azuresqlserver.yaml) for the Azure SQL server. @@ -150,30 +142,11 @@ The default secret name prefix in Key Vault is `azuresqluser----adonet`. Here is a [sample YAML](/config/samples/azure_v1alpha1_azuresqluser.yaml) for creating a database user - The `name` is used to generate the username on the database. The exact name is not used but rather a UUID is appended to this to make it unique. `server` and `dbname` qualify the database on which you want to create the user on. `adminsecret` is the name of the secret where the username and password will be stored. `roles` specify the security roles that this user should have on the specified database. -## View and Troubleshoot SQL Resources - -You can view your created Azure SQL resources using the steps [here](viewresources.md) - -## Delete a SQL Resource - -To delete an existing resource from Kubernetes and Azure, use the following command. - -```shell -kubectl delete   -``` - -For instance, deleting the above SqlServer instance would look like this. - -```shell -kubectl delete AzureSqlServer sqlserver-sample -``` - -The following message should appear: +## Deploy, view and delete resources -`azuresqlserver.azure.microsoft.com sqlserver-sample deleted.` +You can follow the steps [here](/docs/customresource.md) to deploy, view and delete resources. ## Demo diff --git a/docs/cosmosdb/cosmosdb.md b/docs/cosmosdb/cosmosdb.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/viewresources.md b/docs/customresource.md similarity index 75% rename from docs/viewresources.md rename to docs/customresource.md index c178e2eff09..b0a36b32f1c 100644 --- a/docs/viewresources.md +++ b/docs/customresource.md @@ -1,8 +1,25 @@ -## View and Troubleshoot Custom Resources +# Deploy, View and Delete Resources + +## Deploy Custom Resource + +You can follow the steps [here](/docs/development.md) to run the operator locally or follow the steps [here](/docs/deploy.md) to deploy to a real Kubernetes cluster. + +You can use the YAML files in the `config/samples` folder to create the resources using the following command. + +``` +kubectl apply -f +``` + +### Tags in Azure + +When deploying resources using the YAML files, you can specify as labels any tags you want to add to the resource in Azure. +If the labels contain characters that are not allowed as tags in Azure (<,>,\,/,%,?), those characters will be replaced by a period(.) and added as Azure tags. + +## View and Troubleshoot Custom Resource To view your created custom resource, run the following command: -```shell +``` kubectl get  ``` @@ -10,30 +27,30 @@ where CRD is the Custom Resource Definition name or `Kind` for the resource. For instance, you can get the Azure SQL servers provisioned using the command -```shell +``` kubectl get AzureSqlServer ``` You should see the AzureSqlServer instances as below -```shell +``` NAME AGE sqlserver-sample 1h ``` If you want to see more details about a particular resource instance such as the `Status` or `Events`, you can use the below command -```shell +``` kubectl describe ``` For instance, the below command is used to get more details about the `sqlserver-sample` instance -```shell +``` kubectl describe AzureSqlServer sqlserver-sample ``` -```shell +``` Name: sqlserver-sample234 Namespace: default Labels: @@ -70,6 +87,24 @@ The `Status` section gives you the current state of the resource, it's `State` a The `Events` have a chronological record of what occurred through the process of provisioning the resource. +## Delete Resource + +To delete an existing resource from Kubernetes and Azure, use the following command. + +``` +kubectl delete   +``` + +For instance, deleting a AzureSqlServer instance would look like this. + +``` +kubectl delete azuresqlserver sqlserver-sample +``` + +The following message should appear: + +`azuresqlserver.azure.microsoft.com sqlserver-sample deleted.` + ## Delete Kubernetes instances without deleting Azure resources In some cases, like when you setup a new Kubernetes cluster with the same CRDs and want to take down the older cluster, you might need to delete the Kubernetes instances without impacting the Azure resources (as these are still tracked by CRDs in a different cluster) diff --git a/docs/development.md b/docs/development.md index fd544635900..805e67cbe70 100644 --- a/docs/development.md +++ b/docs/development.md @@ -107,8 +107,16 @@ Then, open a web browser and navigate to the [Metrics Endpoint](http://127.0.0.1 If you're using VSCode with [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extensions installed, you can quickly have your environment set up and ready to go, with everything you need to get started. -1. Open this project in VSCode. -2. Inside the folder `.devcontainer`, create a file called `.env` and using the following template, copy your environment variable details. +1. Clone the repository into the following folder `~/go/src/github.com/Azure`. +2. Make sure the environment variable `GO111MODULE` is set to `on`. + + ``` + export GO111MODULE=on + ``` + +3. Install test certificates using `make generate-test-certs`. +4. Open this folder `~/go/src/github.com/Azure` in VSCode. +5. Inside the folder `.devcontainer`, create a file called `.env` and using the following template, copy your environment variable details. ```txt AZURE_CLIENT_ID= @@ -120,18 +128,15 @@ If you're using VSCode with [Remote - Containers](https://marketplace.visualstud AZURE_TENANT_ID= ``` -3. Open the Command Pallet (`Command+Shift+P` on MacOS or `CTRL+Shift+P` on Windows), type `Remote-Containers: Open Folder in Container...`, select the ```Azure-service-operator``` folder and hit enter. - -4. VSCode will relaunch and start building our development container. This will install all the necessary dependencies required for you to begin developing. - -5. Once the container has finished building, you can now start testing your Azure Service Operator within your own local kubernetes environment via the terminal inside VSCode. - -**Note**: after the DevContainer has finished building, the kind cluster will start initialising and installing the Azure Service Operator in the background. This will take some time before it is available. +6. Open the Command Pallet (`Command+Shift+P` on MacOS or `CTRL+Shift+P` on Windows), type `Remote-Containers: Open Folder in Container...`, select the ```Azure-service-operator``` folder and hit enter. +7. VSCode will relaunch and start building our development container. This will install all the necessary dependencies required for you to begin developing. +8. Once the container has finished building, you can now start testing your Azure Service Operator within your own local kubernetes environment via the terminal inside VSCode. -To see when the kind cluster is ready, use `docker ps -a` to list your running containers, look for `IMAGE` with the name `azure-service-operator_devcontainer_docker-in-docker...`. Using that image's `CONTAINER ID`, use `docker logs -f CONTAINER ID` to view the logs from the container setting up your cluster. +**Note**: after the DevContainer has finished building, the Kind cluster will start initializing and installing the Azure Service Operator in the background. This will take some time before it is available. -6. Use `kubectl apply` with the sample YAML files to create custom resources for testing. -For eg., use `kubectl apply -f config/samples/azure_v1alpha1_azuresqlserver.yaml` from the terminal to create a SQL server using the operator. +To see when the Kind cluster is ready, use `docker ps -a` to list your running containers, look for `IMAGE` with the name `azure-service-operator_devcontainer_docker-in-docker...`. Using that image's `CONTAINER ID`, use `docker logs -f CONTAINER ID` to view the logs from the container setting up your cluster. +9. Use `kubectl apply` with the sample YAML files to create custom resources for testing. +For eg., use `kubectl apply -f config/samples/azure_v1alpha1_azuresqlserver.yaml` from the terminal to create a SQL server using the operator. `kubectl describe SqlServer` would show the events that indicate if the resource is created or being created. ## Step-by-Step for developing a new operator using the generic controller @@ -140,7 +145,7 @@ This project utilizes a generic async controller that separates out the Kubernet 1. Clone the repository to your computer. Run `go mod download` to download dependencies. This may take several minutes. 2. Ensure you have `Kubebuilder` installed on your computer. -3. From the root folder of the cloned repository, run the following command +3. From the root folder of the cloned repository, run the following command: ``` kubebuilder create api --group azure --version v1alpha1 --kind @@ -263,15 +268,18 @@ go build -o bin/manager main.go This is the service specific piece that differs from one Azure service to another, and the crux of the service provisioning/deletion logic used by the controller. + **Note** Please make sure that you make all creation and deletion calls to Azure are asynchronous. + a. The guidance is to add a folder under `pkg/resourcemanager` for the new resource, like for instance, `pkg/resourcemanager/newresource` b. Use subfolders under this folder if you have subresources. For instance, for PostgreSQL we have separate sub folders for `server`, `database` and `firewallrule` - c. Add two files under this folder + c. Add these files under this folder azurenewtype_manager.go - azurenewtype_client.go + azurenewtype_reconcile.go + azurenewtype.go - d. The azurenewtype_manager.go file would implement the ARMClient as follows,and in addition have any other functions you need for Create, Delete, and Get of the resource. + d. The `azurenewtype_manager.go` file would implement the interface that includes the ARMClient as follows, and in addition have any other functions you need for Create, Delete, and Get of the resource. ```go type AzureNewTypeManager interface { @@ -283,34 +291,34 @@ go build -o bin/manager main.go ``` - e. The `azurenewtype_client.go` file defines a struct that implements the `AzureNewTypeManager` interface. - - This struct would have the following functions attached to it from the ARMClient interface - `Ensure`, `Delete`, `GetParents`. + The `azurenewtype.go` file defines a struct that implements the `AzureNewTypeManager` interface. It has the definitions of the Create, Delete and Get functions included in the interface in the `azurenewtype_manager.go` + + Here is an example of what this struct looks like. + + **Note** Don't add a logger to this struct. Return all errors from this file to the controller so we can log it there. + + ```go + type AzureNewTypeClient struct {} + + func NewAzureNewTypeClient() *AzureNewTypeClient { + return &AzureNewTypeClient{} + } + ``` + + e. The `azurenewtype_reconcile.go` file implements the following functions in the ARMClient interface: + - `Ensure`, `Delete`, `GetParents`, `GetStatus` - It would also have a `convert` function to convert the runtime object into the appropriate type - - It would also have the other functions defined in the `AzureNewTypeManager` interface Some key points to note: (i) The Ensure and Delete functions return as the first return value, a bool which indicates if the resource was found in Azure. So Ensure() if successful would return `true` and Delete() if successful would return `false` - (ii) On successful provisioning in `Ensure()`, set instance.Status.Message to `successfully provisioned` to be consistent across all controllers. (There is a constant called `SuccessMsg` in the `resourcemanager` package that you can use for this to be consistent) - (ii) The GetParents() function returns the Azure Resource Manager (ARM) hierarchy of the resource. The order here matters - the immediate hierarchical resource should be returned first. For instance, for an Azure SQL database, the first parent should be Azure SQL server followed by the Resource Group. - An example is shown below: + (ii) On successful provisioning in `Ensure()`, + - set instance.Status.Message to the constant `SuccessMsg` in the `resourcemanager` package to be consistent across all controllers. + - set instance.Status.ResourceID to the full Azure Resource ID of the resource + - set instance.Status.Provisioned to `true` and instance.Status.Provisioning to `false` ```go - type AzureNewTypeClient struct { - Telemetry telemetry.Telemetry - } - - func NewAzureNewTypeClient(log logr.Logger) *AzureNewTypeClient { - return &AzureNewTypeClient{ - Telemetry: telemetry.InitializeTelemetryDefault( - "NewType", - log, - ), - } - } - ... - ... - func (p *AzureNewTypeClient) Ensure(ctx context.Context, obj runtime.Object) (found bool, err error) { + func (p *AzureNewTypeClient) Ensure(ctx context.Context, obj runtime.Object) (found bool, err error) { instance, err := p.convert(obj) if err != nil { return true, err @@ -331,7 +339,13 @@ go build -o bin/manager main.go // successful return return false, nil } + ``` + + (ii) The `GetParents()` function returns the Azure Resource Manager (ARM) hierarchy of the resource. The order here matters - the immediate hierarchical resource should be returned first. For instance, for an Azure SQL database, the first parent should be Azure SQL server followed by the Resource Group. + An example is shown below: + + ```go func (p *AzureNewTypeClient) GetParents(obj runtime.Object) ([]resourcemanager.KubeParent, error) { instance, err := p.convert(obj) @@ -351,8 +365,11 @@ go build -o bin/manager main.go }, }, nil } + ``` + + (iii) The `GetStatus()` is a boilerplate function, use the below function and alter to use the struct you attach the function to. - // GetStatus is a boilerplate function... just use this function body (make sure to alter what struct you attach the function to) + ```go func (p *AzureNewTypeClient) GetStatus(obj runtime.Object) (*v1alpha1.ASOStatus, error) { instance, err := g.convert(obj) if err != nil { @@ -360,7 +377,11 @@ go build -o bin/manager main.go } return &instance.Status, nil } + ``` + (iv) The `convert()` function looks like the below, use the correct type based on the controller you are implementing. + + ```go func (p *AzureNewTypeClient) convert(obj runtime.Object) (*v1alpha1.AzureNewType, error) { local, ok := obj.(*v1alpha1.AzureNewType) if !ok { @@ -368,25 +389,16 @@ go build -o bin/manager main.go } return local, nil } - - // Add other Create, Delete, Get methods for the resource as needed - - ``` + ``` 9. *Tests* Add the following tests for your new operator: (i) Unit test for the new type: You will add this as a file named `azurenewtype_types_test.go` under `api/v1alpha`. Refer to the existing tests in the repository to author this for your new type. - (ii) Functional tests for the resource manager: These will be test files under `pkg/resourcemanager/newresource`. You can refer to the tests under `pkg/resourcemanage/psql/server` as reference for writing these. These tests help validate the resource creation and deletion in Azure. - Make sure to add this folder with the tests to the `make test-existing` target in the `Makefile` - *Tip*: You can run `go test` in this folder to test the functions before testing with the controller end-to-end. - (iii) Controller tests: This will be a file named `azurenewresourcetype_controller_test.go` under the `controllers` folder. Refer to the other controller tests in the repo for how to write this test. This test validates the new controller to make sure the resource is created and deleted in Kubernetes effectively. - -10. *Mocks for the controller test* - In order to run the Controller tests (9.(iii) above) without having to communicate with Azure, you will generate a mock of the `AzureNewResourceTypeManager` in `pkg/resourcemanager/mocks`. You can refer to the mocks under `pkg/resourcemanager/mock/psql` for how to write this mock. + (ii) Controller tests: This will be a file named `azurenewresourcetype_controller_test.go` under the `controllers` folder. Refer to the other controller tests in the repo for how to write this test. This test validates the new controller to make sure the resource is created and deleted in Kubernetes effectively. -11. *Instantiating the reconciler* +10. *Instantiating the reconciler* The last step to tie everything together is to ensure that your new controller's reconciler is instantiated in both the `main.go` and the `suite_test.go` (under `controllers` folder) files. @@ -428,14 +440,15 @@ go build -o bin/manager main.go Expect(err).ToNot(HaveOccurred()) ``` -12. Install the new CRD and generate the manifests needed using the following commands. This is required in order to generate canonical resource definitions (manifests as errors about a DeepCopyObject() method missing). +11. Install the new CRD and generate the manifests needed using the following commands. This is required in order to generate canonical resource definitions (manifests as errors about a DeepCopyObject() method missing). ``` + make generate make manifests make install ``` - Run tests using `make test` and deploy using `make deploy` + Run controller tests using `make test-existing-controllers` and deploy using `make deploy` If you make changes to the operator and want to update the deployment without recreating the cluster (when testing locally), you can use the `make update` to update your Azure Operator pod. If you need to rebuild the docker image without cache, use `make ARGS="--no-cache" update` diff --git a/docs/eventhub/eventhub.md b/docs/eventhub/eventhub.md index 3049b9fc236..619705017c3 100644 --- a/docs/eventhub/eventhub.md +++ b/docs/eventhub/eventhub.md @@ -10,20 +10,23 @@ The Eventhub operator can be used to provision the following resources. 3. Consumer groups - Deploys a consumer group given the Eventhub, Eventhub namespace and Resource Group. -## Deploying resources +### Eventhub - Deployment output -You can follow the steps [here](/docs/development.md) to either run the operator locally or in a real Kubernetes cluster. +The Eventhub operator deploys an Eventhub in the specified namespace according to the Spec. -You can use the YAML files in the `config/samples` folder to create the resources. +As an output of deployment, the operator stores a JSON formatted secret with the following fields. For more details on where the secrets are stored, look [here](/docs/secrets.md) -## View and Troubleshoot resource provisioning +- `primaryConnectionString` +- `secondaryConnectionString` +- `primaryKey` +- `secondaryKey` +- `sharedaccessKey` +- `eventhubNamespace` +- `eventhubName` -To view your created Eventhub resources, refer to the steps [here](viewresources.md) +## Deploy, view and delete resources -## Help - -1. If the secret for the Eventhub in k8s gets deleted accidentally, the reconcile for the parent eventhub is triggered and secret gets created again. -2. If EventhubNamespace and Eventhub are deleted in Azure, then we need to delete the objects in k8s for the resources to be recreated. Reason being, if we apply the same manifest k8s does it recognise it as a change and the reconcile is not triggered. +You can follow the steps [here](/docs/customresource.md) to deploy, view and delete resources. ## How would you use the Eventhub Operator from a real application diff --git a/docs/keyvault/keyvault.md b/docs/keyvault/keyvault.md index 64a98cec159..dfcac79daa6 100644 --- a/docs/keyvault/keyvault.md +++ b/docs/keyvault/keyvault.md @@ -4,71 +4,21 @@ The Azure Key Vault operator suite consists of the following operators: -- KeyVault - Deploys an Azure Key Vault given the location and resource group -- KeyVaultKey - Deploys an Azure Key Vault key given the location and resource group +- KeyVault - Deploys an Azure Key Vault given the location and resource group +- KeyVaultKey - Deploys an Azure Key Vault key given the location and resource group -## Deploying Key Vault Resources +### KeyVault -You can follow the steps [here](/docs/development.md) to either run the operator locally or in a real Kubernetes cluster. +You can find a sample YAML for KeyVault [here](/config/samples/azure_v1alpha1_keyvault_simple.yaml) -You can use the YAML files in the `config/samples` folder to create the resources. +The value for Kind, `KeyVault` is the Custom Resource Definition (CRD) name. +`Name` is the name of the KeyVault resource that will be created. -### KeyVault +The values under `spec` provide the values for a location where you want to create the Key Vault in, and the Resource Group in which you want to create it under. -For instance, this is the sample YAML for the Azure SQL server: - -```yaml -apiVersion: azure.microsoft.com/v1alpha1 -kind: KeyVault -metadata: - name: keyvaultsample123 - labels: # Provide tags to add to the Key Vault as labels - tag1: value1 - tag2: value2 -spec: - resourceGroup: resourcegroup-azure-operators - location: westus - enableSoftDelete: false - networkPolicies: - bypass: AzureServices # AzureServices or None - defaultAction: Allow # Allow or Deny - ipRules: - - 172.16.0.0/24 - - 172.16.1.0/24 - virtualNetworkRules: - - /subscriptions//resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/subnet1 - - /subscriptions//resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/subnet2 - accessPolicies: - - tenantID: - clientID: - applicationID: - permissions: - keys: # backup create decrypt delete encrypt get import list purge record restore sign unwrapKey update verify wrapKey - - list - - get - secrets: # backup delete get list purge recover restore set - - list - - get - certificates: # backup create delete deleteissuers get getissuers import list listissuers managecontacts manageissuers purge recover restore setissuers update - - list - - get - storage: # backup delete deleteas get getas list listas purge recovver regenratekey restore set setas update - - list - - get - ``` - -The value for kind, `KeyVault` is the Custom Resource Definition (CRD) name. -`keyvaultsample123` is the name of the SQL server resource that will be created. - -The values under `spec` provide the values for a location where you want to create the Key Vault to be created, and the Resource Group in which you want to create it under. - -Once you've updated the YAML with the settings you need, and you have the operator running, you can create a Key Vault resource using the command: - -```bash -kubectl apply -f config/samples/azure_v1alpha1_keyvault.yaml -``` +You can also configure `Access Policies` and `Network policies` for the KeyVault as part of the Spec. A sample YAML with these settings can be found [here](/config/samples/azure_v1alpha1_keyvault.yaml) -### Access Policies +#### Access Policies Key Vault access is governed via a management plane and a data plane. The management plane is where you manage Key Vault resources, and the data plane is where you work with data stored in a Key Vault. @@ -76,6 +26,10 @@ In order to access a Key Vault in either plane, all callers must have proper aut Access control for the two planes work independently. Therefore you will notice that Azure Service Operators account for data plane access via Access Policy administration. Notice the `accessPolicies` yaml entries in the sample above. These are critical entries for properly securing and authorizing Key Vault data plane access. Access Control for the management plane is administered using RBAC that is also controlled by AAD. Therefore, authorization of the management plane is best achieved using Azure Powershell, the Azure CLI, and the Azure Portal. +#### Network Policies + +KeyVault access can be restricted to only certain virtual networks or IP ranges. You can choose to allow access from all Azure services too. Network policies in the Keyvault spec allow you to configure these. + ### KeyVaultKey Operator The KeyVaultKey operator serves as an operator that allows for declarative management of Key Vault keys - one of the three resources available for storage and management in Key Vault; keys, secrets, and certificates. Keys can be leveraged for various use cases. @@ -91,24 +45,6 @@ ALTER COLUMN ENCRYPTION KEY key_name ) [;] ``` -## View and Troubleshoot Key Vault Resources - -You can view your created Key Vault resources using the steps [here](viewresources.md). - -## Delete a Key Vault Resource - -To delete an existing resource from Kubernetes and Azure, use the following command: - -```shell -kubectl delete   -``` - -For instance, deleting the above Key Vault instance would look like this. - -```shell -kubectl delete KeyVaultKey vaultsample123 -``` - -The following message should appear: +## Deploy, view and delete resources -`keyvault.azure.microsoft.com keyvaultsample123 deleted.` \ No newline at end of file +You can follow the steps [here](/docs/customresource.md) to deploy, view and delete resources. diff --git a/docs/postgresql/postgresql.md b/docs/postgresql/postgresql.md index 2f1df81ca4f..661f923cd07 100644 --- a/docs/postgresql/postgresql.md +++ b/docs/postgresql/postgresql.md @@ -75,6 +75,8 @@ This secret contains the following fields. - `password` : Password for the server admin - `fullyqualifiedusername` : Fully qualified user name that is required by some apps such as @ +For more information on where and how secrets are stored, look [here](/docs/secrets.md) + ### PostgreSQL Database Here is the sample YAML for PostgreSQL database @@ -95,26 +97,6 @@ The `server` indicates the PostgreSQL server on which you want to configure the *Note*: When the `startIpAddress` and `endIpAddress` are 0.0.0.0, it is a special case that adds a firewall rule to allow all Azure services to access the SQL server. -## View and Troubleshoot PostgreSQL Resources - -You can view your created PostgreSQL resources using the steps [here](viewresources.md) - -## Delete a PostgreSQL Resource - -To delete an existing resource from Kubernetes and Azure, use the following command. - -```shell -kubectl delete   -``` - -For instance, deleting the above PostgreSqlServer instance would look like this. - -```shell -kubectl delete PostgreSqlServer postgresqlserver-sample -``` +## Deploy, view and delete resources -The following message should appear: - -```shell -postgresqlserver.azure.microsoft.com postgresqlserver-sample deleted. -``` +You can follow the steps [here](/docs/customresource.md) to deploy, view and delete resources. diff --git a/docs/rediscache/rediscache.md b/docs/rediscache/rediscache.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/resourcegroup/resourcegroup.md b/docs/resourcegroup/resourcegroup.md index 871d48215da..3d24162059c 100644 --- a/docs/resourcegroup/resourcegroup.md +++ b/docs/resourcegroup/resourcegroup.md @@ -2,33 +2,6 @@ The Resource group operator can be used to provision a Resource group given the location and a name -## Deploying resources +## Deploy, view and delete resources -You can follow the steps [here](/docs/development.md) to either run the operator locally or in a real Kubernetes cluster. - -You can use the YAML files in the `config/samples` folder to create the resources. - -Here is the sample YAML file for creating a Resource group. - -```yaml -apiVersion: azure.microsoft.com/v1alpha1 -kind: ResourceGroup -metadata: - name: resourcegroup-sample-1907 -spec: - location: "westus" -``` - -This would create a Resource group by the name `resourcegroup-sample-1907` in the `westus` location. - -## View and Troubleshoot resource provisioning - -To view your created resource group, refer to the steps [here](viewresources.md) - -## Delete a Resource Group - -You can delete the resource group using the below command. - -```shell -kubectl delete ResourceGroup -``` +You can follow the steps [here](/docs/customresource.md) to deploy, view and delete resources. diff --git a/docs/secrets.md b/docs/secrets.md new file mode 100644 index 00000000000..335c9c56012 --- /dev/null +++ b/docs/secrets.md @@ -0,0 +1,28 @@ +# Access information of Resource post deployment + +Many of the Azure resources have access information like connection strings, access keys, admin user password etc. that is required by applications consuming the resource. This information is stored as secrets after resource creation. + +The operator provides two options to store these secrets: + +1. **Kubernetes secrets**: This is the default option. If the secretname is not specified in the Spec, a secret with the same name as ObjectMeta.name is created. + +2. **Azure Keyvault secrets**: You can specify the name of the Azure Keyvault to use to store secrets through the environment variable `AZURE_OPERATOR_KEYVAULT`. + +If the secretname is not specified in the Spec, the secret in Keyvault is normally created with the name `-`. The namespace is preprended to the name to avoid name collisions across Kubernetes namespaces. + + +``` +export AZURE_OPERATOR_KEYVAULT=OperatorSecretKeyVault +``` + +Some things to note about this Keyvault: +(i) The Keyvault should have an access policy added for the identity under which the Operator runs as. This access policy should include at least `get`, `set`, `list` and `delete` Secret permissions. + +(ii) You can use a Keyvault with "Soft delete" enabled. However, you cannot use a Keyvault with "Purge Protection" enabled, as this prevents the secrets from being deleted and causes issues if a resource with the same name is re-deployed. + + +## Per resource Keyvault + +In addition to being able to specify an Azure Keyvault to store secrets, you also have the option to specify a different Keyvault per resource. + +Some situations may require that you use a different Keyvault to store the admin password for the Azure SQL server from the Keyvault used to store the connection string for eventhubs. You can specify the Keyvault name in the Spec field `keyVaultToStoreSecrets`. When this is specified, the secrets from provisioning of that resource will be stored in this Keyvault instead of the global one configured for the operator. \ No newline at end of file diff --git a/docs/storage/azuredatalakegen2.md b/docs/storage/azuredatalakegen2.md new file mode 100644 index 00000000000..09eed59c30a --- /dev/null +++ b/docs/storage/azuredatalakegen2.md @@ -0,0 +1,38 @@ +# Azure Data Lake Gen2 Operator + +## Resources Supported + +The Azure Data Lake Gen2 operator can be used to provision the following resources + +1. Azure Data Lake Gen2 enabled storage account - Deploys a data lake gen2 enabled storage account using the storage operator +2. Azure Data Lake Gen2 Filesystem - Deploys a filesystem inside of a data lake gen2 enabled storage account + +### Azure Data Lake Gen 2 Enabled Storage Account + +A sample YAML for the Data Lake Gen2 FileSystem is [here](/config/samples/azure_v1alpha1_azuredatalakegen2storage.yaml) + +Important Values: + +- Kind: the Custom Resource Definition (CRD) name +- Metadata.Name: the name of the data lake enabled storage account that will be created +- Spec.Location: Azure region where you want to create the data lake enabled storage account +- Spec.ResourceGroup: Name of the resource group under which you want to create the data lake enabled storage account +- Spec.Kind: The kind of storage account that you want to create. This value must be `StorageV2` in order for it to be a data lake +- Spec.AccessTier - The access tier for the storage account. Choose "Hot" access tier for frequently accessed data and "Cool" for infrequently accessed data +- Spec.SupportsHttpsTrafficOnly - When "enabled", requires connection requests to the storage account to be only over HTTPS +- Spec.DataLakeEnabled: If set to `true`, the storage account will have hierarchical namespace enabled and will, therefore, be a data lake enabled storage account + +### Azure Data Lake Gen 2 FileSystem + +A sample YAML for the Data Lake Gen2 FileSystem is [here](/config/samples/azure_v1alpha1_azuredatalakegen2filesystem.yaml) + +Important Values: + +- Kind: the Custom Resource Definition (CRD) name +- Metadata.Name: the name of the data lake enabled storage account that will be created +- Spec.StorageAccountName: Name of the data lake enabled storage account in which you would like to create the filesystem +- Spec.ResourceGroup: Name of the resource group under which the storage account lives + +## Deploy, view and delete resources + +You can follow the steps [here](/docs/customresource.md) to deploy, view and delete resources. diff --git a/docs/storage/blobcontainer.md b/docs/storage/blobcontainer.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/storage/storageaccount.md b/docs/storage/storageaccount.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/virtualnetwork/virtualnetwork.md b/docs/virtualnetwork/virtualnetwork.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/main.go b/main.go index 4416565e4b3..cc055a06bc1 100644 --- a/main.go +++ b/main.go @@ -112,7 +112,6 @@ func main() { resourceGroupManager := resourcemanagerresourcegroup.NewAzureResourceGroupManager() redisCacheManager := resourcemanagerrediscache.NewAzureRedisCacheManager( - ctrl.Log.WithName("rediscachemanager").WithName("RedisCache"), secretClient, scheme, ) diff --git a/pkg/errhelp/errors.go b/pkg/errhelp/errors.go index 0932fde692a..3c76c46791e 100644 --- a/pkg/errhelp/errors.go +++ b/pkg/errhelp/errors.go @@ -49,6 +49,7 @@ const ( NotSupported = "NotSupported" SecretNotFound = "SecretNotFound" RequestDisallowedByPolicy = "RequestDisallowedByPolicy" + ServiceBusy = "ServiceBusy" ) func NewAzureError(err error) error { diff --git a/pkg/resourcemanager/azuresql/azuresqluser/azuresqluser.go b/pkg/resourcemanager/azuresql/azuresqluser/azuresqluser.go index dc7eeb1f907..0741203cf91 100644 --- a/pkg/resourcemanager/azuresql/azuresqluser/azuresqluser.go +++ b/pkg/resourcemanager/azuresql/azuresqluser/azuresqluser.go @@ -221,11 +221,11 @@ func (s *AzureSqlUserManager) Ensure(ctx context.Context, obj runtime.Object, op DBSecret[SecretUsernameKey] = []byte(user) } - // publish user secret + // 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 // 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 + // In keyvault we have to avoid collisions with other secrets so we create a custom namespace with the user's parameters key = types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace} var dbUserCustomNamespace string @@ -536,12 +536,12 @@ func (s *AzureSqlUserManager) convert(obj runtime.Object) (*v1alpha1.AzureSQLUse func (s *AzureSqlUserManager) DeleteSecrets(ctx context.Context, instance *v1alpha1.AzureSQLUser, 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 - DBSecret := s.GetOrPrepareSecret(ctx, instance, s.SecretClient) + DBSecret := s.GetOrPrepareSecret(ctx, instance, secretClient) secretKey := types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace} var dbUserCustomNamespace string - keyVaultEnabled := reflect.TypeOf(s.SecretClient).Elem().Name() == "KeyvaultSecretClient" + keyVaultEnabled := reflect.TypeOf(secretClient).Elem().Name() == "KeyvaultSecretClient" if keyVaultEnabled { // For a keyvault secret store, check for supplied namespace parameters @@ -555,7 +555,7 @@ func (s *AzureSqlUserManager) DeleteSecrets(ctx context.Context, instance *v1alp } // delete standard user secret - err := s.SecretClient.Delete( + err := secretClient.Delete( ctx, secretKey, ) @@ -580,10 +580,16 @@ func (s *AzureSqlUserManager) DeleteSecrets(ctx context.Context, instance *v1alp } for _, formatName := range customFormatNames { - err = s.SecretClient.Delete( + key := types.NamespacedName{Namespace: dbUserCustomNamespace, Name: instance.Name + "-" + formatName} + + err = secretClient.Delete( ctx, - types.NamespacedName{Namespace: dbUserCustomNamespace, Name: instance.Name + "-" + formatName}, + key, ) + if err != nil { + instance.Status.Message = "failed to delete secret, err: " + err.Error() + return false, err + } } } diff --git a/pkg/resourcemanager/keyvaults/keyvault.go b/pkg/resourcemanager/keyvaults/keyvault.go index bb6eb0a0e84..200fd19e707 100644 --- a/pkg/resourcemanager/keyvaults/keyvault.go +++ b/pkg/resourcemanager/keyvaults/keyvault.go @@ -6,6 +6,7 @@ package keyvaults import ( "context" "fmt" + "strings" auth "github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac" "github.com/Azure/azure-sdk-for-go/services/keyvault/mgmt/2018-02-14/keyvault" @@ -247,7 +248,7 @@ func InstantiateVault(ctx context.Context, vaultName string, containsUpdate bool } // CreateVault creates a new key vault -func (k *azureKeyVaultManager) CreateVault(ctx context.Context, instance *v1alpha1.KeyVault, tags map[string]*string) (keyvault.Vault, error) { +func (k *azureKeyVaultManager) CreateVault(ctx context.Context, instance *v1alpha1.KeyVault, sku azurev1alpha1.KeyVaultSku, tags map[string]*string) (keyvault.Vault, error) { vaultName := instance.Name location := instance.Spec.Location groupName := instance.Spec.ResourceGroup @@ -278,14 +279,20 @@ func (k *azureKeyVaultManager) CreateVault(ctx context.Context, instance *v1alph networkAcls = keyvault.NetworkRuleSet{} } + keyVaultSku := keyvault.Sku{ + Family: to.StringPtr("A"), + Name: keyvault.Standard, + } + + if strings.ToLower(sku.Name) == "premium" { + keyVaultSku.Name = keyvault.Premium + } + params := keyvault.VaultCreateOrUpdateParameters{ Properties: &keyvault.VaultProperties{ - TenantID: &id, - AccessPolicies: &accessPolicies, - Sku: &keyvault.Sku{ - Family: to.StringPtr("A"), - Name: keyvault.Standard, - }, + TenantID: &id, + AccessPolicies: &accessPolicies, + Sku: &keyVaultSku, NetworkAcls: &networkAcls, EnableSoftDelete: &enableSoftDelete, }, @@ -298,7 +305,7 @@ func (k *azureKeyVaultManager) CreateVault(ctx context.Context, instance *v1alph return future.Result(vaultsClient) } -// CreateVaultWithAccessPolicies creates a new key vault and provides access policies to the specified user +//CreateVaultWithAccessPolicies creates a new key vault and provides access policies to the specified user func (k *azureKeyVaultManager) CreateVaultWithAccessPolicies(ctx context.Context, groupName string, vaultName string, location string, clientID string) (keyvault.Vault, error) { vaultsClient, id, err := InstantiateVault(ctx, vaultName, false) if err != nil { @@ -406,6 +413,7 @@ func (k *azureKeyVaultManager) Ensure(ctx context.Context, obj runtime.Object, o keyvault, err = k.CreateVault( ctx, instance, + instance.Spec.Sku, labels, ) diff --git a/pkg/resourcemanager/keyvaults/keyvault_manager.go b/pkg/resourcemanager/keyvaults/keyvault_manager.go index c1f23f948b5..b5681d395cb 100644 --- a/pkg/resourcemanager/keyvaults/keyvault_manager.go +++ b/pkg/resourcemanager/keyvaults/keyvault_manager.go @@ -16,7 +16,7 @@ import ( var AzureKeyVaultManager KeyVaultManager = &azureKeyVaultManager{} type KeyVaultManager interface { - CreateVault(ctx context.Context, instance *azurev1alpha1.KeyVault, tags map[string]*string) (keyvault.Vault, error) + CreateVault(ctx context.Context, instance *azurev1alpha1.KeyVault, sku azurev1alpha1.KeyVaultSku, tags map[string]*string) (keyvault.Vault, error) // CreateVault and grant access to the specific user ID CreateVaultWithAccessPolicies(ctx context.Context, groupName string, vaultName string, location string, userID string) (keyvault.Vault, error) diff --git a/pkg/resourcemanager/keyvaults/keyvault_test.go b/pkg/resourcemanager/keyvaults/keyvault_test.go index 32475d76ceb..56b7be06ac1 100644 --- a/pkg/resourcemanager/keyvaults/keyvault_test.go +++ b/pkg/resourcemanager/keyvaults/keyvault_test.go @@ -60,11 +60,16 @@ var _ = Describe("KeyVault Resource Manager test", func() { }, } + sku := v1alpha1.KeyVaultSku{ + Name: "Standard", + } + // Create Key Vault instance Eventually(func() bool { _, err := keyVaultManager.CreateVault( ctx, &kv, + sku, tags, ) if err != nil { diff --git a/pkg/resourcemanager/psql/database/client.go b/pkg/resourcemanager/psql/database/client.go deleted file mode 100644 index 553196d5612..00000000000 --- a/pkg/resourcemanager/psql/database/client.go +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -package database - -import ( - "context" - "fmt" - - psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" - "github.com/Azure/azure-service-operator/api/v1alpha1" - azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" - "github.com/Azure/azure-service-operator/pkg/errhelp" - "github.com/Azure/azure-service-operator/pkg/helpers" - "github.com/Azure/azure-service-operator/pkg/resourcemanager" - "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" - "github.com/Azure/azure-service-operator/pkg/resourcemanager/iam" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" -) - -type PSQLDatabaseClient struct { -} - -func NewPSQLDatabaseClient() *PSQLDatabaseClient { - return &PSQLDatabaseClient{} -} - -func getPSQLDatabasesClient() psql.DatabasesClient { - databasesClient := psql.NewDatabasesClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) - a, _ := iam.GetResourceManagementAuthorizer() - databasesClient.Authorizer = a - databasesClient.AddToUserAgent(config.UserAgent()) - return databasesClient -} - -func getPSQLCheckNameAvailabilityClient() psql.CheckNameAvailabilityClient { - nameavailabilityClient := psql.NewCheckNameAvailabilityClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) - a, _ := iam.GetResourceManagementAuthorizer() - nameavailabilityClient.Authorizer = a - nameavailabilityClient.AddToUserAgent(config.UserAgent()) - return nameavailabilityClient -} - -func (p *PSQLDatabaseClient) CheckDatabaseNameAvailability(ctx context.Context, databasename string) (bool, error) { - - client := getPSQLCheckNameAvailabilityClient() - - resourceType := "database" - - nameAvailabilityRequest := psql.NameAvailabilityRequest{ - Name: &databasename, - Type: &resourceType, - } - _, err := client.Execute(ctx, nameAvailabilityRequest) - if err == nil { // Name available - return true, nil - } - return false, err - -} -func (p *PSQLDatabaseClient) Ensure(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { - - instance, err := p.convert(obj) - if err != nil { - return true, err - } - - client := getPSQLDatabasesClient() - - instance.Status.Provisioning = true - // Check if this database already exists. This is required - // to overcome the issue with the lack of idempotence of the Create call - - _, err = p.GetDatabase(ctx, instance.Spec.ResourceGroup, instance.Spec.Server, instance.Name) - if err == nil { - instance.Status.Provisioned = true - instance.Status.Provisioning = false - instance.Status.Message = resourcemanager.SuccessMsg - return true, nil - } - future, err := p.CreateDatabaseIfValid( - ctx, - instance.Name, - instance.Spec.Server, - instance.Spec.ResourceGroup, - ) - - if err != nil { - // let the user know what happened - instance.Status.Message = err.Error() - instance.Status.Provisioning = false - // errors we expect might happen that we are ok with waiting for - catch := []string{ - errhelp.ResourceGroupNotFoundErrorCode, - errhelp.ParentNotFoundErrorCode, - errhelp.NotFoundErrorCode, - errhelp.AsyncOpIncompleteError, - errhelp.ResourceNotFound, - } - - azerr := errhelp.NewAzureErrorAzureError(err) - if helpers.ContainsString(catch, azerr.Type) { - // most of these error technically mean the resource is actually not provisioning - switch azerr.Type { - case errhelp.AsyncOpIncompleteError: - instance.Status.Provisioning = true - } - // reconciliation is not done but error is acceptable - return false, nil - } - // reconciliation not done and we don't know what happened - return false, err - } - - _, err = future.Result(client) - if err != nil { - // let the user know what happened - instance.Status.Message = err.Error() - instance.Status.Provisioning = false - // errors we expect might happen that we are ok with waiting for - catch := []string{ - errhelp.ResourceGroupNotFoundErrorCode, - errhelp.ParentNotFoundErrorCode, - errhelp.NotFoundErrorCode, - errhelp.AsyncOpIncompleteError, - errhelp.SubscriptionDoesNotHaveServer, - } - - azerr := errhelp.NewAzureErrorAzureError(err) - if helpers.ContainsString(catch, azerr.Type) { - // most of these error technically mean the resource is actually not provisioning - switch azerr.Type { - case errhelp.AsyncOpIncompleteError: - instance.Status.Provisioning = true - case errhelp.SubscriptionDoesNotHaveServer: - instance.Status.Message = fmt.Sprintf("The PostgreSQL Server %s has not been provisioned yet. ", instance.Spec.Server) - } - // reconciliation is not done but error is acceptable - return false, nil - } - // reconciliation not done and we don't know what happened - return false, err - } - - if instance.Status.Provisioning { - instance.Status.Provisioned = true - instance.Status.Provisioning = false - instance.Status.Message = resourcemanager.SuccessMsg - } else { - instance.Status.Provisioned = false - instance.Status.Provisioning = true - } - - return true, nil -} - -func (p *PSQLDatabaseClient) Delete(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { - - instance, err := p.convert(obj) - if err != nil { - return true, err - } - - status, err := p.DeleteDatabase(ctx, instance.Name, instance.Spec.Server, instance.Spec.ResourceGroup) - if err != nil { - if !errhelp.IsAsynchronousOperationNotComplete(err) { - return true, err - } - } - - if err == nil { - if status != "InProgress" { - return false, nil - } - } - - return true, nil -} - -func (p *PSQLDatabaseClient) GetParents(obj runtime.Object) ([]resourcemanager.KubeParent, error) { - - instance, err := p.convert(obj) - if err != nil { - return nil, err - } - - return []resourcemanager.KubeParent{ - { - Key: types.NamespacedName{ - Namespace: instance.Namespace, - Name: instance.Spec.Server, - }, - Target: &azurev1alpha1.PostgreSQLServer{}, - }, - { - Key: types.NamespacedName{ - Namespace: instance.Namespace, - Name: instance.Spec.ResourceGroup, - }, - Target: &azurev1alpha1.ResourceGroup{}, - }, - }, nil -} - -func (g *PSQLDatabaseClient) GetStatus(obj runtime.Object) (*v1alpha1.ASOStatus, error) { - instance, err := g.convert(obj) - if err != nil { - return nil, err - } - return &instance.Status, nil -} - -func (p *PSQLDatabaseClient) convert(obj runtime.Object) (*v1alpha1.PostgreSQLDatabase, error) { - local, ok := obj.(*v1alpha1.PostgreSQLDatabase) - if !ok { - return nil, fmt.Errorf("failed type assertion on kind: %s", obj.GetObjectKind().GroupVersionKind().String()) - } - return local, nil -} - -func (p *PSQLDatabaseClient) CreateDatabaseIfValid(ctx context.Context, databasename string, servername string, resourcegroup string) (future psql.DatabasesCreateOrUpdateFuture, err error) { - - client := getPSQLDatabasesClient() - - // Check if name is valid if this is the first create call - valid, err := p.CheckDatabaseNameAvailability(ctx, databasename) - if valid == false { - return future, err - } - - dbParameters := psql.Database{} - - future, err = client.CreateOrUpdate( - ctx, - resourcegroup, - servername, - databasename, - dbParameters, - ) - - return future, err -} - -func (p *PSQLDatabaseClient) DeleteDatabase(ctx context.Context, databasename string, servername string, resourcegroup string) (status string, err error) { - - client := getPSQLDatabasesClient() - - _, err = client.Get(ctx, resourcegroup, servername, databasename) - if err == nil { // db present, so go ahead and delete - future, err := client.Delete(ctx, resourcegroup, servername, databasename) - return future.Status(), err - } - // db not present so return success anyway - return "db not present", nil - -} - -func (p *PSQLDatabaseClient) GetDatabase(ctx context.Context, resourcegroup string, servername string, databasename string) (db psql.Database, err error) { - - client := getPSQLDatabasesClient() - - return client.Get(ctx, resourcegroup, servername, databasename) -} diff --git a/pkg/resourcemanager/psql/database/database.go b/pkg/resourcemanager/psql/database/database.go new file mode 100644 index 00000000000..5f39c108ae8 --- /dev/null +++ b/pkg/resourcemanager/psql/database/database.go @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package database + +import ( + "context" + "net/http" + + psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/iam" +) + +type PSQLDatabaseClient struct { +} + +func NewPSQLDatabaseClient() *PSQLDatabaseClient { + return &PSQLDatabaseClient{} +} + +func getPSQLDatabasesClient() (psql.DatabasesClient, error) { + databasesClient := psql.NewDatabasesClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) + a, err := iam.GetResourceManagementAuthorizer() + if err != nil { + return psql.DatabasesClient{}, err + } + databasesClient.Authorizer = a + databasesClient.AddToUserAgent(config.UserAgent()) + return databasesClient, err +} + +func getPSQLCheckNameAvailabilityClient() (psql.CheckNameAvailabilityClient, error) { + nameavailabilityClient := psql.NewCheckNameAvailabilityClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) + a, err := iam.GetResourceManagementAuthorizer() + if err != nil { + return psql.CheckNameAvailabilityClient{}, err + } + nameavailabilityClient.Authorizer = a + nameavailabilityClient.AddToUserAgent(config.UserAgent()) + return nameavailabilityClient, err +} + +func (p *PSQLDatabaseClient) CheckDatabaseNameAvailability(ctx context.Context, databasename string) (bool, error) { + + client, err := getPSQLCheckNameAvailabilityClient() + if err != nil { + return false, err + } + + resourceType := "Microsoft.DBforPostgreSQL/servers/databases" + + nameAvailabilityRequest := psql.NameAvailabilityRequest{ + Name: &databasename, + Type: &resourceType, + } + _, err = client.Execute(ctx, nameAvailabilityRequest) + if err == nil { // Name available + return true, nil + } + return false, err + +} + +func (p *PSQLDatabaseClient) CreateDatabaseIfValid(ctx context.Context, databasename string, servername string, resourcegroup string) (*http.Response, error) { + + client, err := getPSQLDatabasesClient() + if err != nil { + return &http.Response{ + StatusCode: 500, + }, err + } + + // Check if name is valid if this is the first create call + valid, err := p.CheckDatabaseNameAvailability(ctx, databasename) + if valid == false { + return &http.Response{ + StatusCode: 500, + }, err + } + + dbParameters := psql.Database{} + + future, err := client.CreateOrUpdate( + ctx, + resourcegroup, + servername, + databasename, + dbParameters, + ) + if err != nil { + return &http.Response{ + StatusCode: 500, + }, err + } + + return future.GetResult(client) +} + +func (p *PSQLDatabaseClient) DeleteDatabase(ctx context.Context, databasename string, servername string, resourcegroup string) (status string, err error) { + + client, err := getPSQLDatabasesClient() + if err != nil { + return "", err + } + + _, err = client.Get(ctx, resourcegroup, servername, databasename) + if err == nil { // db present, so go ahead and delete + future, err := client.Delete(ctx, resourcegroup, servername, databasename) + return future.Status(), err + } + + // db not present so return success anyway + return "db not present", nil +} + +func (p *PSQLDatabaseClient) GetDatabase(ctx context.Context, resourcegroup string, servername string, databasename string) (db psql.Database, err error) { + + client, err := getPSQLDatabasesClient() + if err != nil { + return psql.Database{}, err + } + + return client.Get(ctx, resourcegroup, servername, databasename) +} diff --git a/pkg/resourcemanager/psql/database/database_manager.go b/pkg/resourcemanager/psql/database/database_manager.go new file mode 100644 index 00000000000..b09e24231a1 --- /dev/null +++ b/pkg/resourcemanager/psql/database/database_manager.go @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package database + +import ( + "context" + "net/http" + + psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" + "github.com/Azure/azure-service-operator/pkg/resourcemanager" +) + +type PostgreSQLDatabaseManager interface { + CheckDatabaseNameAvailability(ctx context.Context, + databasename string) (bool, error) + + CreateDatabaseIfValid(ctx context.Context, + databasename string, + servername string, + resourcegroup string) (*http.Response, error) + + DeleteDatabase(ctx context.Context, + databasename string, + servername string, + resourcegroup string) (string, error) + + GetDatabase(ctx context.Context, + resourcegroup string, + servername string, + database string) (psql.Database, error) + + // also embed async client methods + resourcemanager.ARMClient +} diff --git a/pkg/resourcemanager/psql/database/database_reconcile.go b/pkg/resourcemanager/psql/database/database_reconcile.go new file mode 100644 index 00000000000..f93d4f6d100 --- /dev/null +++ b/pkg/resourcemanager/psql/database/database_reconcile.go @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package database + +import ( + "context" + "fmt" + + "github.com/Azure/azure-service-operator/api/v1alpha1" + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/pkg/errhelp" + "github.com/Azure/azure-service-operator/pkg/helpers" + "github.com/Azure/azure-service-operator/pkg/resourcemanager" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +// Ensure ensures a Postgres database exists +func (p *PSQLDatabaseClient) Ensure(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + + instance, err := p.convert(obj) + if err != nil { + return true, err + } + + getDB, err := p.GetDatabase(ctx, instance.Spec.ResourceGroup, instance.Spec.Server, instance.Name) + if err == nil { + + // succeeded! so end reconcilliation successfully + instance.Status.Message = resourcemanager.SuccessMsg + instance.Status.ResourceId = *getDB.ID + instance.Status.State = getDB.Status + instance.Status.Provisioned = true + instance.Status.Provisioning = false + return true, nil + } + + instance.Status.Provisioning = true + resp, err := p.CreateDatabaseIfValid( + ctx, + instance.Name, + instance.Spec.Server, + instance.Spec.ResourceGroup, + ) + if err != nil { + instance.Status.Message = errhelp.StripErrorIDs(err) + azerr := errhelp.NewAzureErrorAzureError(err) + + catchInProgress := []string{ + errhelp.AsyncOpIncompleteError, + errhelp.AlreadyExists, + } + catchKnownError := []string{ + errhelp.ResourceGroupNotFoundErrorCode, + errhelp.ParentNotFoundErrorCode, + errhelp.NotFoundErrorCode, + errhelp.ResourceNotFound, + } + + // assertion that a 404 error implies that the Postgres server hasn't been provisioned yet + if resp != nil && resp.StatusCode == 404 { + instance.Status.Message = fmt.Sprintf("Waiting for Postgres server %s to provision", instance.Spec.Server) + instance.Status.Provisioning = false + return false, nil + } + + // handle the errors + if helpers.ContainsString(catchInProgress, azerr.Type) { + instance.Status.Message = "Postgres database exists but may not be ready" + return false, nil + } else if helpers.ContainsString(catchKnownError, azerr.Type) { + instance.Status.Provisioning = false + return false, nil + } else { + + // serious error occured, end reconcilliation and mark it as failed + instance.Status.Message = fmt.Sprintf("Error occurred creating the Postgres server: %s", errhelp.StripErrorIDs(err)) + instance.Status.Provisioned = false + instance.Status.Provisioning = false + instance.Status.FailedProvisioning = true + return true, nil + } + } + + return false, nil +} + +// Delete removes the Postgres database +func (p *PSQLDatabaseClient) Delete(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + + instance, err := p.convert(obj) + if err != nil { + return true, err + } + + status, err := p.DeleteDatabase(ctx, instance.Name, instance.Spec.Server, instance.Spec.ResourceGroup) + if err != nil { + if !errhelp.IsAsynchronousOperationNotComplete(err) { + return true, err + } + } + + if err == nil { + if status != "InProgress" { + return false, nil + } + } + + return true, nil +} + +// GetParents gets the database's parents +func (p *PSQLDatabaseClient) GetParents(obj runtime.Object) ([]resourcemanager.KubeParent, error) { + + instance, err := p.convert(obj) + if err != nil { + return nil, err + } + + return []resourcemanager.KubeParent{ + { + Key: types.NamespacedName{ + Namespace: instance.Namespace, + Name: instance.Spec.Server, + }, + Target: &azurev1alpha1.PostgreSQLServer{}, + }, + { + Key: types.NamespacedName{ + Namespace: instance.Namespace, + Name: instance.Spec.ResourceGroup, + }, + Target: &azurev1alpha1.ResourceGroup{}, + }, + }, nil +} + +// GetStatus gets the status +func (p *PSQLDatabaseClient) GetStatus(obj runtime.Object) (*v1alpha1.ASOStatus, error) { + instance, err := p.convert(obj) + if err != nil { + return nil, err + } + return &instance.Status, nil +} + +func (p *PSQLDatabaseClient) convert(obj runtime.Object) (*v1alpha1.PostgreSQLDatabase, error) { + local, ok := obj.(*v1alpha1.PostgreSQLDatabase) + if !ok { + return nil, fmt.Errorf("failed type assertion on kind: %s", obj.GetObjectKind().GroupVersionKind().String()) + } + return local, nil +} diff --git a/pkg/resourcemanager/psql/database/manager.go b/pkg/resourcemanager/psql/database/manager.go deleted file mode 100644 index a418007c380..00000000000 --- a/pkg/resourcemanager/psql/database/manager.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -package database - -import ( - "context" - - psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" - "github.com/Azure/azure-service-operator/pkg/resourcemanager" -) - -type PostgreSQLDatabaseManager interface { - //convert(obj runtime.Object) (*v1alpha1.PostgreSQLDatabase, error) - - //CheckDatabaseNameAvailability(ctx context.Context, databasename string) (bool, error) - CreateDatabaseIfValid(ctx context.Context, databasename string, servername string, resourcegroup string) (psql.DatabasesCreateOrUpdateFuture, error) - DeleteDatabase(ctx context.Context, databasename string, servername string, resourcegroup string) (string, error) - GetDatabase(ctx context.Context, resourcegroup string, servername string, database string) (psql.Database, error) - // also embed async client methods - resourcemanager.ARMClient -} diff --git a/pkg/resourcemanager/psql/firewallrule/client.go b/pkg/resourcemanager/psql/firewallrule/client.go deleted file mode 100644 index 3c2148b1ed7..00000000000 --- a/pkg/resourcemanager/psql/firewallrule/client.go +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -package server - -import ( - "context" - "fmt" - - psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" - "github.com/Azure/azure-service-operator/api/v1alpha1" - azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" - "github.com/Azure/azure-service-operator/pkg/errhelp" - "github.com/Azure/azure-service-operator/pkg/helpers" - "github.com/Azure/azure-service-operator/pkg/resourcemanager" - "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" - "github.com/Azure/azure-service-operator/pkg/resourcemanager/iam" - "github.com/Azure/go-autorest/autorest/to" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" -) - -type PSQLFirewallRuleClient struct { -} - -func NewPSQLFirewallRuleClient() *PSQLFirewallRuleClient { - return &PSQLFirewallRuleClient{} -} - -func getPSQLFirewallRulesClient() psql.FirewallRulesClient { - firewallRulesClient := psql.NewFirewallRulesClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) - a, _ := iam.GetResourceManagementAuthorizer() - firewallRulesClient.Authorizer = a - firewallRulesClient.AddToUserAgent(config.UserAgent()) - return firewallRulesClient -} - -func (p *PSQLFirewallRuleClient) Ensure(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { - instance, err := p.convert(obj) - if err != nil { - return true, err - } - - client := getPSQLFirewallRulesClient() - - instance.Status.Provisioning = true - // 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 - - firewallrule, err := p.GetFirewallRule(ctx, instance.Spec.ResourceGroup, instance.Spec.Server, instance.Name) - if err == nil { - instance.Status.Provisioned = true - instance.Status.Provisioning = false - instance.Status.State = firewallrule.Status - return true, nil - } - - future, err := p.CreateFirewallRule( - ctx, - instance.Spec.ResourceGroup, - instance.Spec.Server, - instance.Name, - instance.Spec.StartIPAddress, - instance.Spec.EndIPAddress, - ) - - if err != nil { - // let the user know what happened - instance.Status.Message = err.Error() - instance.Status.Provisioning = false - // errors we expect might happen that we are ok with waiting for - catch := []string{ - errhelp.ResourceGroupNotFoundErrorCode, - errhelp.ParentNotFoundErrorCode, - errhelp.NotFoundErrorCode, - errhelp.AsyncOpIncompleteError, - } - - azerr := errhelp.NewAzureErrorAzureError(err) - if helpers.ContainsString(catch, azerr.Type) { - // most of these error technically mean the resource is actually not provisioning - switch azerr.Type { - case errhelp.AsyncOpIncompleteError: - instance.Status.Provisioning = true - } - // reconciliation is not done but error is acceptable - return false, nil - } - // reconciliation not done and we don't know what happened - return false, err - } - - instance.Status.State = future.Status() - - firewallrule, err = future.Result(client) - if err != nil { - // let the user know what happened - instance.Status.Message = err.Error() - instance.Status.Provisioning = false - // errors we expect might happen that we are ok with waiting for - catch := []string{ - errhelp.ResourceGroupNotFoundErrorCode, - errhelp.ParentNotFoundErrorCode, - errhelp.NotFoundErrorCode, - errhelp.AsyncOpIncompleteError, - } - - azerr := errhelp.NewAzureErrorAzureError(err) - if helpers.ContainsString(catch, azerr.Type) { - // most of these error technically mean the resource is actually not provisioning - switch azerr.Type { - case errhelp.AsyncOpIncompleteError: - instance.Status.Provisioning = true - } - // reconciliation is not done but error is acceptable - return false, nil - } - // reconciliation not done and we don't know what happened - return false, err - } - - instance.Status.State = firewallrule.Status - - if instance.Status.Provisioning { - instance.Status.Provisioned = true - instance.Status.Provisioning = false - instance.Status.Message = resourcemanager.SuccessMsg - } else { - instance.Status.Provisioned = false - instance.Status.Provisioning = true - } - - return true, nil -} - -func (p *PSQLFirewallRuleClient) Delete(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { - instance, err := p.convert(obj) - if err != nil { - return true, err - } - - status, err := p.DeleteFirewallRule(ctx, instance.Spec.ResourceGroup, instance.Spec.Server, instance.Name) - if err != nil { - if !errhelp.IsAsynchronousOperationNotComplete(err) { - return true, err - } - } - instance.Status.State = status - - if err == nil { - if status != "InProgress" { - return false, nil - } - } - - return true, nil -} - -func (p *PSQLFirewallRuleClient) GetParents(obj runtime.Object) ([]resourcemanager.KubeParent, error) { - - instance, err := p.convert(obj) - if err != nil { - return nil, err - } - - return []resourcemanager.KubeParent{ - { - Key: types.NamespacedName{ - Namespace: instance.Namespace, - Name: instance.Spec.Server, - }, - Target: &azurev1alpha1.PostgreSQLServer{}, - }, - { - Key: types.NamespacedName{ - Namespace: instance.Namespace, - Name: instance.Spec.ResourceGroup, - }, - Target: &azurev1alpha1.ResourceGroup{}, - }, - }, nil -} - -func (g *PSQLFirewallRuleClient) GetStatus(obj runtime.Object) (*v1alpha1.ASOStatus, error) { - instance, err := g.convert(obj) - if err != nil { - return nil, err - } - return &instance.Status, nil -} - -func (p *PSQLFirewallRuleClient) convert(obj runtime.Object) (*v1alpha1.PostgreSQLFirewallRule, error) { - local, ok := obj.(*v1alpha1.PostgreSQLFirewallRule) - if !ok { - return nil, fmt.Errorf("failed type assertion on kind: %s", obj.GetObjectKind().GroupVersionKind().String()) - } - return local, nil -} - -func (p *PSQLFirewallRuleClient) CreateFirewallRule(ctx context.Context, resourcegroup string, servername string, firewallrulename string, startip string, endip string) (future psql.FirewallRulesCreateOrUpdateFuture, err error) { - - client := getPSQLFirewallRulesClient() - - firewallRuleProperties := psql.FirewallRuleProperties{ - StartIPAddress: to.StringPtr(startip), - EndIPAddress: to.StringPtr(endip), - } - - future, err = client.CreateOrUpdate( - ctx, - resourcegroup, - servername, - firewallrulename, - psql.FirewallRule{ - FirewallRuleProperties: &firewallRuleProperties, - }, - ) - return future, err -} - -func (p *PSQLFirewallRuleClient) DeleteFirewallRule(ctx context.Context, resourcegroup string, servername string, firewallrulename string) (status string, err error) { - - client := getPSQLFirewallRulesClient() - - _, err = client.Get(ctx, resourcegroup, servername, firewallrulename) - if err == nil { // FW rule present, so go ahead and delete - future, err := client.Delete(ctx, resourcegroup, servername, firewallrulename) - return future.Status(), err - } - // FW rule not present so return success anyway - return "Firewall Rule not present", nil - -} - -func (p *PSQLFirewallRuleClient) GetFirewallRule(ctx context.Context, resourcegroup string, servername string, firewallrulename string) (firewall psql.FirewallRule, err error) { - - client := getPSQLFirewallRulesClient() - - return client.Get(ctx, resourcegroup, servername, firewallrulename) -} diff --git a/pkg/resourcemanager/psql/firewallrule/firewallrule.go b/pkg/resourcemanager/psql/firewallrule/firewallrule.go new file mode 100644 index 00000000000..bddd1e9e68f --- /dev/null +++ b/pkg/resourcemanager/psql/firewallrule/firewallrule.go @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package server + +import ( + "context" + "net/http" + + psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/iam" + "github.com/Azure/go-autorest/autorest/to" +) + +type PSQLFirewallRuleClient struct { +} + +func NewPSQLFirewallRuleClient() *PSQLFirewallRuleClient { + return &PSQLFirewallRuleClient{} +} + +func getPSQLFirewallRulesClient() (psql.FirewallRulesClient, error) { + firewallRulesClient := psql.NewFirewallRulesClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) + a, err := iam.GetResourceManagementAuthorizer() + if err != nil { + return psql.FirewallRulesClient{}, err + } + firewallRulesClient.Authorizer = a + firewallRulesClient.AddToUserAgent(config.UserAgent()) + return firewallRulesClient, err +} + +func (p *PSQLFirewallRuleClient) CreateFirewallRule(ctx context.Context, resourcegroup string, servername string, firewallrulename string, startip string, endip string) (*http.Response, error) { + + client, err := getPSQLFirewallRulesClient() + if err != nil { + return &http.Response{ + StatusCode: 500, + }, err + } + + firewallRuleProperties := psql.FirewallRuleProperties{ + StartIPAddress: to.StringPtr(startip), + EndIPAddress: to.StringPtr(endip), + } + + future, err := client.CreateOrUpdate( + ctx, + resourcegroup, + servername, + firewallrulename, + psql.FirewallRule{ + FirewallRuleProperties: &firewallRuleProperties, + }, + ) + if err != nil { + return &http.Response{ + StatusCode: 500, + }, err + } + + return future.GetResult(client) +} + +func (p *PSQLFirewallRuleClient) DeleteFirewallRule(ctx context.Context, resourcegroup string, servername string, firewallrulename string) (status string, err error) { + + client, err := getPSQLFirewallRulesClient() + if err != nil { + return "", err + } + + _, err = client.Get(ctx, resourcegroup, servername, firewallrulename) + if err == nil { // FW rule present, so go ahead and delete + future, err := client.Delete(ctx, resourcegroup, servername, firewallrulename) + return future.Status(), err + } + + // FW rule not present so return success anyway + return "Firewall Rule not present", nil +} + +func (p *PSQLFirewallRuleClient) GetFirewallRule(ctx context.Context, resourcegroup string, servername string, firewallrulename string) (firewall psql.FirewallRule, err error) { + + client, err := getPSQLFirewallRulesClient() + if err != nil { + return psql.FirewallRule{}, err + } + + return client.Get(ctx, resourcegroup, servername, firewallrulename) +} diff --git a/pkg/resourcemanager/psql/firewallrule/firewallrule_manager.go b/pkg/resourcemanager/psql/firewallrule/firewallrule_manager.go new file mode 100644 index 00000000000..305b9f7293e --- /dev/null +++ b/pkg/resourcemanager/psql/firewallrule/firewallrule_manager.go @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package server + +import ( + "context" + "net/http" + + psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" + "github.com/Azure/azure-service-operator/pkg/resourcemanager" +) + +type PostgreSQLFirewallRuleManager interface { + CreateFirewallRule(ctx context.Context, + resourcegroup string, + servername string, + firewallrulename string, + startip string, + endip string) (*http.Response, error) + + GetFirewallRule(ctx context.Context, + resourcegroup string, + servername string, + firewallrulename string) (psql.FirewallRule, error) + + DeleteFirewallRule(ctx context.Context, + resourcegroup string, + servername string, + firewallrulename string) (string, error) + + // also embed async client methods + resourcemanager.ARMClient +} diff --git a/pkg/resourcemanager/psql/firewallrule/firewallrule_reconcile.go b/pkg/resourcemanager/psql/firewallrule/firewallrule_reconcile.go new file mode 100644 index 00000000000..7d4a7c005c1 --- /dev/null +++ b/pkg/resourcemanager/psql/firewallrule/firewallrule_reconcile.go @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package server + +import ( + "context" + "fmt" + + "github.com/Azure/azure-service-operator/api/v1alpha1" + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/pkg/errhelp" + "github.com/Azure/azure-service-operator/pkg/helpers" + "github.com/Azure/azure-service-operator/pkg/resourcemanager" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +// Ensure makes sure a Postgres firewall rule exists +func (p *PSQLFirewallRuleClient) Ensure(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + instance, err := p.convert(obj) + if err != nil { + return true, err + } + + getRule, err := p.GetFirewallRule(ctx, instance.Spec.ResourceGroup, instance.Spec.Server, instance.Name) + if err == nil { + instance.Status.Message = resourcemanager.SuccessMsg + instance.Status.ResourceId = *getRule.ID + instance.Status.State = getRule.Status + instance.Status.Provisioned = true + instance.Status.Provisioning = false + return true, nil + } + + instance.Status.Provisioning = true + resp, err := p.CreateFirewallRule( + ctx, + instance.Spec.ResourceGroup, + instance.Spec.Server, + instance.Name, + instance.Spec.StartIPAddress, + instance.Spec.EndIPAddress, + ) + if err != nil { + instance.Status.Message = errhelp.StripErrorIDs(err) + azerr := errhelp.NewAzureErrorAzureError(err) + + catchInProgress := []string{ + errhelp.AsyncOpIncompleteError, + errhelp.AlreadyExists, + } + catchKnownError := []string{ + errhelp.ResourceGroupNotFoundErrorCode, + errhelp.ParentNotFoundErrorCode, + errhelp.NotFoundErrorCode, + errhelp.ResourceNotFound, + } + + // assertion that a 404 error implies that the Postgres server hasn't been provisioned yet + if resp != nil && resp.StatusCode == 404 { + instance.Status.Message = fmt.Sprintf("Waiting for Postgres server %s to provision", instance.Spec.Server) + instance.Status.Provisioning = false + return false, nil + } + + // handle the errors + if helpers.ContainsString(catchInProgress, azerr.Type) { + instance.Status.Message = "Postgres database exists but may not be ready" + return false, nil + } else if helpers.ContainsString(catchKnownError, azerr.Type) { + instance.Status.Provisioning = false + return false, nil + } else { + + // serious error occured, end reconcilliation and mark it as failed + instance.Status.Message = fmt.Sprintf("Error occurred creating the Postgres server: %s", errhelp.StripErrorIDs(err)) + instance.Status.Provisioned = false + instance.Status.Provisioning = false + instance.Status.FailedProvisioning = true + return true, nil + } + } + + return false, nil +} + +// Delete removes a Postgres firewall rule +func (p *PSQLFirewallRuleClient) Delete(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + instance, err := p.convert(obj) + if err != nil { + return true, err + } + + status, err := p.DeleteFirewallRule(ctx, instance.Spec.ResourceGroup, instance.Spec.Server, instance.Name) + if err != nil { + if !errhelp.IsAsynchronousOperationNotComplete(err) { + return true, err + } + } + instance.Status.State = status + + if err == nil { + if status != "InProgress" { + return false, nil + } + } + + return true, nil +} + +// GetParents gets the parents +func (p *PSQLFirewallRuleClient) GetParents(obj runtime.Object) ([]resourcemanager.KubeParent, error) { + + instance, err := p.convert(obj) + if err != nil { + return nil, err + } + + return []resourcemanager.KubeParent{ + { + Key: types.NamespacedName{ + Namespace: instance.Namespace, + Name: instance.Spec.Server, + }, + Target: &azurev1alpha1.PostgreSQLServer{}, + }, + { + Key: types.NamespacedName{ + Namespace: instance.Namespace, + Name: instance.Spec.ResourceGroup, + }, + Target: &azurev1alpha1.ResourceGroup{}, + }, + }, nil +} + +// GetStatus retrieves the status +func (p *PSQLFirewallRuleClient) GetStatus(obj runtime.Object) (*v1alpha1.ASOStatus, error) { + instance, err := p.convert(obj) + if err != nil { + return nil, err + } + return &instance.Status, nil +} + +func (p *PSQLFirewallRuleClient) convert(obj runtime.Object) (*v1alpha1.PostgreSQLFirewallRule, error) { + local, ok := obj.(*v1alpha1.PostgreSQLFirewallRule) + if !ok { + return nil, fmt.Errorf("failed type assertion on kind: %s", obj.GetObjectKind().GroupVersionKind().String()) + } + return local, nil +} diff --git a/pkg/resourcemanager/psql/firewallrule/manager.go b/pkg/resourcemanager/psql/firewallrule/manager.go deleted file mode 100644 index 3883ab04088..00000000000 --- a/pkg/resourcemanager/psql/firewallrule/manager.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -package server - -import ( - "context" - - psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" - "github.com/Azure/azure-service-operator/pkg/resourcemanager" -) - -type PostgreSQLFirewallRuleManager interface { - //convert(obj runtime.Object) (*v1alpha1.PostgreSQLFirewallRule, error) - - CreateFirewallRule(ctx context.Context, resourcegroup string, servername string, firewallrulename string, startip string, endip string) (psql.FirewallRulesCreateOrUpdateFuture, error) - DeleteFirewallRule(ctx context.Context, resourcegroup string, servername string, firewallrulename string) (string, error) - GetFirewallRule(ctx context.Context, resourcegroup string, servername string, firewallrulename string) (psql.FirewallRule, error) - // also embed async client methods - resourcemanager.ARMClient -} diff --git a/pkg/resourcemanager/psql/server/client.go b/pkg/resourcemanager/psql/server/client.go deleted file mode 100644 index 6ef051c4c7c..00000000000 --- a/pkg/resourcemanager/psql/server/client.go +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -package server - -import ( - "context" - "fmt" - - psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" - "github.com/Azure/azure-service-operator/api/v1alpha1" - azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" - "github.com/Azure/azure-service-operator/pkg/errhelp" - "github.com/Azure/azure-service-operator/pkg/helpers" - "github.com/Azure/azure-service-operator/pkg/resourcemanager" - "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" - "k8s.io/apimachinery/pkg/types" -) - -type PSQLServerClient struct { - SecretClient secrets.SecretClient - Scheme *runtime.Scheme -} - -func NewPSQLServerClient(secretclient secrets.SecretClient, scheme *runtime.Scheme) *PSQLServerClient { - return &PSQLServerClient{ - SecretClient: secretclient, - Scheme: scheme, - } -} - -func getPSQLServersClient() psql.ServersClient { - serversClient := psql.NewServersClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) - a, _ := iam.GetResourceManagementAuthorizer() - serversClient.Authorizer = a - serversClient.AddToUserAgent(config.UserAgent()) - return serversClient -} - -func getPSQLCheckNameAvailabilityClient() psql.CheckNameAvailabilityClient { - nameavailabilityClient := psql.NewCheckNameAvailabilityClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) - a, _ := iam.GetResourceManagementAuthorizer() - nameavailabilityClient.Authorizer = a - nameavailabilityClient.AddToUserAgent(config.UserAgent()) - return nameavailabilityClient -} - -func (p *PSQLServerClient) CheckServerNameAvailability(ctx context.Context, servername string) (bool, error) { - - client := getPSQLCheckNameAvailabilityClient() - - resourceType := "server" - - nameAvailabilityRequest := psql.NameAvailabilityRequest{ - Name: &servername, - Type: &resourceType, - } - _, err := client.Execute(ctx, nameAvailabilityRequest) - if err == nil { // Name available - return true, nil - } - return false, err - -} -func (p *PSQLServerClient) Ensure(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { - options := &resourcemanager.Options{} - for _, opt := range opts { - opt(options) - } - - if options.SecretClient != nil { - p.SecretClient = options.SecretClient - } - - instance, err := p.convert(obj) - if err != nil { - return true, err - } - - // Check to see if secret exists and if yes retrieve the admin login and password - secret, err := p.GetOrPrepareSecret(ctx, instance) - if err != nil { - return false, err - } - // Update secret - err = p.AddServerCredsToSecrets(ctx, instance.Name, secret, instance) - if err != nil { - return false, err - } - - client := getPSQLServersClient() - - // convert kube labels to expected tag format - labels := map[string]*string{} - for k, v := range instance.GetLabels() { - labels[k] = &v - } - instance.Status.Provisioning = true - // 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 - - server, err := p.GetServer(ctx, instance.Spec.ResourceGroup, instance.Name) - if err == nil { - instance.Status.State = string(server.UserVisibleState) - if server.UserVisibleState == "Ready" { - instance.Status.Provisioned = true - instance.Status.Provisioning = false - instance.Status.Message = resourcemanager.SuccessMsg - return true, nil - } - return false, nil - } - - adminlogin := string(secret["username"]) - adminpassword := string(secret["password"]) - - skuInfo := psql.Sku{ - Name: to.StringPtr(instance.Spec.Sku.Name), - Tier: psql.SkuTier(instance.Spec.Sku.Tier), - Capacity: to.Int32Ptr(instance.Spec.Sku.Capacity), - Size: to.StringPtr(instance.Spec.Sku.Size), - Family: to.StringPtr(instance.Spec.Sku.Family), - } - future, err := p.CreateServerIfValid( - ctx, - instance.Name, - instance.Spec.ResourceGroup, - instance.Spec.Location, - labels, - psql.ServerVersion(instance.Spec.ServerVersion), - psql.SslEnforcementEnum(instance.Spec.SSLEnforcement), - skuInfo, - adminlogin, - adminpassword, - ) - - if err != nil { - // let the user know what happened - instance.Status.Message = err.Error() - instance.Status.Provisioning = false - // errors we expect might happen that we are ok with waiting for - catch := []string{ - errhelp.ResourceGroupNotFoundErrorCode, - errhelp.ParentNotFoundErrorCode, - errhelp.NotFoundErrorCode, - errhelp.AsyncOpIncompleteError, - } - - catchUnrecoverableErrors := []string{ - errhelp.ProvisioningDisabled, - errhelp.LocationNotAvailableForResourceType, - } - - azerr := errhelp.NewAzureErrorAzureError(err) - if helpers.ContainsString(catch, azerr.Type) { - // most of these error technically mean the resource is actually not provisioning - switch azerr.Type { - case errhelp.AsyncOpIncompleteError: - instance.Status.Provisioning = true - } - // reconciliation is not done but error is acceptable - return false, nil - } - if helpers.ContainsString(catchUnrecoverableErrors, azerr.Type) { - // Unrecoverable error, so stop reconcilation - instance.Status.Message = "Reconcilation hit unrecoverable error: " + err.Error() - return true, nil - } - // reconciliation not done and we don't know what happened - return false, err - } - - instance.Status.State = future.Status() - - server, err = future.Result(client) - if err != nil { - // let the user know what happened - instance.Status.Message = err.Error() - instance.Status.Provisioning = false - // errors we expect might happen that we are ok with waiting for - catch := []string{ - errhelp.ResourceGroupNotFoundErrorCode, - errhelp.ParentNotFoundErrorCode, - errhelp.NotFoundErrorCode, - errhelp.AsyncOpIncompleteError, - } - - azerr := errhelp.NewAzureErrorAzureError(err) - if helpers.ContainsString(catch, azerr.Type) { - // most of these error technically mean the resource is actually not provisioning - switch azerr.Type { - case errhelp.AsyncOpIncompleteError: - instance.Status.Provisioning = true - } - // reconciliation is not done but error is acceptable - return false, nil - } - // reconciliation not done and we don't know what happened - return false, err - } - - instance.Status.State = string(server.UserVisibleState) - - if instance.Status.Provisioning { - instance.Status.Provisioned = true - instance.Status.Provisioning = false - instance.Status.Message = resourcemanager.SuccessMsg - } else { - instance.Status.Provisioned = false - instance.Status.Provisioning = true - } - - return true, nil -} - -func (p *PSQLServerClient) Delete(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { - - options := &resourcemanager.Options{} - for _, opt := range opts { - opt(options) - } - - if options.SecretClient != nil { - p.SecretClient = options.SecretClient - } - - instance, err := p.convert(obj) - if err != nil { - return true, err - } - - status, err := p.DeleteServer(ctx, instance.Spec.ResourceGroup, instance.Name) - if err != nil { - if !errhelp.IsAsynchronousOperationNotComplete(err) { - return true, err - } - } - instance.Status.State = status - - if err == nil { - if status != "InProgress" { - // Best case deletion of secrets - key := types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace} - p.SecretClient.Delete(ctx, key) - return false, nil - } - } - - return true, nil -} - -func (p *PSQLServerClient) GetParents(obj runtime.Object) ([]resourcemanager.KubeParent, error) { - - instance, err := p.convert(obj) - if err != nil { - return nil, err - } - - return []resourcemanager.KubeParent{ - { - Key: types.NamespacedName{ - Namespace: instance.Namespace, - Name: instance.Spec.ResourceGroup, - }, - Target: &azurev1alpha1.ResourceGroup{}, - }, - }, nil -} - -func (g *PSQLServerClient) GetStatus(obj runtime.Object) (*v1alpha1.ASOStatus, error) { - instance, err := g.convert(obj) - if err != nil { - return nil, err - } - return &instance.Status, nil -} - -func (p *PSQLServerClient) convert(obj runtime.Object) (*v1alpha1.PostgreSQLServer, error) { - local, ok := obj.(*v1alpha1.PostgreSQLServer) - if !ok { - return nil, fmt.Errorf("failed type assertion on kind: %s", obj.GetObjectKind().GroupVersionKind().String()) - } - return local, nil -} - -func (p *PSQLServerClient) CreateServerIfValid(ctx context.Context, servername string, resourcegroup string, location string, tags map[string]*string, serverversion psql.ServerVersion, sslenforcement psql.SslEnforcementEnum, skuInfo psql.Sku, adminlogin string, adminpassword string) (future psql.ServersCreateFuture, err error) { - - client := getPSQLServersClient() - - // Check if name is valid if this is the first create call - valid, err := p.CheckServerNameAvailability(ctx, servername) - if valid == false { - return future, err - } - - future, err = client.Create( - ctx, - resourcegroup, - servername, - psql.ServerForCreate{ - Location: &location, - Tags: tags, - Properties: &psql.ServerPropertiesForDefaultCreate{ - AdministratorLogin: &adminlogin, - AdministratorLoginPassword: &adminpassword, - Version: serverversion, - SslEnforcement: sslenforcement, - //StorageProfile: &psql.StorageProfile{}, - CreateMode: psql.CreateModeServerPropertiesForCreate, - }, - Sku: &skuInfo, - }, - ) - - return future, err -} - -func (p *PSQLServerClient) DeleteServer(ctx context.Context, resourcegroup string, servername string) (status string, err error) { - - client := getPSQLServersClient() - - _, err = client.Get(ctx, resourcegroup, servername) - if err == nil { // Server present, so go ahead and delete - future, err := client.Delete(ctx, resourcegroup, servername) - return future.Status(), err - } - // Server not present so return success anyway - return "Server not present", nil - -} - -func (p *PSQLServerClient) GetServer(ctx context.Context, resourcegroup string, servername string) (server psql.Server, err error) { - - client := getPSQLServersClient() - return client.Get(ctx, resourcegroup, servername) -} - -func (p *PSQLServerClient) AddServerCredsToSecrets(ctx context.Context, secretName string, data map[string][]byte, instance *azurev1alpha1.PostgreSQLServer) error { - key := types.NamespacedName{ - Name: secretName, - Namespace: instance.Namespace, - } - - err := p.SecretClient.Upsert(ctx, - key, - data, - secrets.WithOwner(instance), - secrets.WithScheme(p.Scheme), - ) - if err != nil { - return err - } - - return nil -} - -func (p *PSQLServerClient) GetOrPrepareSecret(ctx context.Context, instance *azurev1alpha1.PostgreSQLServer) (map[string][]byte, error) { - name := instance.Name - - usernameLength := 8 - passwordLength := 16 - - secret := map[string][]byte{} - - key := types.NamespacedName{Name: name, Namespace: instance.Namespace} - if stored, err := p.SecretClient.Get(ctx, key); err == nil { - return stored, nil - } - - randomUsername, err := helpers.GenerateRandomUsername(usernameLength, 0) - if err != nil { - return secret, err - } - - randomPassword, err := helpers.GenerateRandomPassword(passwordLength) - if err != nil { - return secret, err - } - - secret["username"] = []byte(randomUsername) - secret["fullyQualifiedUsername"] = []byte(fmt.Sprintf("%s@%s", randomUsername, name)) - secret["password"] = []byte(randomPassword) - secret["postgreSqlServerName"] = []byte(name) - // TODO: The below may not be right for non Azure public cloud. - secret["fullyQualifiedServerName"] = []byte(name + ".postgres.database.azure.com") - - return secret, nil -} diff --git a/pkg/resourcemanager/psql/server/manager.go b/pkg/resourcemanager/psql/server/manager.go deleted file mode 100644 index 7843a62c882..00000000000 --- a/pkg/resourcemanager/psql/server/manager.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -package server - -import ( - "context" - - psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" - "github.com/Azure/azure-service-operator/pkg/resourcemanager" -) - -type PostgreSQLServerManager interface { - //convert(obj runtime.Object) (*v1alpha1.PostgreSQLServer, error) - - //CheckServerNameAvailability(ctx context.Context, servername string) (bool, error) - CreateServerIfValid(ctx context.Context, servername string, resourcegroup string, location string, tags map[string]*string, serverversion psql.ServerVersion, sslenforcement psql.SslEnforcementEnum, skuInfo psql.Sku, adminlogin string, adminpassword string) (psql.ServersCreateFuture, error) - DeleteServer(ctx context.Context, resourcegroup string, servername string) (string, error) - GetServer(ctx context.Context, resourcegroup string, servername string) (psql.Server, error) - //AddServerCredsToSecrets(ctx context.Context, secretName string, data map[string][]byte, instance *azurev1alpha1.PostgreSQLServer) error - - // also embed async client methods - resourcemanager.ARMClient -} diff --git a/pkg/resourcemanager/psql/server/server.go b/pkg/resourcemanager/psql/server/server.go new file mode 100644 index 00000000000..34f30824c31 --- /dev/null +++ b/pkg/resourcemanager/psql/server/server.go @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package server + +import ( + "context" + "fmt" + + psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" + 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/secrets" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +type PSQLServerClient struct { + SecretClient secrets.SecretClient + Scheme *runtime.Scheme +} + +func NewPSQLServerClient(secretclient secrets.SecretClient, scheme *runtime.Scheme) *PSQLServerClient { + return &PSQLServerClient{ + SecretClient: secretclient, + Scheme: scheme, + } +} + +func getPSQLServersClient() (psql.ServersClient, error) { + serversClient := psql.NewServersClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) + a, err := iam.GetResourceManagementAuthorizer() + if err != nil { + return psql.ServersClient{}, err + } + serversClient.Authorizer = a + serversClient.AddToUserAgent(config.UserAgent()) + return serversClient, nil +} + +func getPSQLCheckNameAvailabilityClient() (psql.CheckNameAvailabilityClient, error) { + nameavailabilityClient := psql.NewCheckNameAvailabilityClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) + a, err := iam.GetResourceManagementAuthorizer() + if err != nil { + return psql.CheckNameAvailabilityClient{}, err + } + nameavailabilityClient.Authorizer = a + nameavailabilityClient.AddToUserAgent(config.UserAgent()) + return nameavailabilityClient, nil +} + +func (p *PSQLServerClient) CheckServerNameAvailability(ctx context.Context, servername string) (bool, error) { + + client, err := getPSQLCheckNameAvailabilityClient() + if err != nil { + return false, err + } + + resourceType := "Microsoft.DBforPostgreSQL/servers" + + nameAvailabilityRequest := psql.NameAvailabilityRequest{ + Name: &servername, + Type: &resourceType, + } + _, err = client.Execute(ctx, nameAvailabilityRequest) + if err == nil { // Name available + return true, nil + } + return false, err + +} + +func (p *PSQLServerClient) CreateServerIfValid(ctx context.Context, servername string, resourcegroup string, location string, tags map[string]*string, serverversion psql.ServerVersion, sslenforcement psql.SslEnforcementEnum, skuInfo psql.Sku, adminlogin string, adminpassword string) (server psql.Server, err error) { + + client, err := getPSQLServersClient() + if err != nil { + return psql.Server{}, err + } + + // Check if name is valid if this is the first create call + valid, err := p.CheckServerNameAvailability(ctx, servername) + if valid == false { + return psql.Server{}, err + } + + future, err := client.Create( + ctx, + resourcegroup, + servername, + psql.ServerForCreate{ + Location: &location, + Tags: tags, + Properties: &psql.ServerPropertiesForDefaultCreate{ + AdministratorLogin: &adminlogin, + AdministratorLoginPassword: &adminpassword, + Version: serverversion, + SslEnforcement: sslenforcement, + //StorageProfile: &psql.StorageProfile{}, + CreateMode: psql.CreateModeServerPropertiesForCreate, + }, + Sku: &skuInfo, + }, + ) + if err != nil { + return psql.Server{}, err + } + + return future.Result(client) +} + +func (p *PSQLServerClient) DeleteServer(ctx context.Context, resourcegroup string, servername string) (status string, err error) { + + client, err := getPSQLServersClient() + if err != nil { + return "", err + } + + _, err = client.Get(ctx, resourcegroup, servername) + if err == nil { // Server present, so go ahead and delete + future, err := client.Delete(ctx, resourcegroup, servername) + return future.Status(), err + } + // Server not present so return success anyway + return "Server not present", nil + +} + +func (p *PSQLServerClient) GetServer(ctx context.Context, resourcegroup string, servername string) (server psql.Server, err error) { + + client, err := getPSQLServersClient() + if err != nil { + return psql.Server{}, err + } + + return client.Get(ctx, resourcegroup, servername) +} + +func (p *PSQLServerClient) AddServerCredsToSecrets(ctx context.Context, secretName string, data map[string][]byte, instance *azurev1alpha1.PostgreSQLServer) error { + key := types.NamespacedName{ + Name: secretName, + Namespace: instance.Namespace, + } + + err := p.SecretClient.Upsert(ctx, + key, + data, + secrets.WithOwner(instance), + secrets.WithScheme(p.Scheme), + ) + if err != nil { + return err + } + + return nil +} + +func (p *PSQLServerClient) GetOrPrepareSecret(ctx context.Context, instance *azurev1alpha1.PostgreSQLServer) (map[string][]byte, error) { + name := instance.Name + + usernameLength := 8 + passwordLength := 16 + + secret := map[string][]byte{} + + key := types.NamespacedName{Name: name, Namespace: instance.Namespace} + if stored, err := p.SecretClient.Get(ctx, key); err == nil { + return stored, nil + } + + randomUsername, err := helpers.GenerateRandomUsername(usernameLength, 0) + if err != nil { + return secret, err + } + + randomPassword, err := helpers.GenerateRandomPassword(passwordLength) + if err != nil { + return secret, err + } + + secret["username"] = []byte(randomUsername) + secret["fullyQualifiedUsername"] = []byte(fmt.Sprintf("%s@%s", randomUsername, name)) + secret["password"] = []byte(randomPassword) + secret["postgreSqlServerName"] = []byte(name) + // TODO: The below may not be right for non Azure public cloud. + secret["fullyQualifiedServerName"] = []byte(name + ".postgres.database.azure.com") + + return secret, nil +} diff --git a/pkg/resourcemanager/psql/server/server_manager.go b/pkg/resourcemanager/psql/server/server_manager.go new file mode 100644 index 00000000000..3fc3ad42f8c --- /dev/null +++ b/pkg/resourcemanager/psql/server/server_manager.go @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package server + +import ( + "context" + + psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/pkg/resourcemanager" +) + +type PostgreSQLServerManager interface { + //convert(obj runtime.Object) (*v1alpha1.PostgreSQLServer, error) + + CheckServerNameAvailability(ctx context.Context, + servername string) (bool, error) + + CreateServerIfValid(ctx context.Context, + servername string, + resourcegroup string, + location string, + tags map[string]*string, + serverversion psql.ServerVersion, + sslenforcement psql.SslEnforcementEnum, + skuInfo psql.Sku, + adminlogin string, + adminpassword string) (psql.Server, error) + + DeleteServer(ctx context.Context, + resourcegroup string, + servername string) (string, error) + + GetServer(ctx context.Context, + resourcegroup string, + servername string) (psql.Server, error) + + AddServerCredsToSecrets(ctx context.Context, + secretName string, + data map[string][]byte, + instance *azurev1alpha1.PostgreSQLServer) error + + GetOrPrepareSecret(ctx context.Context, + instance *azurev1alpha1.PostgreSQLServer) (map[string][]byte, error) + + // also embed async client methods + resourcemanager.ARMClient +} diff --git a/pkg/resourcemanager/psql/server/server_reconcile.go b/pkg/resourcemanager/psql/server/server_reconcile.go new file mode 100644 index 00000000000..94ed8b64d92 --- /dev/null +++ b/pkg/resourcemanager/psql/server/server_reconcile.go @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package server + +import ( + "context" + "fmt" + + psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" + "github.com/Azure/azure-service-operator/api/v1alpha1" + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/pkg/errhelp" + "github.com/Azure/azure-service-operator/pkg/helpers" + "github.com/Azure/azure-service-operator/pkg/resourcemanager" + "github.com/Azure/go-autorest/autorest/to" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +// Ensure creates the Postgres server +func (p *PSQLServerClient) Ensure(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + options := &resourcemanager.Options{} + for _, opt := range opts { + opt(options) + } + + if options.SecretClient != nil { + p.SecretClient = options.SecretClient + } + + instance, err := p.convert(obj) + if err != nil { + return true, err + } + + // Check to see if secret exists and if yes retrieve the admin login and password + secret, err := p.GetOrPrepareSecret(ctx, instance) + if err != nil { + return false, err + } + // Update secret + err = p.AddServerCredsToSecrets(ctx, instance.Name, secret, instance) + if err != nil { + return false, err + } + + // if an error occurs thats ok as it means that it doesn't exist yet + getServer, err := p.GetServer(ctx, instance.Spec.ResourceGroup, instance.Name) + if err == nil { + instance.Status.State = string(getServer.UserVisibleState) + + // succeeded! so end reconcilliation successfully + if getServer.UserVisibleState == "Ready" { + instance.Status.Message = resourcemanager.SuccessMsg + instance.Status.ResourceId = *getServer.ID + instance.Status.Provisioned = true + instance.Status.Provisioning = false + return true, nil + } + + // the database exists but has not provisioned yet - so keep waiting + instance.Status.Message = "Postgres server exists but may not be ready" + instance.Status.State = string(getServer.UserVisibleState) + return false, nil + } + + // setup variables for create call + labels := helpers.LabelsToTags(instance.GetLabels()) + adminlogin := string(secret["username"]) + adminpassword := string(secret["password"]) + skuInfo := psql.Sku{ + Name: to.StringPtr(instance.Spec.Sku.Name), + Tier: psql.SkuTier(instance.Spec.Sku.Tier), + Capacity: to.Int32Ptr(instance.Spec.Sku.Capacity), + Size: to.StringPtr(instance.Spec.Sku.Size), + Family: to.StringPtr(instance.Spec.Sku.Family), + } + + // create the server + instance.Status.Provisioning = true + _, err = p.CreateServerIfValid( + ctx, + instance.Name, + instance.Spec.ResourceGroup, + instance.Spec.Location, + labels, + psql.ServerVersion(instance.Spec.ServerVersion), + psql.SslEnforcementEnum(instance.Spec.SSLEnforcement), + skuInfo, + adminlogin, + adminpassword, + ) + if err != nil { + instance.Status.Message = errhelp.StripErrorIDs(err) + azerr := errhelp.NewAzureErrorAzureError(err) + + catchInProgress := []string{ + errhelp.AsyncOpIncompleteError, + errhelp.AlreadyExists, + } + catchKnownError := []string{ + errhelp.ResourceGroupNotFoundErrorCode, + errhelp.ParentNotFoundErrorCode, + errhelp.NotFoundErrorCode, + errhelp.ServiceBusy, + } + + // handle the errors + if helpers.ContainsString(catchInProgress, azerr.Type) { + instance.Status.Message = "Postgres server exists but may not be ready" + return false, nil + } else if helpers.ContainsString(catchKnownError, azerr.Type) { + instance.Status.Provisioning = false + return false, nil + } else { + + // serious error occured, end reconcilliation and mark it as failed + instance.Status.Message = fmt.Sprintf("Error occurred creating the Postgres server: %s", errhelp.StripErrorIDs(err)) + instance.Status.Provisioned = false + instance.Status.Provisioning = false + instance.Status.FailedProvisioning = true + return true, nil + } + } + + return false, nil +} + +// Delete deletes the Postgres server +func (p *PSQLServerClient) Delete(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + + options := &resourcemanager.Options{} + for _, opt := range opts { + opt(options) + } + + if options.SecretClient != nil { + p.SecretClient = options.SecretClient + } + + instance, err := p.convert(obj) + if err != nil { + return true, err + } + + status, err := p.DeleteServer(ctx, instance.Spec.ResourceGroup, instance.Name) + if err != nil { + if !errhelp.IsAsynchronousOperationNotComplete(err) { + return true, err + } + } + instance.Status.State = status + + if err == nil { + if status != "InProgress" { + // Best case deletion of secrets + key := types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace} + p.SecretClient.Delete(ctx, key) + return false, nil + } + } + + return true, nil +} + +// GetParents gets the resource's parents +func (p *PSQLServerClient) GetParents(obj runtime.Object) ([]resourcemanager.KubeParent, error) { + + instance, err := p.convert(obj) + if err != nil { + return nil, err + } + + return []resourcemanager.KubeParent{ + { + Key: types.NamespacedName{ + Namespace: instance.Namespace, + Name: instance.Spec.ResourceGroup, + }, + Target: &azurev1alpha1.ResourceGroup{}, + }, + }, nil +} + +// GetStatus returns the status +func (p *PSQLServerClient) GetStatus(obj runtime.Object) (*v1alpha1.ASOStatus, error) { + instance, err := p.convert(obj) + if err != nil { + return nil, err + } + return &instance.Status, nil +} + +func (p *PSQLServerClient) convert(obj runtime.Object) (*v1alpha1.PostgreSQLServer, error) { + local, ok := obj.(*v1alpha1.PostgreSQLServer) + if !ok { + return nil, fmt.Errorf("failed type assertion on kind: %s", obj.GetObjectKind().GroupVersionKind().String()) + } + return local, nil +} diff --git a/pkg/resourcemanager/rediscaches/rediscache_reconcile.go b/pkg/resourcemanager/rediscaches/rediscache_reconcile.go index 28e8ee253f7..bbb62234e90 100644 --- a/pkg/resourcemanager/rediscaches/rediscache_reconcile.go +++ b/pkg/resourcemanager/rediscaches/rediscache_reconcile.go @@ -41,11 +41,11 @@ func (rc *AzureRedisCacheManager) Ensure(ctx context.Context, obj runtime.Object instance.Spec.SecretName = redisName } - instance.Status.Provisioning = true - instance.Status.FailedProvisioning = false - + // if an error occurs thats ok as it means that it doesn't exist yet newRc, err := rc.GetRedisCache(ctx, groupName, name) if err == nil { + + // succeeded! so end reconcilliation successfully if newRc.ProvisioningState == "Succeeded" { err = rc.ListKeysAndCreateSecrets(groupName, redisName, instance.Spec.SecretName, instance) if err != nil { @@ -59,48 +59,49 @@ func (rc *AzureRedisCacheManager) Ensure(ctx context.Context, obj runtime.Object instance.Status.Provisioning = false return true, nil } + + // the redis cache exists but has not provisioned yet - so keep waiting instance.Status.Message = "RedisCache exists but may not be ready" instance.Status.State = string(newRc.ProvisioningState) return false, nil } - instance.Status.Message = fmt.Sprintf("RedisCache Get error %s", err.Error()) + // actually provision the redis cache + instance.Status.Provisioning = true _, err = rc.CreateRedisCache(ctx, *instance) if err != nil { instance.Status.Message = errhelp.StripErrorIDs(err) - instance.Status.Provisioning = false azerr := errhelp.NewAzureErrorAzureError(err) - unrecoverable := []string{ - errhelp.RequestConflictError, - errhelp.BadRequest, - } - if helpers.ContainsString(unrecoverable, azerr.Type) || azerr.Code == http.StatusBadRequest { - return true, nil + catchInProgress := []string{ + errhelp.AsyncOpIncompleteError, + errhelp.AlreadyExists, } - - catchNotProvisioning := []string{ + catchKnownError := []string{ errhelp.ParentNotFoundErrorCode, errhelp.ResourceGroupNotFoundErrorCode, - errhelp.AlreadyExists, errhelp.NotFoundErrorCode, } - if helpers.ContainsString(catchNotProvisioning, azerr.Type) { - return false, nil - } - catchProvisioning := []string{ - errhelp.AsyncOpIncompleteError, - } - if helpers.ContainsString(catchProvisioning, azerr.Type) { - instance.Status.Provisioning = true + // handle the error + if helpers.ContainsString(catchInProgress, azerr.Type) { + instance.Status.Message = "RedisCache exists but may not be ready" return false, nil - } + } else if helpers.ContainsString(catchKnownError, azerr.Type) { + instance.Status.Provisioning = false + return false, nil + } else { - return false, err + // serious error occured, end reconcilliation and mark it as failed + instance.Status.Message = fmt.Sprintf("Error occurred creating the RedisCache: %s", errhelp.StripErrorIDs(err)) + instance.Status.Provisioned = false + instance.Status.Provisioning = false + instance.Status.FailedProvisioning = true + return true, nil + } } - return true, nil + return false, nil } // Delete drops a rediscache diff --git a/pkg/resourcemanager/rediscaches/rediscaches.go b/pkg/resourcemanager/rediscaches/rediscaches.go index c4922fe71b9..f9bf0e4001d 100644 --- a/pkg/resourcemanager/rediscaches/rediscaches.go +++ b/pkg/resourcemanager/rediscaches/rediscaches.go @@ -16,22 +16,19 @@ import ( "github.com/Azure/azure-service-operator/pkg/resourcemanager/iam" "github.com/Azure/azure-service-operator/pkg/secrets" "github.com/Azure/go-autorest/autorest/to" - "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ) // AzureRedisCacheManager creates a new RedisCacheManager type AzureRedisCacheManager struct { - Log logr.Logger SecretClient secrets.SecretClient Scheme *runtime.Scheme } // NewAzureRedisCacheManager creates a new RedisCacheManager -func NewAzureRedisCacheManager(log logr.Logger, secretClient secrets.SecretClient, scheme *runtime.Scheme) *AzureRedisCacheManager { +func NewAzureRedisCacheManager(secretClient secrets.SecretClient, scheme *runtime.Scheme) *AzureRedisCacheManager { return &AzureRedisCacheManager{ - Log: log, SecretClient: secretClient, Scheme: scheme, } diff --git a/pkg/resourcemanager/rediscaches/suite_test.go b/pkg/resourcemanager/rediscaches/suite_test.go index 770bf37c19c..5f8c782ad1e 100644 --- a/pkg/resourcemanager/rediscaches/suite_test.go +++ b/pkg/resourcemanager/rediscaches/suite_test.go @@ -61,10 +61,8 @@ var _ = BeforeSuite(func() { tc = TestContext{ ResourceGroupName: resourceGroupName, ResourceGroupLocation: resourceGroupLocation, - RedisCacheManager: &AzureRedisCacheManager{ - Log: zaplogger, - }, - ResourceGroupManager: resourceGroupManager, + RedisCacheManager: &AzureRedisCacheManager{}, + ResourceGroupManager: resourceGroupManager, } }) diff --git a/pkg/resourcemanager/storages/storageaccount/storage_reconcile.go b/pkg/resourcemanager/storages/storageaccount/storage_reconcile.go index a8865ae71c5..55ebed67f06 100644 --- a/pkg/resourcemanager/storages/storageaccount/storage_reconcile.go +++ b/pkg/resourcemanager/storages/storageaccount/storage_reconcile.go @@ -133,6 +133,15 @@ func (sa *azureStorageManager) Delete(ctx context.Context, obj runtime.Object, o groupName := instance.Spec.ResourceGroup _, err = sa.DeleteStorage(ctx, groupName, name) if err != nil { + catch := []string{ + errhelp.ValidationError, + } + err = errhelp.NewAzureError(err) + if azerr, ok := err.(*errhelp.AzureError); ok { + if helpers.ContainsString(catch, azerr.Type) { + return false, nil + } + } return true, err } diff --git a/pkg/secrets/keyvault/client.go b/pkg/secrets/keyvault/client.go index 470867f47b3..30b60d8b356 100644 --- a/pkg/secrets/keyvault/client.go +++ b/pkg/secrets/keyvault/client.go @@ -336,8 +336,15 @@ func (k *KeyvaultSecretClient) Get(ctx context.Context, key types.NamespacedName stringSecret := *result.Value - // Convert the data from json string to map and return - json.Unmarshal([]byte(stringSecret), &data) + // Convert the data from json string to map + jsonErr := json.Unmarshal([]byte(stringSecret), &data) + + // If Unmarshal fails on the input data, the secret likely not a json string so we return the string value directly rather than unmarshaling + if jsonErr != nil { + data = map[string][]byte{ + secretName: []byte(stringSecret), + } + } return data, err } diff --git a/pkg/secrets/keyvault/client_test.go b/pkg/secrets/keyvault/client_test.go index aef31ef1d8b..d3301341314 100644 --- a/pkg/secrets/keyvault/client_test.go +++ b/pkg/secrets/keyvault/client_test.go @@ -164,5 +164,80 @@ var _ = Describe("Keyvault Secrets Client", func() { }) }) + It("should create and delete secrets in Keyvault with Flatten enabled", func() { + secretName := "kvsecret" + strconv.FormatInt(GinkgoRandomSeed(), 10) + + var err error + + data := map[string][]byte{ + "test": []byte("data"), + "sweet": []byte("potato"), + } + + client := New(keyVaultName) + + key := types.NamespacedName{Name: secretName, Namespace: "default"} + + Context("creating flattened secret with KeyVault client", func() { + err = client.Create(ctx, key, data, secrets.Flatten(true)) + Expect(err).To(BeNil()) + }) + + Context("ensuring flattened secrets exist using keyvault client", func() { + // Look for each originally passed secret item in the keyvault + for testKey, testValue := range data { + returnedValue, err := client.Get( + ctx, + types.NamespacedName{Namespace: "default", Name: secretName + "-" + testKey}, + ) + + Expect(err).To(BeNil()) + + expectedReturnSecretKey := "default-" + secretName + "-" + testKey + + Expect(testValue).To(Equal(returnedValue[expectedReturnSecretKey])) + } + }) + + datanew := map[string][]byte{ + "french": []byte("fries"), + "hot": []byte("dogs"), + } + + Context("upserting the flattened secret to make sure it can be overwritten", func() { + err = client.Upsert(ctx, key, datanew, secrets.Flatten(true)) + Expect(err).To(BeNil()) + }) + + Context("ensuring updated flattened secret exists using keyvault client", func() { + // Look for each originally passed secret item in the keyvault + for testKey, testValue := range datanew { + returnedValue, err := client.Get( + ctx, + types.NamespacedName{Namespace: "default", Name: secretName + "-" + testKey}, + ) + + Expect(err).To(BeNil()) + + expectedReturnSecretKey := "default-" + secretName + "-" + testKey + + Expect(testValue).To(Equal(returnedValue[expectedReturnSecretKey])) + } + }) + + Context("delete flattened secrets and ensure they're gone", func() { + for testKey, _ := range datanew { + err := client.Delete( + ctx, + types.NamespacedName{Namespace: "default", Name: secretName + "-" + testKey}, + ) + + Expect(err).To(BeNil()) + + _, err = client.Get(ctx, key) + Expect(err).ToNot(BeNil()) + } + }) + }) }) })