forked from netgroup-polito/CrownLabs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathworkspace_controller.go
412 lines (358 loc) · 16.4 KB
/
workspace_controller.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
// Copyright 2020-2024 Politecnico di Torino
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tenant_controller
import (
"context"
"fmt"
"time"
v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlUtil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
crownlabsv1alpha1 "github.com/netgroup-polito/CrownLabs/operators/api/v1alpha1"
crownlabsv1alpha2 "github.com/netgroup-polito/CrownLabs/operators/api/v1alpha2"
"github.com/netgroup-polito/CrownLabs/operators/pkg/utils"
)
// WorkspaceReconciler reconciles a Workspace object.
type WorkspaceReconciler struct {
client.Client
Scheme *runtime.Scheme
KcA *KcActor
TargetLabelKey string
TargetLabelValue string
RequeueTimeMinimum time.Duration
RequeueTimeMaximum time.Duration
// This function, if configured, is deferred at the beginning of the Reconcile.
// Specifically, it is meant to be set to GinkgoRecover during the tests,
// in order to lead to a controlled failure in case the Reconcile panics.
ReconcileDeferHook func()
}
// Reconcile reconciles the state of a workspace resource.
func (r *WorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
if r.ReconcileDeferHook != nil {
defer r.ReconcileDeferHook()
}
var ws crownlabsv1alpha1.Workspace
if err := r.Get(ctx, req.NamespacedName, &ws); client.IgnoreNotFound(err) != nil {
klog.Errorf("Error when getting workspace %s before starting reconcile -> %s", ws.Name, err)
return ctrl.Result{}, err
} else if err != nil {
klog.Infof("Workspace %s deleted", req.Name)
return ctrl.Result{}, client.IgnoreNotFound(err)
}
if ws.Labels[r.TargetLabelKey] != r.TargetLabelValue {
// if entered here it means that is in the reconcile
// which has been requed after
// the last successful one with the old target label
return ctrl.Result{}, nil
}
var retrigErr error
if !ws.ObjectMeta.DeletionTimestamp.IsZero() {
klog.Infof("Processing deletion of workspace %s", ws.Name)
// workspace is being deleted
if ctrlUtil.ContainsFinalizer(&ws, crownlabsv1alpha2.TnOperatorFinalizerName) {
// our finalizer is present, so lets handle any external dependency
if err := r.handleDeletion(ctx, ws.Name, ws.Spec.PrettyName); err != nil {
klog.Errorf("Error when deleting resources handled by workspace %s -> %s", ws.Name, err)
retrigErr = err
}
// can remove the finalizer from the workspace if the eternal resources have been successfully deleted
if retrigErr == nil {
// remove finalizer from the workspace
ctrlUtil.RemoveFinalizer(&ws, crownlabsv1alpha2.TnOperatorFinalizerName)
if err := r.Update(context.Background(), &ws); err != nil {
klog.Errorf("Error when removing tenant operator finalizer from workspace %s -> %s", ws.Name, err)
retrigErr = err
tnOpinternalErrors.WithLabelValues("workspace", "self-update").Inc()
}
}
}
if retrigErr == nil {
klog.Infof("Workspace %s ready for deletion", ws.Name)
} else {
klog.Errorf("Error when preparing workspace %s for deletion, need to retry -> %s", ws.Name, retrigErr)
}
return ctrl.Result{}, retrigErr
}
// workspace is NOT being deleted
klog.Infof("Reconciling workspace %s", ws.Name)
// add tenant operator finalizer to workspace
if !ctrlUtil.ContainsFinalizer(&ws, crownlabsv1alpha2.TnOperatorFinalizerName) {
ctrlUtil.AddFinalizer(&ws, crownlabsv1alpha2.TnOperatorFinalizerName)
if err := r.Update(context.Background(), &ws); err != nil {
klog.Errorf("Error when adding finalizer to workspace %s -> %s", ws.Name, err)
retrigErr = err
}
}
nsName := fmt.Sprintf("workspace-%s", ws.Name)
nsOk, err := r.createOrUpdateClusterResources(ctx, &ws, nsName)
if nsOk {
klog.Infof("Namespace %s for tenant %s updated", nsName, ws.Name)
ws.Status.Namespace.Created = true
ws.Status.Namespace.Name = nsName
if err != nil {
klog.Errorf("Unable to update cluster resource of tenant %s -> %s", ws.Name, err)
retrigErr = err
tnOpinternalErrors.WithLabelValues("workspace", "cluster-resources").Inc()
}
klog.Infof("Cluster resources for tenant %s updated", ws.Name)
} else {
klog.Errorf("Unable to update namespace of tenant %s -> %s", ws.Name, err)
ws.Status.Namespace.Created = false
ws.Status.Namespace.Name = ""
retrigErr = err
tnOpinternalErrors.WithLabelValues("workspace", "cluster-resources").Inc()
}
// handling autoEnrollment
err = r.handleAutoEnrollment(ctx, &ws)
if err != nil {
klog.Errorf("Error when handling autoEnrollment for workspace %s -> %s", ws.Name, err)
retrigErr = err
tnOpinternalErrors.WithLabelValues("workspace", "auto-enrollment").Inc()
}
if ws.Status.Subscriptions == nil {
// len 1 of the map is for the number of subscriptions (keycloak)
ws.Status.Subscriptions = make(map[string]crownlabsv1alpha2.SubscriptionStatus, 1)
}
// handling keycloak resources
if r.KcA == nil {
// KcA could be nil for local testing skipping the keycloak subscription
klog.Warningf("Skipping creation/update of roles in keycloak for workspace %s", ws.Name)
ws.Status.Subscriptions["keycloak"] = crownlabsv1alpha2.SubscrFailed
} else if err = r.KcA.createKcRoles(ctx, genWsKcRolesData(ws.Name, ws.Spec.PrettyName)); err != nil {
klog.Errorf("Error when creating roles for workspace %s -> %s", ws.Name, err)
ws.Status.Subscriptions["keycloak"] = crownlabsv1alpha2.SubscrFailed
retrigErr = err
tnOpinternalErrors.WithLabelValues("workspace", "keycloak").Inc()
} else {
klog.Infof("Roles for workspace %s created successfully", ws.Name)
ws.Status.Subscriptions["keycloak"] = crownlabsv1alpha2.SubscrOk
}
ws.Status.Ready = retrigErr == nil
// update status before exiting reconcile
if err = r.Status().Update(ctx, &ws); err != nil {
// if status update fails, still try to reconcile later
klog.Errorf("Unable to update status of workspace %s before exiting reconciler -> %s", ws.Name, err)
tnOpinternalErrors.WithLabelValues("workspace", "self-update").Inc()
retrigErr = err
}
if retrigErr != nil {
klog.Errorf("Workspace %s failed to reconcile -> %s", ws.Name, retrigErr)
return ctrl.Result{}, retrigErr
}
// no retrigErr, need to normal reconcile later, so need to create random number and exit
nextRequeueDuration := randomDuration(r.RequeueTimeMinimum, r.RequeueTimeMaximum)
klog.Infof("Workspace %s reconciled successfully, next in %s", ws.Name, nextRequeueDuration)
return ctrl.Result{RequeueAfter: nextRequeueDuration}, nil
}
// SetupWithManager registers a new controller for Workspace resources.
func (r *WorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
WithEventFilter(labelSelectorPredicate(r.TargetLabelKey, r.TargetLabelValue)).
For(&crownlabsv1alpha1.Workspace{}).
Owns(&v1.Namespace{}).
Owns(&rbacv1.ClusterRoleBinding{}).
Owns(&rbacv1.RoleBinding{}).
WithLogConstructor(utils.LogConstructor(mgr.GetLogger(), "Workspace")).
Complete(r)
}
func (r *WorkspaceReconciler) handleDeletion(ctx context.Context, wsName, wsPrettyName string) error {
var retErr error
rolesToDelete := genWsKcRolesData(wsName, wsPrettyName)
if r.KcA != nil {
if err := r.KcA.deleteKcRoles(ctx, rolesToDelete); err != nil {
klog.Errorf("Error when deleting roles of workspace %s -> %s", wsName, err)
tnOpinternalErrors.WithLabelValues("workspace", "self-update").Inc()
retErr = err
}
}
// unsubscribe tenants from workspace to delete
var tenantsToUpdate crownlabsv1alpha2.TenantList
targetLabel := fmt.Sprintf("%s%s", crownlabsv1alpha2.WorkspaceLabelPrefix, wsName)
err := r.List(ctx, &tenantsToUpdate, &client.HasLabels{targetLabel})
switch {
case client.IgnoreNotFound(err) != nil:
klog.Errorf("Error when listing tenants subscribed to workspace %s upon deletion -> %s", wsName, err)
retErr = err
case err != nil:
klog.Infof("No tenants subscribed to workspace %s", wsName)
default:
for i := range tenantsToUpdate.Items {
removeWsFromTn(&tenantsToUpdate.Items[i].Spec.Workspaces, wsName)
if err := r.Update(ctx, &tenantsToUpdate.Items[i]); err != nil {
klog.Errorf("Error when unsubscribing tenant %s from workspace %s -> %s", tenantsToUpdate.Items[i].Name, wsName, err)
tnOpinternalErrors.WithLabelValues("workspace", "tenant-unsubscription").Inc()
retErr = err
}
}
}
return retErr
}
func removeWsFromTn(workspaces *[]crownlabsv1alpha2.TenantWorkspaceEntry, wsToRemove string) {
idxToRemove := -1
for i, wsData := range *workspaces {
if wsData.Name == wsToRemove {
idxToRemove = i
}
}
if idxToRemove != -1 {
*workspaces = append((*workspaces)[:idxToRemove], (*workspaces)[idxToRemove+1:]...) // Truncate slice.
}
}
func (r *WorkspaceReconciler) handleAutoEnrollment(ctx context.Context, ws *crownlabsv1alpha1.Workspace) error {
// check label and update if needed
var wantedLabel string
if utils.AutoEnrollEnabled(ws.Spec.AutoEnroll) {
wantedLabel = string(ws.Spec.AutoEnroll)
} else {
wantedLabel = "disabled"
}
if ws.Labels[crownlabsv1alpha2.WorkspaceLabelAutoenroll] != wantedLabel {
ws.Labels[crownlabsv1alpha2.WorkspaceLabelAutoenroll] = wantedLabel
if err := r.Update(ctx, ws); err != nil {
klog.Errorf("Error when updating workspace %s -> %s", ws.Name, err)
return err
}
}
// if actual AutoEnroll is WithApproval, nothing left to do
if ws.Spec.AutoEnroll == crownlabsv1alpha1.AutoenrollWithApproval {
return nil
}
// if actual AutoEnroll is not WithApproval, manage Tenants in candidate status
var tenantsToUpdate crownlabsv1alpha2.TenantList
targetLabel := fmt.Sprintf("%s%s", crownlabsv1alpha2.WorkspaceLabelPrefix, ws.Name)
err := r.List(ctx, &tenantsToUpdate, &client.MatchingLabels{targetLabel: string(crownlabsv1alpha2.Candidate)})
if err != nil {
klog.Errorf("Error when listing tenants subscribed to workspace %s -> %s", ws.Name, err)
return err
}
for i := range tenantsToUpdate.Items {
patch := client.MergeFrom(tenantsToUpdate.Items[i].DeepCopy())
removeWsFromTn(&tenantsToUpdate.Items[i].Spec.Workspaces, ws.Name)
// if AutoEnrollment is Immediate, add the workspace with User role
if ws.Spec.AutoEnroll == crownlabsv1alpha1.AutoenrollImmediate {
tenantsToUpdate.Items[i].Spec.Workspaces = append(
tenantsToUpdate.Items[i].Spec.Workspaces,
crownlabsv1alpha2.TenantWorkspaceEntry{
Name: ws.Name,
Role: crownlabsv1alpha2.User,
},
)
}
if err := r.Patch(ctx, &tenantsToUpdate.Items[i], patch); err != nil {
klog.Errorf("Error when updating tenant %s -> %s", tenantsToUpdate.Items[i].Name, err)
return err
}
}
return nil
}
func (r *WorkspaceReconciler) createOrUpdateClusterResources(ctx context.Context, ws *crownlabsv1alpha1.Workspace, nsName string) (nsOk bool, err error) {
ns := v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}
if _, nsErr := ctrl.CreateOrUpdate(ctx, r.Client, &ns, func() error {
r.updateWsNamespace(&ns)
return ctrl.SetControllerReference(ws, &ns, r.Scheme)
}); nsErr != nil {
klog.Errorf("Error when updating namespace of workspace %s -> %s", ws.Name, nsErr)
return false, nsErr
}
var retErr error
// handle clusterRoleBinding
crb := rbacv1.ClusterRoleBinding{ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("crownlabs-manage-instances-%s", ws.Name)}}
crbOpRes, err := ctrl.CreateOrUpdate(ctx, r.Client, &crb, func() error {
r.updateWsCrb(&crb, ws.Name)
return ctrl.SetControllerReference(ws, &crb, r.Scheme)
})
if err != nil {
klog.Errorf("Unable to create or update cluster role binding for workspace %s -> %s", ws.Name, err)
retErr = err
}
klog.Infof("Cluster role binding for workspace %s %s", ws.Name, crbOpRes)
// handle roleBinding
rb := rbacv1.RoleBinding{ObjectMeta: metav1.ObjectMeta{Name: "crownlabs-view-templates", Namespace: nsName}}
rbOpRes, err := ctrl.CreateOrUpdate(ctx, r.Client, &rb, func() error {
r.updateWsRb(&rb, ws.Name)
return ctrl.SetControllerReference(ws, &rb, r.Scheme)
})
if err != nil {
klog.Errorf("Unable to create or update role binding for workspace %s -> %s", ws.Name, err)
retErr = err
}
klog.Infof("Role binding for workspace %s %s", ws.Name, rbOpRes)
// handle manager roleBinding
managerRb := rbacv1.RoleBinding{ObjectMeta: metav1.ObjectMeta{Name: "crownlabs-manage-templates", Namespace: nsName}}
mngRbOpRes, err := ctrl.CreateOrUpdate(ctx, r.Client, &managerRb, func() error {
r.updateWsRbMng(&managerRb, ws.Name)
return ctrl.SetControllerReference(ws, &managerRb, r.Scheme)
})
if err != nil {
klog.Errorf("Unable to create or update manager role binding for workspace %s -> %s", ws.Name, err)
retErr = err
}
klog.Infof("Manager role binding for workspace %s %s", ws.Name, mngRbOpRes)
// handle manager clusterRoleBinding
tenantEditCrb := rbacv1.ClusterRoleBinding{ObjectMeta: metav1.ObjectMeta{Name: "crownlabs-manage-tenants-" + ws.Name}}
tenEdCrbOpRes, err := ctrl.CreateOrUpdate(ctx, r.Client, &tenantEditCrb, func() error {
r.updateWsRbMngTnts(&tenantEditCrb, ws.Name)
return ctrl.SetControllerReference(ws, &tenantEditCrb, r.Scheme)
})
if err != nil {
klog.Errorf("Unable to create or update tenant manager role binding for workspace %s -> %s", ws.Name, err)
retErr = err
}
klog.Infof("Tenant manager role binding for workspace %s %s", ws.Name, tenEdCrbOpRes)
return true, retErr
}
func (r *WorkspaceReconciler) updateWsNamespace(ns *v1.Namespace) {
ns.Labels = r.updateWsResourceCommonLabels(ns.Labels)
ns.Labels["crownlabs.polito.it/type"] = "workspace"
}
func (r *WorkspaceReconciler) updateWsCrb(crb *rbacv1.ClusterRoleBinding, wsName string) {
crb.Labels = r.updateWsResourceCommonLabels(crb.Labels)
crb.RoleRef = rbacv1.RoleRef{Kind: "ClusterRole", Name: "crownlabs-manage-instances", APIGroup: "rbac.authorization.k8s.io"}
crb.Subjects = []rbacv1.Subject{{Kind: "Group", Name: fmt.Sprintf("kubernetes:%s", genWsKcRoleName(wsName, crownlabsv1alpha2.Manager)), APIGroup: "rbac.authorization.k8s.io"}}
}
func (r *WorkspaceReconciler) updateWsRb(rb *rbacv1.RoleBinding, wsName string) {
rb.Labels = r.updateWsResourceCommonLabels(rb.Labels)
rb.RoleRef = rbacv1.RoleRef{Kind: "ClusterRole", Name: "crownlabs-view-templates", APIGroup: "rbac.authorization.k8s.io"}
rb.Subjects = []rbacv1.Subject{{Kind: "Group", Name: fmt.Sprintf("kubernetes:%s", genWsKcRoleName(wsName, crownlabsv1alpha2.User)), APIGroup: "rbac.authorization.k8s.io"}}
}
func (r *WorkspaceReconciler) updateWsRbMng(rb *rbacv1.RoleBinding, wsName string) {
rb.Labels = r.updateWsResourceCommonLabels(rb.Labels)
rb.RoleRef = rbacv1.RoleRef{Kind: "ClusterRole", Name: "crownlabs-manage-templates", APIGroup: "rbac.authorization.k8s.io"}
rb.Subjects = []rbacv1.Subject{{Kind: "Group", Name: fmt.Sprintf("kubernetes:%s", genWsKcRoleName(wsName, crownlabsv1alpha2.Manager)), APIGroup: "rbac.authorization.k8s.io"}}
}
func (r *WorkspaceReconciler) updateWsRbMngTnts(rb *rbacv1.ClusterRoleBinding, wsName string) {
rb.Labels = r.updateWsResourceCommonLabels(rb.Labels)
rb.RoleRef = rbacv1.RoleRef{Kind: "ClusterRole", Name: "crownlabs-manage-tenants", APIGroup: "rbac.authorization.k8s.io"}
rb.Subjects = []rbacv1.Subject{{Kind: "Group", Name: fmt.Sprintf("kubernetes:%s", genWsKcRoleName(wsName, crownlabsv1alpha2.Manager)), APIGroup: "rbac.authorization.k8s.io"}}
}
func genWsKcRolesData(wsName, wsPrettyName string) map[string]string {
return map[string]string{genWsKcRoleName(wsName, crownlabsv1alpha2.Manager): wsPrettyName, genWsKcRoleName(wsName, crownlabsv1alpha2.User): wsPrettyName}
}
func genWsKcRoleName(wsName string, role crownlabsv1alpha2.WorkspaceUserRole) string {
return fmt.Sprintf("workspace-%s:%s", wsName, role)
}
func (r *WorkspaceReconciler) updateWsResourceCommonLabels(labels map[string]string) map[string]string {
if labels == nil {
labels = make(map[string]string, 1)
}
labels[r.TargetLabelKey] = r.TargetLabelValue
labels["crownlabs.polito.it/managed-by"] = "workspace"
// don't know why the initialization of the map doesn't work, so need to return a new one
return labels
}