diff --git a/api/v1alpha1/cosmosdb_types.go b/api/v1alpha1/cosmosdb_types.go index 34fea0935fc..a037f4e02f1 100644 --- a/api/v1alpha1/cosmosdb_types.go +++ b/api/v1alpha1/cosmosdb_types.go @@ -17,10 +17,11 @@ type CosmosDBSpec struct { // +kubebuilder:validation:MinLength=0 - Location string `json:"location,omitempty"` - ResourceGroup string `json:"resourceGroup"` - Kind CosmosDBKind `json:"kind,omitempty"` - Properties CosmosDBProperties `json:"properties,omitempty"` + Location string `json:"location,omitempty"` + ResourceGroup string `json:"resourceGroup"` + Kind CosmosDBKind `json:"kind,omitempty"` + Properties CosmosDBProperties `json:"properties,omitempty"` + KeyVaultToStoreSecrets string `json:"keyVaultToStoreSecrets,omitempty"` } // CosmosDBKind enumerates the values for kind. @@ -42,6 +43,7 @@ type CosmosDBProperties struct { // CosmosDBDatabaseAccountOfferType - The offer type for the Cosmos DB database account. DatabaseAccountOfferType CosmosDBDatabaseAccountOfferType `json:"databaseAccountOfferType,omitempty"` //Locations []CosmosDBLocation `json:"locations,omitempty"` + MongoDBVersion string `json:"mongoDBVersion,omitempty"` } // +kubebuilder:validation:Enum=Standard diff --git a/config/samples/azure_v1alpha1_cosmosdb.yaml b/config/samples/azure_v1alpha1_cosmosdb.yaml index d3efa498467..8580c570468 100644 --- a/config/samples/azure_v1alpha1_cosmosdb.yaml +++ b/config/samples/azure_v1alpha1_cosmosdb.yaml @@ -8,3 +8,10 @@ spec: resourceGroup: resourcegroup-azure-operators properties: databaseAccountOfferType: Standard + # optionally set the mongoDBVersion to "3.2" or "3.6", if omitted the default is "3.2" + # NOTE: kind must be set to MongoDB for this to take effect + #mongoDBVersion: "3.6" + + # Use the field below to optionally specify a different keyvault + # to store the connectiong string secrets in + #keyVaultToStoreSecrets: asoSecretKeyVault \ No newline at end of file diff --git a/config/samples/azure_v1alpha1_storageaccount.yaml b/config/samples/azure_v1alpha1_storageaccount.yaml index 39b590fe276..29cd0fe5cdf 100644 --- a/config/samples/azure_v1alpha1_storageaccount.yaml +++ b/config/samples/azure_v1alpha1_storageaccount.yaml @@ -11,11 +11,11 @@ spec: accessTier: Hot supportsHttpsTrafficOnly: true # Optional: networkRule - networkRule: - bypass: AzureServices # Possible values are AzureServices, Metrics, None, Logging - defaultAction: Deny # Possible values are Allow, Deny - virtualNetworkRules: - - subnetId: /subscriptions/{subscription}/resourceGroups/{resourcegroup}/providers/Microsoft.Network/virtualNetworks/{vnet}/subnets/{subnet} - ipRules: #could be an ip range or a ip address - - ipAddressOrRange: 2.2.0.0/24 - - ipAddressOrRange: 2.2.2.1 + # networkRule: + # bypass: AzureServices # Possible values are AzureServices, Metrics, None, Logging + # defaultAction: Deny # Possible values are Allow, Deny + # virtualNetworkRules: + # - subnetId: /subscriptions/{subscription}/resourceGroups/{resourcegroup}/providers/Microsoft.Network/virtualNetworks/{vnet}/subnets/{subnet} + # ipRules: #could be an ip range or a ip address + # - ipAddressOrRange: 2.2.0.0/24 + # - ipAddressOrRange: 2.2.2.1 diff --git a/controllers/cosmosdb_controller_test.go b/controllers/cosmosdb_controller_test.go index e68912ae8f4..31ad06cb5e7 100644 --- a/controllers/cosmosdb_controller_test.go +++ b/controllers/cosmosdb_controller_test.go @@ -10,22 +10,27 @@ import ( "testing" "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/pkg/errhelp" + + "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ) func TestCosmosDBHappyPath(t *testing.T) { t.Parallel() defer PanicRecover(t) ctx := context.Background() + assert := assert.New(t) - cosmosDBAccountName := GenerateTestResourceNameWithRandom("cosmosdb", 8) - cosmosDBNamespace := "default" + name := GenerateTestResourceNameWithRandom("cosmosdb", 8) + namespace := "default" dbInstance := &v1alpha1.CosmosDB{ ObjectMeta: metav1.ObjectMeta{ - Name: cosmosDBAccountName, - Namespace: cosmosDBNamespace, + Name: name, + Namespace: namespace, }, Spec: v1alpha1.CosmosDBSpec{ Location: tc.resourceGroupLocation, @@ -37,10 +42,22 @@ func TestCosmosDBHappyPath(t *testing.T) { }, } + key := types.NamespacedName{Name: name, Namespace: namespace} + EnsureInstance(ctx, t, tc, dbInstance) + assert.Eventually(func() bool { + secret, err := tc.secretClient.Get(ctx, key) + return err == nil && len(secret) > 0 + }, tc.timeoutFast, tc.retry, "wait for cosmosdb to have secret") + EnsureDelete(ctx, t, tc, dbInstance) + assert.Eventually(func() bool { + _, err := tc.secretClient.Get(ctx, key) + return err != nil + }, tc.timeoutFast, tc.retry, "wait for cosmosdb to delete secret") + } func TestCosmosDBControllerNoResourceGroup(t *testing.T) { @@ -69,10 +86,8 @@ func TestCosmosDBControllerNoResourceGroup(t *testing.T) { }, }, } - //the expected error meessage to be shown - errMessage := "Waiting for resource group '" + resourceGroupName + "' to be available" - EnsureInstanceWithResult(ctx, t, tc, dbInstance1, errMessage, false) + EnsureInstanceWithResult(ctx, t, tc, dbInstance1, errhelp.ResourceGroupNotFoundErrorCode, false) EnsureDelete(ctx, t, tc, dbInstance1) } diff --git a/controllers/suite_test.go b/controllers/suite_test.go index f3fb0123e48..337d905aca7 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -154,7 +154,7 @@ func setup() error { secretClient, scheme.Scheme, ) - cosmosDbManager = resourcemanagercosmosdb.NewAzureCosmosDBManager() + cosmosDbManager = resourcemanagercosmosdb.NewAzureCosmosDBManager(secretClient) apiMgmtManager = resourcemanagerapimgmt.NewManager() resourceGroupManager = resourcegroupsresourcemanager.NewAzureResourceGroupManager() eventHubManagers = resourcemanagereventhub.AzureEventHubManagers diff --git a/main.go b/main.go index 2b1fb8b788c..b675d0a7b3e 100644 --- a/main.go +++ b/main.go @@ -127,6 +127,9 @@ func main() { ) eventhubNamespaceClient := resourcemanagereventhub.NewEventHubNamespaceClient() consumerGroupClient := resourcemanagereventhub.NewConsumerGroupClient() + cosmosDBClient := resourcemanagercosmosdb.NewAzureCosmosDBManager( + secretClient, + ) storageManagers := resourcemanagerstorage.AzureStorageManagers keyVaultManager := resourcemanagerkeyvault.NewAzureKeyVaultManager(mgr.GetScheme()) keyVaultKeyManager := &resourcemanagerkeyvault.KeyvaultKeyClient{ @@ -172,7 +175,7 @@ func main() { err = (&controllers.CosmosDBReconciler{ Reconciler: &controllers.AsyncReconciler{ Client: mgr.GetClient(), - AzureClient: resourcemanagercosmosdb.NewAzureCosmosDBManager(), + AzureClient: cosmosDBClient, Telemetry: telemetry.InitializeTelemetryDefault( "CosmosDB", ctrl.Log.WithName("controllers").WithName("CosmosDB"), diff --git a/pkg/errhelp/errors.go b/pkg/errhelp/errors.go index 673f2de9a59..8431091e439 100644 --- a/pkg/errhelp/errors.go +++ b/pkg/errhelp/errors.go @@ -42,6 +42,7 @@ const ( NotFoundErrorCode = "NotFound" NoSuchHost = "no such host" ParentNotFoundErrorCode = "ParentResourceNotFound" + PreconditionFailed = "PreconditionFailed" QuotaExceeded = "QuotaExceeded" ResourceGroupNotFoundErrorCode = "ResourceGroupNotFound" RegionDoesNotAllowProvisioning = "RegionDoesNotAllowProvisioning" diff --git a/pkg/resourcemanager/cosmosdbs/cosmosdb.go b/pkg/resourcemanager/cosmosdbs/cosmosdb.go index 0d22bb85e85..aa405dac93a 100644 --- a/pkg/resourcemanager/cosmosdbs/cosmosdb.go +++ b/pkg/resourcemanager/cosmosdbs/cosmosdb.go @@ -10,15 +10,17 @@ import ( "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2015-04-08/documentdb" "github.com/Azure/azure-service-operator/api/v1alpha1" - "github.com/Azure/azure-service-operator/pkg/errhelp" "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" "github.com/Azure/go-autorest/autorest/to" ) // AzureCosmosDBManager is the struct which contains helper functions for resource groups -type AzureCosmosDBManager struct{} +type AzureCosmosDBManager struct { + SecretClient secrets.SecretClient +} func getCosmosDBClient() (documentdb.DatabaseAccountsClient, error) { cosmosDBClient := documentdb.NewDatabaseAccountsClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) @@ -28,9 +30,9 @@ func getCosmosDBClient() (documentdb.DatabaseAccountsClient, error) { cosmosDBClient = documentdb.DatabaseAccountsClient{} } else { cosmosDBClient.Authorizer = a - cosmosDBClient.AddToUserAgent(config.UserAgent()) } + err = cosmosDBClient.AddToUserAgent(config.UserAgent()) return cosmosDBClient, err } @@ -42,15 +44,25 @@ func (*AzureCosmosDBManager) CreateOrUpdateCosmosDB( location string, kind v1alpha1.CosmosDBKind, dbType v1alpha1.CosmosDBDatabaseAccountOfferType, - tags map[string]*string) (*documentdb.DatabaseAccount, *errhelp.AzureError) { + dbVersion string, + tags map[string]*string) (*documentdb.DatabaseAccount, error) { cosmosDBClient, err := getCosmosDBClient() if err != nil { - return nil, errhelp.NewAzureErrorAzureError(err) + return nil, err } dbKind := documentdb.DatabaseAccountKind(kind) sDBType := string(dbType) + var capabilities []documentdb.Capability + if dbKind == documentdb.MongoDB && dbVersion == "3.6" { + capabilities = []documentdb.Capability{ + {Name: to.StringPtr("EnableMongo")}, + } + } else { + capabilities = make([]documentdb.Capability, 0) + } + /* * Current state of Locations and CosmosDB properties: * Creating a Database account with CosmosDB requires @@ -84,6 +96,7 @@ func (*AzureCosmosDBManager) CreateOrUpdateCosmosDB( EnableMultipleWriteLocations: to.BoolPtr(false), IsVirtualNetworkFilterEnabled: to.BoolPtr(false), Locations: &locationsArray, + Capabilities: &capabilities, }, } createUpdateFuture, err := cosmosDBClient.CreateOrUpdate( @@ -91,13 +104,13 @@ func (*AzureCosmosDBManager) CreateOrUpdateCosmosDB( if err != nil { // initial create request failed, wrap error - return nil, errhelp.NewAzureErrorAzureError(err) + return nil, err } result, err := createUpdateFuture.Result(cosmosDBClient) if err != nil { // there is no immediate result, wrap error - return &result, errhelp.NewAzureErrorAzureError(err) + return &result, err } return &result, nil } @@ -106,15 +119,15 @@ func (*AzureCosmosDBManager) CreateOrUpdateCosmosDB( func (*AzureCosmosDBManager) GetCosmosDB( ctx context.Context, groupName string, - cosmosDBName string) (*documentdb.DatabaseAccount, *errhelp.AzureError) { + cosmosDBName string) (*documentdb.DatabaseAccount, error) { cosmosDBClient, err := getCosmosDBClient() if err != nil { - return nil, errhelp.NewAzureErrorAzureError(err) + return nil, err } result, err := cosmosDBClient.Get(ctx, groupName, cosmosDBName) if err != nil { - return &result, errhelp.NewAzureErrorAzureError(err) + return &result, err } return &result, nil } @@ -122,15 +135,15 @@ func (*AzureCosmosDBManager) GetCosmosDB( // CheckNameExistsCosmosDB checks if the global account name already exists func (*AzureCosmosDBManager) CheckNameExistsCosmosDB( ctx context.Context, - accountName string) (bool, *errhelp.AzureError) { + accountName string) (bool, error) { cosmosDBClient, err := getCosmosDBClient() if err != nil { - return false, errhelp.NewAzureErrorAzureError(err) + return false, err } response, err := cosmosDBClient.CheckNameExists(ctx, accountName) if err != nil { - return false, errhelp.NewAzureErrorAzureError(err) + return false, err } switch response.StatusCode { @@ -139,7 +152,7 @@ func (*AzureCosmosDBManager) CheckNameExistsCosmosDB( case http.StatusOK: return true, nil default: - return false, errhelp.NewAzureErrorAzureError(fmt.Errorf("unhandled status code for CheckNameExists")) + return false, fmt.Errorf("unhandled status code for CheckNameExists") } } @@ -147,20 +160,38 @@ func (*AzureCosmosDBManager) CheckNameExistsCosmosDB( func (*AzureCosmosDBManager) DeleteCosmosDB( ctx context.Context, groupName string, - cosmosDBName string) (*autorest.Response, *errhelp.AzureError) { + cosmosDBName string) (*autorest.Response, error) { cosmosDBClient, err := getCosmosDBClient() if err != nil { - return nil, errhelp.NewAzureErrorAzureError(err) + return nil, err } deleteFuture, err := cosmosDBClient.Delete(ctx, groupName, cosmosDBName) if err != nil { - return nil, errhelp.NewAzureErrorAzureError(err) + return nil, err } ar, err := deleteFuture.Result(cosmosDBClient) if err != nil { - return nil, errhelp.NewAzureErrorAzureError(err) + return nil, err } return &ar, nil } + +// ListKeys lists the read & write keys for a database account +func (*AzureCosmosDBManager) ListKeys( + ctx context.Context, + groupName string, + accountName string) (*documentdb.DatabaseAccountListKeysResult, error) { + client, err := getCosmosDBClient() + if err != nil { + return nil, err + } + + result, err := client.ListKeys(ctx, groupName, accountName) + if err != nil { + return nil, err + } + + return &result, nil +} diff --git a/pkg/resourcemanager/cosmosdbs/cosmosdb_manager.go b/pkg/resourcemanager/cosmosdbs/cosmosdb_manager.go index f916e70fd54..88e42e8dc31 100644 --- a/pkg/resourcemanager/cosmosdbs/cosmosdb_manager.go +++ b/pkg/resourcemanager/cosmosdbs/cosmosdb_manager.go @@ -8,29 +8,32 @@ import ( "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2015-04-08/documentdb" "github.com/Azure/azure-service-operator/api/v1alpha1" - "github.com/Azure/azure-service-operator/pkg/errhelp" "github.com/Azure/azure-service-operator/pkg/resourcemanager" + "github.com/Azure/azure-service-operator/pkg/secrets" "github.com/Azure/go-autorest/autorest" ) // NewAzureCosmosDBManager creates a new cosmos db client -func NewAzureCosmosDBManager() *AzureCosmosDBManager { - return &AzureCosmosDBManager{} +func NewAzureCosmosDBManager(secretClient secrets.SecretClient) *AzureCosmosDBManager { + return &AzureCosmosDBManager{secretClient} } // CosmosDBManager client functions type CosmosDBManager interface { // CreateOrUpdateCosmosDB creates a new cosmos database account - CreateOrUpdateCosmosDB(ctx context.Context, groupName string, cosmosDBName string, location string, kind v1alpha1.CosmosDBKind, dbType v1alpha1.CosmosDBDatabaseAccountOfferType, tags map[string]*string) (*documentdb.DatabaseAccount, *errhelp.AzureError) + CreateOrUpdateCosmosDB(ctx context.Context, groupName string, cosmosDBName string, location string, kind v1alpha1.CosmosDBKind, dbType v1alpha1.CosmosDBDatabaseAccountOfferType, dbVersion string, tags map[string]*string) (*documentdb.DatabaseAccount, error) // GetCosmosDB gets a cosmos database account - GetCosmosDB(ctx context.Context, groupName string, cosmosDBName string) (*documentdb.DatabaseAccount, *errhelp.AzureError) + GetCosmosDB(ctx context.Context, groupName string, cosmosDBName string) (*documentdb.DatabaseAccount, error) // DeleteCosmosDB removes the cosmos database account - DeleteCosmosDB(ctx context.Context, groupName string, cosmosDBName string) (*autorest.Response, *errhelp.AzureError) + DeleteCosmosDB(ctx context.Context, groupName string, cosmosDBName string) (*autorest.Response, error) // CheckNameExistsCosmosDB check if the account name already exists globally - CheckNameExistsCosmosDB(ctx context.Context, accountName string) (bool, *errhelp.AzureError) + CheckNameExistsCosmosDB(ctx context.Context, accountName string) (bool, error) + + // ListKeys lists the read & write keys for a database account + ListKeys(ctx context.Context, groupName string, accountName string) (*documentdb.DatabaseAccountListKeysResult, error) resourcemanager.ARMClient } diff --git a/pkg/resourcemanager/cosmosdbs/cosmosdb_reconcile.go b/pkg/resourcemanager/cosmosdbs/cosmosdb_reconcile.go index 4eb8f6b0a0f..7885ff5958a 100644 --- a/pkg/resourcemanager/cosmosdbs/cosmosdb_reconcile.go +++ b/pkg/resourcemanager/cosmosdbs/cosmosdb_reconcile.go @@ -6,6 +6,7 @@ package cosmosdbs import ( "context" "fmt" + "strings" "github.com/Azure/azure-service-operator/api/v1alpha1" "github.com/Azure/azure-service-operator/pkg/errhelp" @@ -18,70 +19,74 @@ import ( // Ensure ensures that cosmosdb is provisioned as specified func (m *AzureCosmosDBManager) 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 { + m.SecretClient = options.SecretClient + } + instance, err := m.convert(obj) if err != nil { return false, err } hash := helpers.Hash256(instance.Spec) - if instance.Status.SpecHash != hash { - // need to push a create or update - instance.Status.SpecHash = hash - } else if instance.Status.Provisioned { - // provisioned and no changes needed + + if instance.Status.SpecHash == hash && instance.Status.Provisioned { instance.Status.RequestedAt = nil return true, nil - } else if instance.Status.Provisioning { - // get the instance and update status - db, azerr := m.GetCosmosDB(ctx, instance.Spec.ResourceGroup, instance.Name) - if azerr == nil { - instance.Status.ResourceId = *db.ID - instance.Status.State = *db.ProvisioningState - - if instance.Status.State == "Creating" { - instance.Status.Message = "Waiting for resource to finish creation" - return false, nil - } + } + instance.Status.Provisioned = false - if instance.Status.State == "Succeeded" { - instance.Status.Message = resourcemanager.SuccessMsg - instance.Status.Provisioning = false - instance.Status.Provisioned = true - return true, nil - } + // get the instance and update status + db, err := m.GetCosmosDB(ctx, instance.Spec.ResourceGroup, instance.Name) + if err != nil { + azerr := errhelp.NewAzureErrorAzureError(err) - if instance.Status.State == "Failed" { - instance.Status.Message = "Failed to provision CosmosDB" - instance.Status.Provisioning = false - return true, nil - } - } else if azerr.Type == errhelp.ResourceGroupNotFoundErrorCode { + instance.Status.Message = err.Error() + + switch azerr.Type { + case errhelp.ResourceGroupNotFoundErrorCode, errhelp.ParentNotFoundErrorCode: instance.Status.Provisioning = false - instance.Status.Message = fmt.Sprintf("Waiting for resource group '%s' to be available", instance.Spec.ResourceGroup) instance.Status.State = "Waiting" return false, nil - } else if azerr.Type == errhelp.ResourceNotFound { - exists, azerr := m.CheckNameExistsCosmosDB(ctx, instance.Name) - if azerr != nil { - instance.Status.Provisioning = false - instance.Status.Message = "Unexpected error occurred during resource request" - instance.Status.State = "Failed" - return false, err - } else if exists { - // get request returned resource not found and the name already exists - // so it must exist in a different resource group, user must fix it - instance.Status.Provisioning = false - instance.Status.Message = "CosmosDB name already exists" - instance.Status.State = "Failed" - return true, nil - } - } else { + } + + } else { + instance.Status.ResourceId = *db.ID + instance.Status.State = *db.ProvisioningState + } + + if instance.Status.State == "Creating" { + // avoid multiple CreateOrUpdate requests while resource is already creating + return false, nil + } + + if instance.Status.State == "Succeeded" { + // provisioning is complete, update the secrets + if err = m.createOrUpdateAccountKeysSecret(ctx, instance); err != nil { + instance.Status.Message = err.Error() + return false, err + } + + if instance.Status.SpecHash == hash { + instance.Status.Message = resourcemanager.SuccessMsg instance.Status.Provisioning = false - instance.Status.Message = azerr.Error() - return false, azerr.Original + instance.Status.Provisioned = true + return true, nil } } + if instance.Status.State == "Failed" { + instance.Status.Message = "Failed to provision CosmosDB" + instance.Status.Provisioning = false + instance.Status.Provisioned = false + return true, nil + } + instance.Status.Provisioning = true tags := helpers.LabelsToTags(instance.GetLabels()) @@ -90,97 +95,106 @@ func (m *AzureCosmosDBManager) Ensure(ctx context.Context, obj runtime.Object, o location := instance.Spec.Location kind := instance.Spec.Kind dbType := instance.Spec.Properties.DatabaseAccountOfferType + dbVersion := instance.Spec.Properties.MongoDBVersion - db, azerr := m.CreateOrUpdateCosmosDB(ctx, groupName, accountName, location, kind, dbType, tags) + db, err = m.CreateOrUpdateCosmosDB(ctx, groupName, accountName, location, kind, dbType, dbVersion, tags) + if err != nil { + azerr := errhelp.NewAzureErrorAzureError(err) + instance.Status.Message = err.Error() + + switch azerr.Type { + case errhelp.AsyncOpIncompleteError: + instance.Status.State = "Creating" + instance.Status.Message = "Resource request successfully submitted to Azure" + instance.Status.SpecHash = hash + return false, nil + case errhelp.InvalidResourceLocation, errhelp.LocationNotAvailableForResourceType: + instance.Status.Provisioning = false + instance.Status.Message = azerr.Error() + return true, nil + case errhelp.ResourceGroupNotFoundErrorCode, errhelp.ParentNotFoundErrorCode: + instance.Status.Provisioning = false + case errhelp.NotFoundErrorCode: + nameExists, err := m.CheckNameExistsCosmosDB(ctx, accountName) + if err != nil { + instance.Status.Message = err.Error() + } + if nameExists { + instance.Status.Provisioning = false + instance.Status.Message = "CosmosDB Account name already exists" + return true, nil + } + } - // everything is in a created/updated state - if azerr == nil { - instance.Status.Provisioned = true - instance.Status.Provisioning = false - instance.Status.Message = resourcemanager.SuccessMsg - instance.Status.State = "Succeeded" - instance.Status.ResourceId = *db.ID - return true, nil + return false, err } - switch azerr.Type { - - case errhelp.AsyncOpIncompleteError: - instance.Status.Message = "Resource request successfully submitted to Azure" - instance.Status.State = "Creating" - - case errhelp.InvalidResourceLocation: - instance.Status.Provisioning = false - instance.Status.Message = azerr.Reason - return true, nil - + if err = m.createOrUpdateAccountKeysSecret(ctx, instance); err != nil { + instance.Status.Message = err.Error() + return false, err } + + instance.Status.SpecHash = hash + instance.Status.ResourceId = *db.ID + instance.Status.State = *db.ProvisioningState + instance.Status.Provisioned = true + instance.Status.Provisioning = false + instance.Status.Message = resourcemanager.SuccessMsg return false, nil } // Delete drops cosmosdb func (m *AzureCosmosDBManager) 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 { + m.SecretClient = options.SecretClient + } + instance, err := m.convert(obj) if err != nil { return false, err } - accountName := instance.ObjectMeta.Name - groupName := instance.Spec.ResourceGroup - // if the resource is in a failed state it was never created or could never be verified // so we skip attempting to delete the resrouce from Azure if instance.Status.FailedProvisioning { return false, nil } - notFoundErrors := []string{ - errhelp.NotFoundErrorCode, // happens on first request after deletion succeeds - errhelp.ResourceNotFound, // happens on subsequent requests after deletion succeeds - errhelp.ResourceGroupNotFoundErrorCode, // database doesn't exist in this resource group but the name exists globally - } - - // fetch the latest to inspect provisioning state - cosmosDB, azerr := m.GetCosmosDB(ctx, groupName, accountName) - if azerr != nil { - // deletion finished - if helpers.ContainsString(notFoundErrors, azerr.Type) { - return false, nil - } - - //TODO: are there other errors that need handling here? - instance.Status.Message = azerr.Error() - return true, azerr.Original - } - - instance.Status.State = *cosmosDB.ProvisioningState + groupName := instance.Spec.ResourceGroup + accountName := instance.ObjectMeta.Name - // already deleting the resource, try again later - if instance.Status.State == "Deleting" { - return true, nil - } + // try to delete the cosmosdb instance & secrets + _, err = m.DeleteCosmosDB(ctx, groupName, accountName) + if err != nil { + azerr := errhelp.NewAzureErrorAzureError(err) - // try to delete the cosmosdb instance - _, azerr = m.DeleteCosmosDB(ctx, groupName, accountName) - if azerr != nil { - // this is likely to happen on first try due to not waiting for the future to complete - if azerr.Type == errhelp.AsyncOpIncompleteError { + // request submitted or already in progress + if azerr.Type == errhelp.AsyncOpIncompleteError || (azerr.Type == errhelp.PreconditionFailed && strings.Contains(azerr.Reason, "operation in progress")) { + instance.Status.State = "Deleting" instance.Status.Message = "Deletion request submitted successfully" return true, nil } - // already deleted - if helpers.ContainsString(notFoundErrors, azerr.Type) { - return false, nil + notFound := []string{ + errhelp.NotFoundErrorCode, + errhelp.ResourceNotFound, + errhelp.ResourceGroupNotFoundErrorCode, + } + if helpers.ContainsString(notFound, azerr.Type) { + return false, m.deleteAccountKeysSecret(ctx, instance) } // unhandled error instance.Status.Message = azerr.Error() - return false, azerr.Original + return false, err } - // second delete calls succeed immediately - return false, nil + return false, m.deleteAccountKeysSecret(ctx, instance) } // GetParents returns the parents of cosmosdb @@ -217,3 +231,36 @@ func (m *AzureCosmosDBManager) convert(obj runtime.Object) (*v1alpha1.CosmosDB, } return db, nil } + +func (m *AzureCosmosDBManager) createOrUpdateAccountKeysSecret(ctx context.Context, instance *v1alpha1.CosmosDB) error { + result, err := m.ListKeys(ctx, instance.Spec.ResourceGroup, instance.ObjectMeta.Name) + if err != nil { + return err + } + + secretKey := types.NamespacedName{ + Name: instance.Name, + Namespace: instance.Namespace, + } + secretData := map[string][]byte{ + "primaryConnectionString": []byte(*result.PrimaryMasterKey), + "secondaryConnectionString": []byte(*result.SecondaryMasterKey), + "primaryReadonlyMasterKey": []byte(*result.PrimaryReadonlyMasterKey), + "secondaryReadonlyMasterKey": []byte(*result.SecondaryReadonlyMasterKey), + } + + err = m.SecretClient.Upsert(ctx, secretKey, secretData) + if err != nil { + return err + } + + return nil +} + +func (m *AzureCosmosDBManager) deleteAccountKeysSecret(ctx context.Context, instance *v1alpha1.CosmosDB) error { + secretKey := types.NamespacedName{ + Name: instance.Name, + Namespace: instance.Namespace, + } + return m.SecretClient.Delete(ctx, secretKey) +} diff --git a/pkg/resourcemanager/keyvaults/keyvault.go b/pkg/resourcemanager/keyvaults/keyvault.go index 2522fe0f3af..17fc2e07edd 100644 --- a/pkg/resourcemanager/keyvaults/keyvault.go +++ b/pkg/resourcemanager/keyvaults/keyvault.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "strings" + "time" 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" @@ -21,6 +22,7 @@ import ( "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/to" uuid "github.com/satori/go.uuid" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ) @@ -447,6 +449,15 @@ func (k *azureKeyVaultManager) Ensure(ctx context.Context, obj runtime.Object, o } if helpers.ContainsString(catchUnrecoverableErrors, azerr.Type) { // Unrecoverable error, so stop reconcilation + switch azerr.Type { + case errhelp.AlreadyExists: + timeNow := metav1.NewTime(time.Now()) + if timeNow.Sub(instance.Status.RequestedAt.Time) < (30 * time.Second) { + instance.Status.Provisioning = true + return false, nil + } + + } instance.Status.Message = "Reconcilation hit unrecoverable error " + err.Error() return true, nil } diff --git a/pkg/resourcemanager/storages/storageaccount/storageaccount_reconcile.go b/pkg/resourcemanager/storages/storageaccount/storageaccount_reconcile.go index 45efda9e48c..b21e2e2b682 100644 --- a/pkg/resourcemanager/storages/storageaccount/storageaccount_reconcile.go +++ b/pkg/resourcemanager/storages/storageaccount/storageaccount_reconcile.go @@ -42,11 +42,7 @@ func (sa *azureStorageManager) Ensure(ctx context.Context, obj runtime.Object, o networkAcls = ParseNetworkPolicy(instance.Spec.NetworkRule) } // convert kube labels to expected tag format - labels := map[string]*string{} - for k, v := range instance.GetLabels() { - value := v - labels[k] = &value - } + labels := helpers.LabelsToTags(instance.GetLabels()) hash := "" stor, err := sa.GetStorage(ctx, groupName, name) @@ -132,6 +128,8 @@ func (sa *azureStorageManager) Ensure(ctx context.Context, obj runtime.Object, o instance.Status.Message = "Storage Account Already exists somewhere else" return true, nil } + + instance.Status.Message = "Storage Account already exists and should be available shortly" instance.Status.Provisioning = true }