generated from kyma-project/template-repository
-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathsample_controller_rendered_resources.go
365 lines (315 loc) · 14.3 KB
/
sample_controller_rendered_resources.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
/*
Copyright 2022.
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 controllers
import (
"context"
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/go-logr/logr"
errors2 "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/scheme"
"github.com/kyma-project/template-operator/api/v1alpha1"
)
// SampleReconciler reconciles a Sample object.
type SampleReconciler struct {
client.Client
Scheme *runtime.Scheme
*rest.Config
// EventRecorder for creating k8s events
record.EventRecorder
FinalState v1alpha1.State
FinalDeletionState v1alpha1.State
}
type ManifestResources struct {
Items []*unstructured.Unstructured
Blobs [][]byte
}
var (
// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
//nolint:gochecknoglobals // used to register Sample CRD on startup
SchemeBuilder = &scheme.Builder{GroupVersion: v1alpha1.GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
//nolint:gochecknoglobals // used to register Sample CRD on startup
AddToScheme = SchemeBuilder.AddToScheme
)
func init() { //nolint:gochecknoinits // used to register Sample CRD on startup
SchemeBuilder.Register(&v1alpha1.Sample{}, &v1alpha1.SampleList{})
}
// +kubebuilder:rbac:groups=operator.kyma-project.io,resources=samples,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=operator.kyma-project.io,resources=samples/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=operator.kyma-project.io,resources=samples/finalizers,verbs=update
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch;get;list;watch
// +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=create;patch;delete
// +kubebuilder:rbac:groups="apps",resources=deployments,verbs=create;patch;delete
// +kubebuilder:rbac:groups="apps",resources=statefulsets,verbs=create;patch;delete
// SetupWithManager sets up the controller with the Manager.
func (r *SampleReconciler) SetupWithManager(mgr ctrl.Manager, rateLimiter RateLimiter) error {
r.Config = mgr.GetConfig()
if err := ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.Sample{}).
WithOptions(controller.Options{
RateLimiter: TemplateRateLimiter(
rateLimiter.BaseDelay,
rateLimiter.FailureMaxDelay,
rateLimiter.Frequency,
rateLimiter.Burst,
),
}).
Complete(r); err != nil {
return fmt.Errorf("error while setting up controller: %w", err)
}
return nil
}
// Reconcile is the entry point from the controller-runtime framework.
// It performs a reconciliation based on the passed ctrl.Request object.
func (r *SampleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
objectInstance := v1alpha1.Sample{}
if err := r.Client.Get(ctx, req.NamespacedName, &objectInstance); err != nil {
// we'll ignore not-found errors, since they can't be fixed by an immediate
// requeue (we'll need to wait for a new notification), and we can get them
// on deleted requests.
logger.Info(req.NamespacedName.String() + " got deleted!")
if client.IgnoreNotFound(err) != nil {
return ctrl.Result{}, fmt.Errorf("error while getting object: %w", err)
}
return ctrl.Result{}, nil
}
// check if deletionTimestamp is set, retry until it gets deleted
status := getStatusFromSample(&objectInstance)
// set state to FinalDeletionState (default is Deleting) if not set for an object with deletion timestamp
if !objectInstance.GetDeletionTimestamp().IsZero() && status.State != r.FinalDeletionState {
return ctrl.Result{}, r.setStatusForObjectInstance(ctx, &objectInstance, status.WithState(r.FinalDeletionState))
}
if objectInstance.GetDeletionTimestamp().IsZero() {
// add finalizer if not present
if controllerutil.AddFinalizer(&objectInstance, finalizer) {
return ctrl.Result{}, r.ssa(ctx, &objectInstance)
}
}
switch status.State {
case "":
return ctrl.Result{}, r.HandleInitialState(ctx, &objectInstance)
case v1alpha1.StateProcessing:
return ctrl.Result{Requeue: true}, r.HandleProcessingState(ctx, &objectInstance)
case v1alpha1.StateDeleting:
return ctrl.Result{Requeue: true}, r.HandleDeletingState(ctx, &objectInstance)
case v1alpha1.StateError:
return ctrl.Result{Requeue: true}, r.HandleErrorState(ctx, &objectInstance)
case v1alpha1.StateReady, v1alpha1.StateWarning:
return ctrl.Result{RequeueAfter: requeueInterval}, r.HandleReadyState(ctx, &objectInstance)
}
return ctrl.Result{}, nil
}
// HandleInitialState bootstraps state handling for the reconciled resource.
func (r *SampleReconciler) HandleInitialState(ctx context.Context, objectInstance *v1alpha1.Sample) error {
status := getStatusFromSample(objectInstance)
return r.setStatusForObjectInstance(ctx, objectInstance, status.
WithState(v1alpha1.StateProcessing).
WithInstallConditionStatus(metav1.ConditionUnknown, objectInstance.GetGeneration()))
}
// HandleProcessingState processes the reconciled resource by processing the underlying resources.
// Based on the processing either a success or failure state is set on the reconciled resource.
func (r *SampleReconciler) HandleProcessingState(ctx context.Context, objectInstance *v1alpha1.Sample) error {
status := getStatusFromSample(objectInstance)
if err := r.processResources(ctx, objectInstance); err != nil {
// stay in Processing state if FinalDeletionState is set to Processing
if !objectInstance.GetDeletionTimestamp().IsZero() && r.FinalDeletionState == v1alpha1.StateProcessing {
return nil
}
r.Event(objectInstance, "Warning", "ResourcesInstall", err.Error())
return r.setStatusForObjectInstance(ctx, objectInstance, status.
WithState(v1alpha1.StateError).
WithInstallConditionStatus(metav1.ConditionFalse, objectInstance.GetGeneration()))
}
// set eventual state to Ready - if no errors were found
return r.setStatusForObjectInstance(ctx, objectInstance, status.
WithState(r.FinalState).
WithInstallConditionStatus(metav1.ConditionTrue, objectInstance.GetGeneration()))
}
// HandleErrorState handles error recovery for the reconciled resource.
func (r *SampleReconciler) HandleErrorState(ctx context.Context, objectInstance *v1alpha1.Sample) error {
status := getStatusFromSample(objectInstance)
if err := r.processResources(ctx, objectInstance); err != nil {
return err
}
// stay in Error state if FinalDeletionState is set to Error
if !objectInstance.GetDeletionTimestamp().IsZero() && r.FinalDeletionState == v1alpha1.StateError {
return nil
}
// set eventual state to Ready - if no errors were found
return r.setStatusForObjectInstance(ctx, objectInstance, status.
WithState(r.FinalState).
WithInstallConditionStatus(metav1.ConditionTrue, objectInstance.GetGeneration()))
}
// HandleDeletingState processed the deletion on the reconciled resource.
// Once the deletion if processed the relevant finalizers (if applied) are removed.
func (r *SampleReconciler) HandleDeletingState(ctx context.Context, objectInstance *v1alpha1.Sample) error {
r.Event(objectInstance, "Normal", "Deleting", "resource deleting")
logger := log.FromContext(ctx)
status := getStatusFromSample(objectInstance)
resourceObjs, err := getResourcesFromLocalPath(objectInstance.Spec.ResourceFilePath, logger)
if err != nil && controllerutil.RemoveFinalizer(objectInstance, finalizer) {
// if error is encountered simply remove the finalizer and delete the reconciled resource
if err := r.Client.Update(ctx, objectInstance); err != nil {
return fmt.Errorf("error while removing finalizer: %w", err)
}
return nil
}
r.Event(objectInstance, "Normal", "ResourcesDelete", "deleting resources")
// the resources to be installed are unstructured,
// so please make sure the types are available on the target cluster
for _, obj := range resourceObjs.Items {
if err = r.Client.Delete(ctx, obj); err != nil && !errors2.IsNotFound(err) {
// stay in Deleting state if FinalDeletionState is set to Deleting
if !objectInstance.GetDeletionTimestamp().IsZero() && r.FinalDeletionState == v1alpha1.StateDeleting {
return nil
}
logger.Error(err, "error during uninstallation of resources")
r.Event(objectInstance, "Warning", "ResourcesDelete", "deleting resources error")
return r.setStatusForObjectInstance(ctx, objectInstance, status.
WithState(v1alpha1.StateError).
WithInstallConditionStatus(metav1.ConditionFalse, objectInstance.GetGeneration()))
}
}
// if resources are ready to be deleted, remove finalizer
if controllerutil.RemoveFinalizer(objectInstance, finalizer) {
if err := r.Client.Update(ctx, objectInstance); err != nil {
return fmt.Errorf("error while removing finalizer: %w", err)
}
return nil
}
return nil
}
// HandleReadyState checks for the consistency of reconciled resource, by verifying the underlying resources.
func (r *SampleReconciler) HandleReadyState(ctx context.Context, objectInstance *v1alpha1.Sample) error {
status := getStatusFromSample(objectInstance)
if err := r.processResources(ctx, objectInstance); err != nil {
// stay in Ready/Warning state if FinalDeletionState is set to Ready/Warning
if !objectInstance.GetDeletionTimestamp().IsZero() &&
(r.FinalDeletionState == v1alpha1.StateReady || r.FinalDeletionState == v1alpha1.StateWarning) {
return nil
}
r.Event(objectInstance, "Warning", "ResourcesInstall", err.Error())
return r.setStatusForObjectInstance(ctx, objectInstance, status.
WithState(v1alpha1.StateError).
WithInstallConditionStatus(metav1.ConditionFalse, objectInstance.GetGeneration()))
}
return nil
}
func (r *SampleReconciler) setStatusForObjectInstance(ctx context.Context, objectInstance *v1alpha1.Sample,
status *v1alpha1.SampleStatus,
) error {
objectInstance.Status = *status
if err := r.ssaStatus(ctx, objectInstance); err != nil {
r.Event(objectInstance, "Warning", "ErrorUpdatingStatus",
fmt.Sprintf("updating state to %v", string(status.State)))
return fmt.Errorf("error while updating status %s to: %w", status.State, err)
}
r.Event(objectInstance, "Normal", "StatusUpdated", fmt.Sprintf("updating state to %v", string(status.State)))
return nil
}
func (r *SampleReconciler) processResources(ctx context.Context, objectInstance *v1alpha1.Sample) error {
logger := log.FromContext(ctx)
resourceObjs, err := getResourcesFromLocalPath(objectInstance.Spec.ResourceFilePath, logger)
if err != nil {
logger.Error(err, "error locating manifest of resources")
return fmt.Errorf("error locating manifest of resources: %w", err)
}
r.Event(objectInstance, "Normal", "ResourcesInstall", "installing resources")
// the resources to be installed are unstructured,
// so please make sure the types are available on the target cluster
for _, obj := range resourceObjs.Items {
if err = r.ssa(ctx, obj); err != nil && !errors2.IsAlreadyExists(err) {
logger.Error(err, "error during installation of resources")
return fmt.Errorf("error during installation of resources: %w", err)
}
}
return nil
}
func getStatusFromSample(objectInstance *v1alpha1.Sample) v1alpha1.SampleStatus {
return objectInstance.Status
}
// getResourcesFromLocalPath returns resources from the dirPath in unstructured format.
// Only one file in .yaml or .yml format should be present in the target directory.
func getResourcesFromLocalPath(dirPath string, logger logr.Logger) (*ManifestResources, error) {
dirEntries := make([]fs.DirEntry, 0)
err := filepath.WalkDir(dirPath, func(path string, info fs.DirEntry, err error) error {
// initial error
if err != nil {
return fmt.Errorf("error while walkdir %s: %w", dirPath, err)
}
if !info.IsDir() {
return nil
}
dirEntries, err = os.ReadDir(dirPath)
if err != nil {
return fmt.Errorf("error while reading directory %s: %w", dirPath, err)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("error while walkdir %s: %w", dirPath, err)
}
childCount := len(dirEntries)
if childCount == 0 {
logger.V(debugLogLevel).Info("no yaml file found at file path" + dirPath)
return nil, nil //nolint:nilnil // nil is returned if no yaml file is found
} else if childCount > 1 {
logger.V(debugLogLevel).Info("more than one yaml file found at file path" + dirPath)
return nil, nil //nolint:nilnil // nil is returned if more than one yaml file is found
}
file := dirEntries[0]
allowedExtns := sets.NewString(".yaml", ".yml")
if !allowedExtns.Has(filepath.Ext(file.Name())) {
return nil, nil //nolint:nilnil // nil is returned if file is not in yaml format
}
fileBytes, err := os.ReadFile(filepath.Join(dirPath, file.Name()))
if err != nil {
return nil, fmt.Errorf("yaml file could not be read %s in dir %s: %w", file.Name(), dirPath, err)
}
return parseManifestStringToObjects(string(fileBytes))
}
// ssaStatus patches status using SSA on the passed object.
func (r *SampleReconciler) ssaStatus(ctx context.Context, obj client.Object) error {
obj.SetManagedFields(nil)
obj.SetResourceVersion("")
if err := r.Status().Patch(ctx, obj, client.Apply,
&client.SubResourcePatchOptions{PatchOptions: client.PatchOptions{FieldManager: fieldOwner}}); err != nil {
return fmt.Errorf("error while patching status: %w", err)
}
return nil
}
// ssa patches the object using SSA.
func (r *SampleReconciler) ssa(ctx context.Context, obj client.Object) error {
obj.SetManagedFields(nil)
obj.SetResourceVersion("")
if err := r.Patch(ctx, obj, client.Apply, client.ForceOwnership, client.FieldOwner(fieldOwner)); err != nil {
return fmt.Errorf("error while patching object: %w", err)
}
return nil
}