1
16
17 package clientctl
18
19 import (
20 "context"
21 "errors"
22 "fmt"
23 "net/http"
24 "time"
25
26 apiv1 "k8s.io/api/core/v1"
27
28 apierrs "k8s.io/apimachinery/pkg/api/errors"
29 "k8s.io/apimachinery/pkg/runtime"
30 "k8s.io/apimachinery/pkg/types"
31
32 ctrl "sigs.k8s.io/controller-runtime"
33
34 "sigs.k8s.io/controller-runtime/pkg/builder"
35 kclient "sigs.k8s.io/controller-runtime/pkg/client"
36 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
37 "sigs.k8s.io/controller-runtime/pkg/predicate"
38
39 logger "sigs.k8s.io/controller-runtime/pkg/log"
40
41 "github.com/go-kivik/kivik/v4"
42 "github.com/go-kivik/kivik/v4/couchdb"
43 "github.com/go-logr/logr"
44
45 api "edge-infra.dev/pkg/edge/iam/api/v1alpha1"
46 iamclient "edge-infra.dev/pkg/edge/iam/client"
47 "edge-infra.dev/pkg/edge/iam/config"
48 "edge-infra.dev/pkg/edge/iam/storage/database"
49 "edge-infra.dev/pkg/k8s/runtime/controller/metrics"
50 "edge-infra.dev/pkg/lib/logging"
51 )
52
53 const (
54 EdgeIDFinalizer = "finalizers.id.edge.ncr.com"
55 ClientRegistrationSucceeded = "ClientRegistrationSucceeded"
56 MissingSecretData = "MissingSecretData"
57 ClientRegistrationFailed = "ClientRegistrationFailed"
58 ClientSecretExistFailure = "ClientSecretExistFailure"
59 ClientSecretCreationFailure = "ClientSecretCreationFailure"
60 SaveClientCredentailsFailed = "SaveClientCredentailsFailed"
61 CouchDBNotReady = "CouchDBNotReady"
62 )
63
64
65 type ClientReconciler struct {
66 kclient.Client
67 Scheme *runtime.Scheme
68 Name string
69
70 Metrics metrics.Metrics
71 }
72
73 var clientStorer iamclient.Storage
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88 func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
89 var (
90 reconcileStart = time.Now()
91 log = logger.FromContext(ctx)
92 client = api.Client{}
93 )
94
95 log.V(2).Info("reconcile client", "req", req)
96
97
98 if err := r.Get(ctx, req.NamespacedName, &client); err != nil {
99
100 return ctrl.Result{}, kclient.IgnoreNotFound(err)
101 }
102
103 defer func() {
104 r.Metrics.RecordReadiness(ctx, &client)
105 r.Metrics.RecordDuration(ctx, &client, reconcileStart)
106 }()
107
108 if !controllerutil.ContainsFinalizer(&client, EdgeIDFinalizer) {
109 controllerutil.AddFinalizer(&client, EdgeIDFinalizer)
110
111 err := r.Update(ctx, &client)
112 if err != nil {
113 log.Error(err, "error updating client resource after adding finalizer")
114 }
115 return ctrl.Result{Requeue: true}, nil
116 }
117 if !client.ObjectMeta.DeletionTimestamp.IsZero() {
118 err = r.finalize(ctx, req, &client)
119 if err != nil {
120 log.Error(err, "error executing finalizer on client resource")
121 return ctrl.Result{Requeue: true}, nil
122 }
123 return ctrl.Result{}, nil
124 }
125 var reconciled api.Client
126 up, reconcileErr := isCouchDBUp()
127 if up {
128
129
130 reconciled, reconcileErr = r.reconcile(ctx, req, *client.DeepCopy())
131 } else {
132 reconciled = api.MarkNotReady(reconciled, CouchDBNotReady, reconcileErr.Error())
133 log.Info("database not ready yet, will try again soon", "error", reconcileErr)
134 }
135
136
137 if err := r.updateClientStatus(ctx, req, reconciled.Status); err != nil {
138 log.Info("failed to update client status. will retry soon", "status", reconciled.Status)
139 return ctrl.Result{Requeue: true}, nil
140 }
141
142
143
144
145 if !up {
146 return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
147 }
148 if reconcileErr != nil {
149 log.Error(reconcileErr, "error reconciling client resource")
150 return ctrl.Result{Requeue: true}, nil
151 }
152 return ctrl.Result{}, nil
153 }
154
155 func (r *ClientReconciler) finalize(ctx context.Context, req ctrl.Request, client *api.Client) error {
156 log := logr.FromContextOrDiscard(ctx)
157 log.Info("running finalizer")
158 log.Info("unregistering client from DB")
159
160 if unregisterErr := r.unregisterClient(ctx, req); unregisterErr != nil {
161 return unregisterErr
162 }
163 controllerutil.RemoveFinalizer(client, EdgeIDFinalizer)
164 err := r.Update(ctx, client)
165 if err != nil {
166 return err
167 }
168 log.Info("executed finalizer")
169 return nil
170 }
171
172
173 func (r *ClientReconciler) SetupWithManager(mgr ctrl.Manager) error {
174 return ctrl.NewControllerManagedBy(mgr).
175 For(&api.Client{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
176 Complete(r)
177 }
178
179 func (r *ClientReconciler) reconcile(ctx context.Context, req ctrl.Request, client api.Client) (api.Client, error) {
180 var secret *apiv1.Secret
181 var err error
182
183 log := logger.FromContext(ctx)
184
185 client, secret, err = r.reconcileClientSecret(ctx, req, client)
186 if err != nil {
187 return client, err
188 }
189
190
191 client, err = r.validateClientSecret(client, secret)
192 if err != nil {
193 log.Error(err, fmt.Sprintf("secret %s/%s is invalid", secret.Name, secret.Namespace))
194 return client, err
195 }
196
197
198 err = r.saveClientCredentials(ctx, secret)
199 if err != nil {
200 return api.MarkNotReady(client, SaveClientCredentailsFailed, err.Error()), err
201 }
202
203
204 client, err = r.registerClient(ctx, req, client, secret)
205 if err != nil {
206 return client, err
207 }
208
209
210 return api.MarkReady(client, ClientRegistrationSucceeded, "successfully registered the client"), nil
211 }
212
213 func clientSecretExists(ctx context.Context, req ctrl.Request, c kclient.Client, secretName string) (*apiv1.Secret, error) {
214 clientSecret := &apiv1.Secret{}
215 err := c.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: secretName}, clientSecret)
216 switch {
217 case err == nil:
218 return clientSecret, nil
219 case apierrs.IsNotFound(err):
220 return nil, nil
221 default:
222 return nil, fmt.Errorf("failed to check if client secret exists: %w", err)
223 }
224 }
225
226 func (r *ClientReconciler) validateClientSecret(client api.Client, secret *apiv1.Secret) (api.Client, error) {
227 _, found := secret.Data["client_id"]
228 if !found {
229 e := errors.New(`"client_id property missing"`)
230 return api.MarkNotReady(client, MissingSecretData, e.Error()), e
231 }
232
233 _, found = secret.Data["client_secret"]
234 if !found {
235 e := errors.New(`"client_secret property missing"`)
236 return api.MarkNotReady(client, MissingSecretData, e.Error()), e
237 }
238
239 return client, nil
240 }
241
242
243 func isCouchDBUp() (bool, error) {
244 log := logging.NewLogger().WithName("isCouchDBUp")
245 couchURI := config.CouchDBAddress()
246 client := &http.Client{Transport: &http.Transport{DisableKeepAlives: true}}
247 couchDBClient, err := kivik.New("couch", couchURI, couchdb.BasicAuth(config.CouchDBUser(), config.CouchDBPassword()), couchdb.OptionHTTPClient(client))
248 if err != nil {
249 return false, err
250 }
251 defer func(couchDBClient *kivik.Client) {
252 err := couchDBClient.Close()
253 if err != nil {
254 log.Error(err, "Failed to close CouchDB client")
255 }
256 }(couchDBClient)
257
258 online, err := couchDBClient.Ping(context.Background())
259 if err != nil {
260 log.Error(err, "couchClient.Ping error")
261 }
262 return online, err
263 }
264
265 func getClientStorer(log logr.Logger) (iamclient.Storage, error) {
266 if clientStorer == nil {
267 var err error
268 clientStorer, err = database.NewOperatorStore(log)
269 if err != nil {
270 return nil, err
271 }
272 }
273 return clientStorer, nil
274 }
275
276
277
278
279
280 func (r *ClientReconciler) updateClientStatus(ctx context.Context, req ctrl.Request, status api.ClientStatus) error {
281 var c api.Client
282 if err := r.Get(ctx, req.NamespacedName, &c); err != nil {
283 return err
284 }
285
286 patch := kclient.MergeFrom(c.DeepCopy())
287 c.Status = status
288
289 return r.Status().Patch(ctx, &c, patch)
290 }
291
View as plain text