...

Source file src/edge-infra.dev/pkg/edge/datasync/controllers/couchctl/database_controller.go

Documentation: edge-infra.dev/pkg/edge/datasync/controllers/couchctl

     1  package couchctl
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"time"
     8  
     9  	"github.com/go-logr/logr"
    10  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    11  	"k8s.io/apimachinery/pkg/types"
    12  	kuberecorder "k8s.io/client-go/tools/record"
    13  	ctrl "sigs.k8s.io/controller-runtime"
    14  	"sigs.k8s.io/controller-runtime/pkg/builder"
    15  	"sigs.k8s.io/controller-runtime/pkg/client"
    16  	"sigs.k8s.io/controller-runtime/pkg/controller"
    17  	"sigs.k8s.io/controller-runtime/pkg/predicate"
    18  
    19  	dsapi "edge-infra.dev/pkg/edge/datasync/apis/v1alpha1"
    20  	"edge-infra.dev/pkg/edge/datasync/couchdb"
    21  	"edge-infra.dev/pkg/k8s/meta/status"
    22  	"edge-infra.dev/pkg/k8s/runtime/conditions"
    23  	"edge-infra.dev/pkg/k8s/runtime/controller/metrics"
    24  	"edge-infra.dev/pkg/k8s/runtime/controller/reconcile"
    25  	"edge-infra.dev/pkg/k8s/runtime/controller/reconcile/recerr"
    26  	"edge-infra.dev/pkg/k8s/runtime/patch"
    27  )
    28  
    29  type CouchDatabaseReconciler struct {
    30  	client.Client
    31  	NodeResourcePredicate
    32  	kuberecorder.EventRecorder
    33  	Name                 string
    34  	Config               *Config
    35  	Metrics              metrics.Metrics
    36  	patchOptions         []patch.Option
    37  	ReconcileConcurrency int
    38  }
    39  
    40  var (
    41  	ErrServerNotReady                   = errors.New("server is not ready yet")
    42  	ErrNewDatabaseNotFound              = errors.New("new database not found")
    43  	ErrNewDatabaseRolesAndUsersNotFound = errors.New("new database roles and users are not set")
    44  
    45  	databaseConditions = reconcile.Conditions{
    46  		Target: status.ReadyCondition,
    47  		Owned: []string{
    48  			dsapi.DatabaseSetupSucceededReason,
    49  			status.ReadyCondition,
    50  			status.ReconcilingCondition,
    51  			status.StalledCondition,
    52  		},
    53  		Summarize: []string{
    54  			dsapi.DatabaseSetupSucceededReason,
    55  			status.StalledCondition,
    56  		},
    57  		NegativePolarity: []string{
    58  			status.ReconcilingCondition,
    59  			status.StalledCondition,
    60  		},
    61  	}
    62  )
    63  
    64  // SetupWithManager sets up ComputeAddressReconciler with the manager
    65  func (r *CouchDatabaseReconciler) SetupWithManager(mgr ctrl.Manager) error {
    66  	r.patchOptions = getPatchOptions(serverConditions.Owned, r.Name)
    67  	return ctrl.NewControllerManagedBy(mgr).
    68  		For(&dsapi.CouchDBDatabase{}, r.databasePredicates()).
    69  		WithOptions(controller.Options{MaxConcurrentReconciles: r.ReconcileConcurrency}).
    70  		Complete(r)
    71  }
    72  
    73  func (r *CouchDatabaseReconciler) databasePredicates() builder.Predicates {
    74  	return builder.WithPredicates(
    75  		predicate.GenerationChangedPredicate{},
    76  		predicate.NewPredicateFuncs(func(obj client.Object) bool {
    77  			if r.Config.IsDSDS() {
    78  				return r.ShouldReconcile(r.Config, obj)
    79  			}
    80  			return true
    81  		}))
    82  }
    83  
    84  func (r *CouchDatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
    85  	reconcileStart := time.Now()
    86  	log := ctrl.LoggerFrom(ctx)
    87  
    88  	database := &dsapi.CouchDBDatabase{}
    89  	if err := r.Client.Get(ctx, req.NamespacedName, database); err != nil {
    90  		return ctrl.Result{}, client.IgnoreNotFound(err)
    91  	}
    92  	database.WithRetry(r.Config.RequeueTime)
    93  	database.WithInterval(r.Config.PollingInterval)
    94  
    95  	log = log.WithValues("dbname", database.Spec.Name)
    96  	ctx = logr.NewContext(ctx, log)
    97  
    98  	patcher := patch.NewSerialPatcher(database, r.Client)
    99  	if err := reconcile.Progressing(ctx, database, patcher, r.patchOptions...); err != nil {
   100  		log.Error(err, "unable to update status")
   101  		return ctrl.Result{}, err
   102  	}
   103  
   104  	recResult := reconcile.ResultEmpty
   105  	var recErr recerr.Error
   106  
   107  	defer func() {
   108  		summarizer := reconcile.NewSummarizer(patcher)
   109  		res, err = summarizer.SummarizeAndPatch(ctx, database, []reconcile.SummarizeOption{
   110  			reconcile.WithConditions(databaseConditions),
   111  			reconcile.WithResult(recResult),
   112  			reconcile.WithError(recErr),
   113  			reconcile.WithIgnoreNotFound(),
   114  			reconcile.WithProcessors(
   115  				reconcile.RecordResult,
   116  			),
   117  			reconcile.WithFieldOwner(r.Name),
   118  			reconcile.WithEventRecorder(r.EventRecorder),
   119  		}...)
   120  		r.Metrics.RecordDuration(ctx, database, reconcileStart)
   121  		r.Metrics.RecordReadiness(ctx, database)
   122  	}()
   123  
   124  	recErr = r.reconcile(ctx, database)
   125  
   126  	if recErr != nil {
   127  		if !couchDBNotReadyOrNotFound(recErr) {
   128  			recErr.ToCondition(database, dsapi.DatabaseSetupSucceededReason)
   129  			err = recErr
   130  			return
   131  		}
   132  		//recErr = recerr.NewWait(fmt.Errorf("server not readey"), status.DependencyNotFoundReason, r.Config.ServerNotReady)
   133  	}
   134  	recResult = reconcile.ResultSuccess
   135  	conditions.MarkTrue(database, dsapi.DatabaseSetupSucceededReason, status.SucceededReason, "Successfully created CouchDB Database")
   136  	log.Info("Successfully created CouchDB Database")
   137  
   138  	return
   139  }
   140  
   141  func (r *CouchDatabaseReconciler) reconcile(ctx context.Context, database *dsapi.CouchDBDatabase) recerr.Error {
   142  	// attempt to get the couchdbserver
   143  	ready, server, err := checkIfServerIsReady(ctx, r.Client, database)
   144  	if err != nil {
   145  		return recerr.NewWait(err, status.DependencyNotFoundReason, r.Config.ServerNotReady)
   146  	}
   147  
   148  	if !ready {
   149  		err := fmt.Errorf("%w", ErrServerNotReady)
   150  		return recerr.NewWait(err, status.DependencyNotFoundReason, r.Config.ServerNotReady)
   151  	}
   152  
   153  	return r.reconcileDatabase(ctx, database, server)
   154  }
   155  
   156  func (r *CouchDatabaseReconciler) reconcileDatabase(ctx context.Context, database *dsapi.CouchDBDatabase, server *dsapi.CouchDBServer) recerr.Error {
   157  	log := logr.FromContextOrDiscard(ctx)
   158  	spec := database.Spec
   159  
   160  	// get the admin creds
   161  	creds := &couchdb.AdminCredentials{}
   162  	adminCreds := server.AdminCredentials()
   163  	nn := types.NamespacedName{Name: adminCreds.Name, Namespace: adminCreds.Namespace}
   164  	_, err := creds.FromSecret(ctx, r.Client, nn)
   165  	switch {
   166  	case err != nil && kerrors.IsNotFound(err):
   167  		log.Error(err, "couchdb admin secret not found", "NamespacedName", nn)
   168  		return recerr.NewWait(err, status.DependencyNotReadyReason, r.Config.ServerNotReady)
   169  	case err != nil:
   170  		log.Error(err, "error getting couchdb admin secret", "NamespacedName", nn)
   171  		return recerr.New(err, status.DependencyInvalidReason)
   172  	}
   173  
   174  	// create the couchdb driver using the admin credentials
   175  	cc := &couchdb.CouchDB{}
   176  	err = cc.New(couchdb.Driver, string(creds.Username), string(creds.Password), server.Spec.URI, r.Config.CouchDBPort)
   177  
   178  	if err != nil {
   179  		log.Error(err, "error initializing couchdb client", "URI", server.Spec.URI)
   180  		return recerr.NewWait(err, dsapi.DatabaseCredentialsInvalidReason, r.Config.ServerNotReady)
   181  	}
   182  
   183  	// defer closing the client
   184  	defer cc.Close(ctx)
   185  
   186  	// attempt to create the new database
   187  	err = cc.CreateDB(ctx, spec.Name)
   188  	if err != nil && !errors.Is(err, couchdb.ErrPreconditionFailed) {
   189  		log.Error(err, "error creating couchdb database")
   190  		return recerr.NewWait(err, dsapi.DatabaseCreationFailedReason, r.Config.ServerNotReady)
   191  	}
   192  
   193  	if server.IsCloud() { // todo this can removed in the next release
   194  		err = cc.RemoveReadOnly(ctx, spec.Name)
   195  		if err != nil {
   196  			log.Error(err, "error removing read-only")
   197  		}
   198  	} else {
   199  		err = cc.MakeReadOnly(ctx, spec.Name)
   200  		if err != nil && !errors.Is(err, couchdb.ErrPreconditionFailed) {
   201  			log.Error(err, "error making database read-only", "name", spec.Name)
   202  			return recerr.NewWait(err, dsapi.DatabaseCreationFailedReason, r.Config.ServerNotReady)
   203  		}
   204  	}
   205  
   206  	// check that the DB actually exists in couchdb
   207  	exists, err := cc.CheckIfDBExists(ctx, spec.Name)
   208  	if err != nil {
   209  		log.Error(err, "error checking for couchdb database")
   210  		return recerr.NewWait(err, dsapi.DatabaseCreationFailedReason, r.Config.ServerNotReady)
   211  	}
   212  	if !exists {
   213  		err := fmt.Errorf("%w", ErrNewDatabaseNotFound)
   214  		log.Error(err, "expected couchdb database does not exist")
   215  		return recerr.NewWait(err, dsapi.DatabaseCreationFailedReason, r.Config.DatabaseNotFound)
   216  	}
   217  
   218  	// TODO create a second user and add then into the members role
   219  	// https://medium.com/@eiri/couchdb-authorization-in-a-database-58c8ee633c96
   220  
   221  	// add the user defined roles and members
   222  	security := couchdbSecurity(database)
   223  
   224  	// add the user defined members / roles to the new db
   225  	err = cc.AddMemberUserAndRolesToDB(ctx, security, spec.Name)
   226  	if err != nil {
   227  		log.Error(err, "error adding admins and members to database")
   228  		return recerr.NewWait(err, dsapi.DatabaseAddRolesFailedReason, r.Config.DatabaseNotFound)
   229  	}
   230  
   231  	// verify that the members / roles exist
   232  	exists, err = cc.CheckDBUsersAndRoles(ctx, security, spec.Name)
   233  	if err != nil {
   234  		log.Error(err, "error checking for couchdb users and roles")
   235  		return recerr.NewWait(err, dsapi.UserCreationFailedReason, r.Config.DatabaseNotFound)
   236  	}
   237  	if !exists {
   238  		err := fmt.Errorf("%w", ErrNewDatabaseRolesAndUsersNotFound)
   239  		log.Error(err, "expected data members and roles not found")
   240  		return recerr.NewWait(err, dsapi.DatabaseAddRolesFailedReason, r.Config.DatabaseNotFound)
   241  	}
   242  
   243  	return nil
   244  }
   245  
   246  func couchdbSecurity(couchDBDatabase *dsapi.CouchDBDatabase) couchdb.Security {
   247  	dbSec := couchDBDatabase.Spec.Security
   248  	return couchdb.Security{
   249  		Admins: couchdb.NameRole{
   250  			Names: dbSec.Admins.Names,
   251  			Roles: dbSec.Admins.Roles,
   252  		},
   253  		Members: couchdb.NameRole{
   254  			Names: dbSec.Members.Names,
   255  			Roles: dbSec.Members.Roles,
   256  		},
   257  	}
   258  }
   259  

View as plain text