Skip to content

Commit

Permalink
Merge pull request #854 from jpflueger/cosmosdb-async-controller
Browse files Browse the repository at this point in the history
CosmosDB Async Controller Refactor
  • Loading branch information
jananivMS authored Apr 6, 2020
2 parents 25ad83f + cc17dfe commit d9b4959
Show file tree
Hide file tree
Showing 9 changed files with 416 additions and 257 deletions.
33 changes: 11 additions & 22 deletions api/v1alpha1/cosmosdb_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ type CosmosDBSpec struct {

// +kubebuilder:validation:MinLength=0

Location string `json:"location,omitempty"`
ResourceGroupName 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"`
}

// CosmosDBKind enumerates the values for kind.
Expand All @@ -31,8 +31,10 @@ type CosmosDBSpec struct {
type CosmosDBKind string

const (
// CosmosDBKindGlobalDocumentDB string constant describing global document database
CosmosDBKindGlobalDocumentDB CosmosDBKind = "GlobalDocumentDB"
CosmosDBKindMongoDB CosmosDBKind = "MongoDB"
// CosmosDBKindMongoDB string constant describing mongo database
CosmosDBKindMongoDB CosmosDBKind = "MongoDB"
)

// CosmosDBProperties the CosmosDBProperties of CosmosDB.
Expand All @@ -46,6 +48,7 @@ type CosmosDBProperties struct {
type CosmosDBDatabaseAccountOfferType string

const (
// CosmosDBDatabaseAccountOfferTypeStandard string constant describing standard account offer type
CosmosDBDatabaseAccountOfferTypeStandard CosmosDBDatabaseAccountOfferType = "Standard"
)

Expand All @@ -57,19 +60,6 @@ type CosmosDBLocation struct {
}
*/

type CosmosDBOutput struct {
CosmosDBName string `json:"cosmosDBName,omitempty"`
PrimaryMasterKey string `json:"primaryMasterKey,omitempty"`
//SecondaryMasterKey string `json:"secondaryMasterKey,omitempty"`
//PrimaryReadonlyMasterKey string `json:"primaryReadonlyMasterKey,omitempty"`
//SecondaryReadonlyMasterKey string `json:"secondaryReadonlyMasterKey,omitempty"`
}

// CosmosDBAdditionalResources holds the additional resources
type CosmosDBAdditionalResources struct {
Secrets []string `json:"secrets,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

Expand All @@ -78,10 +68,8 @@ type CosmosDB struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec CosmosDBSpec `json:"spec,omitempty"`
Status ASOStatus `json:"status,omitempty"`
Output CosmosDBOutput `json:"output,omitempty"`
AdditionalResources CosmosDBAdditionalResources `json:"additionalResources,omitempty"`
Spec CosmosDBSpec `json:"spec,omitempty"`
Status ASOStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true
Expand All @@ -98,6 +86,7 @@ func init() {
SchemeBuilder.Register(&CosmosDB{}, &CosmosDBList{})
}

// IsSubmitted function to determine if CosmosDB is provisioning or provisioned
func (cosmosDB *CosmosDB) IsSubmitted() bool {
return cosmosDB.Status.Provisioning || cosmosDB.Status.Provisioned
}
216 changes: 4 additions & 212 deletions controllers/cosmosdb_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,235 +4,27 @@
package controllers

import (
"context"
"fmt"
"os"
"strconv"
"time"

"github.com/go-logr/logr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

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/cosmosdbs"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/record"
"github.com/Azure/azure-service-operator/api/v1alpha1"
)

const cosmosDBFinalizerName = "cosmosdb.finalizers.azure.com"

// CosmosDBReconciler reconciles a CosmosDB object
type CosmosDBReconciler struct {
client.Client
Log logr.Logger
Recorder record.EventRecorder
RequeueTime time.Duration
Reconciler *AsyncReconciler
}

// +kubebuilder:rbac:groups=azure.microsoft.com,resources=cosmosdbs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=azure.microsoft.com,resources=cosmosdbs/status,verbs=get;update;patch

// Reconcile function does the main reconciliation loop of the operator
func (r *CosmosDBReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
log := r.Log.WithValues("cosmosdb", req.NamespacedName)

// Fetch the CosmosDB instance
var instance azurev1alpha1.CosmosDB

requeueAfter, err := strconv.Atoi(os.Getenv("REQUEUE_AFTER"))
if err != nil {
requeueAfter = 30
}

if err := r.Get(ctx, req.NamespacedName, &instance); err != nil {
log.Info("Unable to retrieve cosmosDB resource", "err", err.Error())
// we'll ignore not-found errors, since they can't be fixed by an immediate
// requeue (we'll need to wait for a new notification), and we can get them
// on deleted requests.
return ctrl.Result{}, client.IgnoreNotFound(err)
}

if helpers.IsBeingDeleted(&instance) {
if HasFinalizer(&instance, cosmosDBFinalizerName) {
if err := r.deleteExternal(&instance); err != nil {
log.Info("Error", "Delete CosmosDB failed with ", err)
return ctrl.Result{}, err
}

RemoveFinalizer(&instance, cosmosDBFinalizerName)
if err := r.Update(context.Background(), &instance); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}

if !HasFinalizer(&instance, cosmosDBFinalizerName) {
err := r.addFinalizer(&instance)
if err != nil {
return ctrl.Result{}, fmt.Errorf("error when adding finalizer: %v", err)
}
return ctrl.Result{}, nil
}

if !instance.IsSubmitted() {
err := r.reconcileExternal(&instance)
if err != nil {
catch := []string{
errhelp.ParentNotFoundErrorCode,
errhelp.ResourceGroupNotFoundErrorCode,
}
if helpers.ContainsString(catch, err.(*errhelp.AzureError).Type) {
log.Info("Got ignorable error", "type", err.(*errhelp.AzureError).Type)
return ctrl.Result{Requeue: true, RequeueAfter: time.Second * time.Duration(requeueAfter)}, nil
}
return ctrl.Result{}, fmt.Errorf("error when creating resource in azure: %v", err)
}
return ctrl.Result{}, nil
}
return ctrl.Result{}, nil
}

func (r *CosmosDBReconciler) addFinalizer(instance *azurev1alpha1.CosmosDB) error {
AddFinalizer(instance, cosmosDBFinalizerName)
err := r.Update(context.Background(), instance)
if err != nil {
return fmt.Errorf("failed to update finalizer: %v", err)
}
r.Recorder.Event(instance, v1.EventTypeNormal, "Updated", fmt.Sprintf("finalizer %s added", cosmosDBFinalizerName))
return nil
}

func (r *CosmosDBReconciler) reconcileExternal(instance *azurev1alpha1.CosmosDB) error {
ctx := context.Background()
location := instance.Spec.Location
name := instance.ObjectMeta.Name
groupName := instance.Spec.ResourceGroupName
kind := instance.Spec.Kind
dbType := instance.Spec.Properties.DatabaseAccountOfferType

var err error

// write information back to instance
instance.Status.Provisioning = true
err = r.Update(ctx, instance)
if err != nil {
//log error and kill it
r.Recorder.Event(instance, v1.EventTypeWarning, "Failed", "Unable to update instance")
}
_, err = cosmosdbs.CreateCosmosDB(ctx, groupName, name, location, kind, dbType, nil)
if err != nil {
r.Recorder.Event(instance, v1.EventTypeWarning, "Failed", "Couldn't create resource in azure")
instance.Status.Provisioning = false
errUpdate := r.Update(ctx, instance)
if errUpdate != nil {
//log error and kill it
r.Recorder.Event(instance, v1.EventTypeWarning, "Failed", "Unable to update instance")
}
return errhelp.NewAzureError(err)
}
instance.Status.Provisioning = false
instance.Status.Provisioned = true

err = r.Update(ctx, instance)
if err != nil {
r.Recorder.Event(instance, v1.EventTypeWarning, "Failed", "Unable to update instance")
}
r.Recorder.Event(instance, v1.EventTypeNormal, "Updated", name+" provisioned")

return nil
}

func (r *CosmosDBReconciler) deleteExternal(instance *azurev1alpha1.CosmosDB) error {
ctx := context.Background()
name := instance.ObjectMeta.Name
groupName := instance.Spec.ResourceGroupName
_, err := cosmosdbs.DeleteCosmosDB(ctx, groupName, name)
if err != nil {
if errhelp.IsStatusCode204(err) {
r.Recorder.Event(instance, v1.EventTypeWarning, "DoesNotExist", "Resource to delete does not exist")
return nil
}

r.Recorder.Event(instance, v1.EventTypeWarning, "Failed", "Couldn't delete resource in azure")
return err
}

r.Recorder.Event(instance, v1.EventTypeNormal, "Deleted", name+" deleted")
return nil
return r.Reconciler.Reconcile(req, &v1alpha1.CosmosDB{})
}

// SetupWithManager sets up the controller functions
func (r *CosmosDBReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&azurev1alpha1.CosmosDB{}).
For(&v1alpha1.CosmosDB{}).
Complete(r)
}

/* Below code was from prior to refactor.
Left here for future reference for pulling out values post deployment.
func (r *CosmosDBReconciler) updateStatus(req ctrl.Request, resourceGroupName, deploymentName, provisioningState string, outputs interface{}) (*servicev1alpha1.CosmosDB, error) {
ctx := context.Background()
log := r.Log.WithValues("cosmosdb", req.NamespacedName)
resource := &servicev1alpha1.CosmosDB{}
r.Get(ctx, req.NamespacedName, resource)
log.Info("Getting CosmosDB Account", "CosmosDB.Namespace", resource.Namespace, "CosmosDB.Name", resource.Name)
resourceCopy := resource.DeepCopy()
resourceCopy.Status.DeploymentName = deploymentName
resourceCopy.Status.ProvisioningState = provisioningState
err := r.Status().Update(ctx, resourceCopy)
if err != nil {
log.Error(err, "unable to update CosmosDB status")
return nil, err
}
log.V(1).Info("Updated Status", "CosmosDB.Namespace", resourceCopy.Namespace, "CosmosDB.Name", resourceCopy.Name, "CosmosDB.Status", resourceCopy.Status)
if helpers.IsDeploymentComplete(provisioningState) {
if outputs != nil {
resourceCopy.Output.CosmosDBName = helpers.GetOutput(outputs, "cosmosDBName")
resourceCopy.Output.PrimaryMasterKey = helpers.GetOutput(outputs, "primaryMasterKey")
}
err := r.syncAdditionalResourcesAndOutput(req, resourceCopy)
if err != nil {
log.Error(err, "error syncing resources")
return nil, err
}
log.V(1).Info("Updated additional resources", "CosmosDB.Namespace", resourceCopy.Namespace, "CosmosDB.Name", resourceCopy.Name, "CosmosDB.AdditionalResources", resourceCopy.AdditionalResources, "CosmosDB.Output", resourceCopy.Output)
}
return resourceCopy, nil
}
func (r *CosmosDBReconciler) syncAdditionalResourcesAndOutput(req ctrl.Request, s *servicev1alpha1.CosmosDB) (err error) {
ctx := context.Background()
log := r.Log.WithValues("cosmosdb", req.NamespacedName)
secrets := []string{}
secretData := map[string]string{
"cosmosDBName": "{{.Obj.Output.CosmosDBName}}",
"primaryMasterKey": "{{.Obj.Output.PrimaryMasterKey}}",
}
secret := helpers.CreateSecret(s, s.Name, s.Namespace, secretData)
secrets = append(secrets, secret)
resourceCopy := s.DeepCopy()
resourceCopy.AdditionalResources.Secrets = secrets
err = r.Update(ctx, resourceCopy)
if err != nil {
log.Error(err, "unable to update CosmosDB status")
return err
}
return nil
}*/
44 changes: 44 additions & 0 deletions controllers/cosmosdb_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// +build all cosmos

package controllers

import (
"context"
"testing"

"github.com/Azure/azure-service-operator/api/v1alpha1"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestCosmosDBHappyPath(t *testing.T) {
t.Parallel()
defer PanicRecover(t)
ctx := context.Background()

cosmosDBAccountName := GenerateTestResourceNameWithRandom("cosmosdb", 8)
cosmosDBNamespace := "default"

dbInstance := &v1alpha1.CosmosDB{
ObjectMeta: metav1.ObjectMeta{
Name: cosmosDBAccountName,
Namespace: cosmosDBNamespace,
},
Spec: v1alpha1.CosmosDBSpec{
Location: tc.resourceGroupLocation,
ResourceGroup: tc.resourceGroupName,
Kind: v1alpha1.CosmosDBKindGlobalDocumentDB,
Properties: v1alpha1.CosmosDBProperties{
DatabaseAccountOfferType: v1alpha1.CosmosDBDatabaseAccountOfferTypeStandard,
},
},
}

EnsureInstance(ctx, t, tc, dbInstance)

EnsureDelete(ctx, t, tc, dbInstance)

}
2 changes: 1 addition & 1 deletion controllers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ func EnsureDelete(ctx context.Context, t *testing.T, tc TestContext, instance ru
assert.Eventually(func() bool {
err = tc.k8sClient.Get(ctx, names, instance)
return apierrors.IsNotFound(err)
}, tc.timeoutFast, tc.retry, fmt.Sprintf("wait for %s to be gone from k8s", typeOf))
}, tc.timeout, tc.retry, fmt.Sprintf("wait for %s to be gone from k8s", typeOf))

}

Expand Down
Loading

0 comments on commit d9b4959

Please sign in to comment.