package couchctl import ( "context" "errors" "fmt" "time" "github.com/go-logr/logr" kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" kuberecorder "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/predicate" dsapi "edge-infra.dev/pkg/edge/datasync/apis/v1alpha1" "edge-infra.dev/pkg/edge/datasync/couchdb" "edge-infra.dev/pkg/k8s/meta/status" "edge-infra.dev/pkg/k8s/runtime/conditions" "edge-infra.dev/pkg/k8s/runtime/controller/metrics" "edge-infra.dev/pkg/k8s/runtime/controller/reconcile" "edge-infra.dev/pkg/k8s/runtime/controller/reconcile/recerr" "edge-infra.dev/pkg/k8s/runtime/patch" ) type CouchDatabaseReconciler struct { client.Client NodeResourcePredicate kuberecorder.EventRecorder Name string Config *Config Metrics metrics.Metrics patchOptions []patch.Option ReconcileConcurrency int } var ( ErrServerNotReady = errors.New("server is not ready yet") ErrNewDatabaseNotFound = errors.New("new database not found") ErrNewDatabaseRolesAndUsersNotFound = errors.New("new database roles and users are not set") databaseConditions = reconcile.Conditions{ Target: status.ReadyCondition, Owned: []string{ dsapi.DatabaseSetupSucceededReason, status.ReadyCondition, status.ReconcilingCondition, status.StalledCondition, }, Summarize: []string{ dsapi.DatabaseSetupSucceededReason, status.StalledCondition, }, NegativePolarity: []string{ status.ReconcilingCondition, status.StalledCondition, }, } ) // SetupWithManager sets up ComputeAddressReconciler with the manager func (r *CouchDatabaseReconciler) SetupWithManager(mgr ctrl.Manager) error { r.patchOptions = getPatchOptions(serverConditions.Owned, r.Name) return ctrl.NewControllerManagedBy(mgr). For(&dsapi.CouchDBDatabase{}, r.databasePredicates()). WithOptions(controller.Options{MaxConcurrentReconciles: r.ReconcileConcurrency}). Complete(r) } func (r *CouchDatabaseReconciler) databasePredicates() builder.Predicates { return builder.WithPredicates( predicate.GenerationChangedPredicate{}, predicate.NewPredicateFuncs(func(obj client.Object) bool { if r.Config.IsDSDS() { return r.ShouldReconcile(r.Config, obj) } return true })) } func (r *CouchDatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) { reconcileStart := time.Now() log := ctrl.LoggerFrom(ctx) database := &dsapi.CouchDBDatabase{} if err := r.Client.Get(ctx, req.NamespacedName, database); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } database.WithRetry(r.Config.RequeueTime) database.WithInterval(r.Config.PollingInterval) log = log.WithValues("dbname", database.Spec.Name) ctx = logr.NewContext(ctx, log) patcher := patch.NewSerialPatcher(database, r.Client) if err := reconcile.Progressing(ctx, database, patcher, r.patchOptions...); err != nil { log.Error(err, "unable to update status") return ctrl.Result{}, err } recResult := reconcile.ResultEmpty var recErr recerr.Error defer func() { summarizer := reconcile.NewSummarizer(patcher) res, err = summarizer.SummarizeAndPatch(ctx, database, []reconcile.SummarizeOption{ reconcile.WithConditions(databaseConditions), reconcile.WithResult(recResult), reconcile.WithError(recErr), reconcile.WithIgnoreNotFound(), reconcile.WithProcessors( reconcile.RecordResult, ), reconcile.WithFieldOwner(r.Name), reconcile.WithEventRecorder(r.EventRecorder), }...) r.Metrics.RecordDuration(ctx, database, reconcileStart) r.Metrics.RecordReadiness(ctx, database) }() recErr = r.reconcile(ctx, database) if recErr != nil { if !couchDBNotReadyOrNotFound(recErr) { recErr.ToCondition(database, dsapi.DatabaseSetupSucceededReason) err = recErr return } //recErr = recerr.NewWait(fmt.Errorf("server not readey"), status.DependencyNotFoundReason, r.Config.ServerNotReady) } recResult = reconcile.ResultSuccess conditions.MarkTrue(database, dsapi.DatabaseSetupSucceededReason, status.SucceededReason, "Successfully created CouchDB Database") log.Info("Successfully created CouchDB Database") return } func (r *CouchDatabaseReconciler) reconcile(ctx context.Context, database *dsapi.CouchDBDatabase) recerr.Error { // attempt to get the couchdbserver ready, server, err := checkIfServerIsReady(ctx, r.Client, database) if err != nil { return recerr.NewWait(err, status.DependencyNotFoundReason, r.Config.ServerNotReady) } if !ready { err := fmt.Errorf("%w", ErrServerNotReady) return recerr.NewWait(err, status.DependencyNotFoundReason, r.Config.ServerNotReady) } return r.reconcileDatabase(ctx, database, server) } func (r *CouchDatabaseReconciler) reconcileDatabase(ctx context.Context, database *dsapi.CouchDBDatabase, server *dsapi.CouchDBServer) recerr.Error { log := logr.FromContextOrDiscard(ctx) spec := database.Spec // get the admin creds creds := &couchdb.AdminCredentials{} adminCreds := server.AdminCredentials() nn := types.NamespacedName{Name: adminCreds.Name, Namespace: adminCreds.Namespace} _, err := creds.FromSecret(ctx, r.Client, nn) switch { case err != nil && kerrors.IsNotFound(err): log.Error(err, "couchdb admin secret not found", "NamespacedName", nn) return recerr.NewWait(err, status.DependencyNotReadyReason, r.Config.ServerNotReady) case err != nil: log.Error(err, "error getting couchdb admin secret", "NamespacedName", nn) return recerr.New(err, status.DependencyInvalidReason) } // create the couchdb driver using the admin credentials cc := &couchdb.CouchDB{} err = cc.New(couchdb.Driver, string(creds.Username), string(creds.Password), server.Spec.URI, r.Config.CouchDBPort) if err != nil { log.Error(err, "error initializing couchdb client", "URI", server.Spec.URI) return recerr.NewWait(err, dsapi.DatabaseCredentialsInvalidReason, r.Config.ServerNotReady) } // defer closing the client defer cc.Close(ctx) // attempt to create the new database err = cc.CreateDB(ctx, spec.Name) if err != nil && !errors.Is(err, couchdb.ErrPreconditionFailed) { log.Error(err, "error creating couchdb database") return recerr.NewWait(err, dsapi.DatabaseCreationFailedReason, r.Config.ServerNotReady) } if server.IsCloud() { // todo this can removed in the next release err = cc.RemoveReadOnly(ctx, spec.Name) if err != nil { log.Error(err, "error removing read-only") } } else { err = cc.MakeReadOnly(ctx, spec.Name) if err != nil && !errors.Is(err, couchdb.ErrPreconditionFailed) { log.Error(err, "error making database read-only", "name", spec.Name) return recerr.NewWait(err, dsapi.DatabaseCreationFailedReason, r.Config.ServerNotReady) } } // check that the DB actually exists in couchdb exists, err := cc.CheckIfDBExists(ctx, spec.Name) if err != nil { log.Error(err, "error checking for couchdb database") return recerr.NewWait(err, dsapi.DatabaseCreationFailedReason, r.Config.ServerNotReady) } if !exists { err := fmt.Errorf("%w", ErrNewDatabaseNotFound) log.Error(err, "expected couchdb database does not exist") return recerr.NewWait(err, dsapi.DatabaseCreationFailedReason, r.Config.DatabaseNotFound) } // TODO create a second user and add then into the members role // https://medium.com/@eiri/couchdb-authorization-in-a-database-58c8ee633c96 // add the user defined roles and members security := couchdbSecurity(database) // add the user defined members / roles to the new db err = cc.AddMemberUserAndRolesToDB(ctx, security, spec.Name) if err != nil { log.Error(err, "error adding admins and members to database") return recerr.NewWait(err, dsapi.DatabaseAddRolesFailedReason, r.Config.DatabaseNotFound) } // verify that the members / roles exist exists, err = cc.CheckDBUsersAndRoles(ctx, security, spec.Name) if err != nil { log.Error(err, "error checking for couchdb users and roles") return recerr.NewWait(err, dsapi.UserCreationFailedReason, r.Config.DatabaseNotFound) } if !exists { err := fmt.Errorf("%w", ErrNewDatabaseRolesAndUsersNotFound) log.Error(err, "expected data members and roles not found") return recerr.NewWait(err, dsapi.DatabaseAddRolesFailedReason, r.Config.DatabaseNotFound) } return nil } func couchdbSecurity(couchDBDatabase *dsapi.CouchDBDatabase) couchdb.Security { dbSec := couchDBDatabase.Spec.Security return couchdb.Security{ Admins: couchdb.NameRole{ Names: dbSec.Admins.Names, Roles: dbSec.Admins.Roles, }, Members: couchdb.NameRole{ Names: dbSec.Members.Names, Roles: dbSec.Members.Roles, }, } }