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
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
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
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
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
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
184 defer cc.Close(ctx)
185
186
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() {
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
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
219
220
221
222 security := couchdbSecurity(database)
223
224
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
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