1 package terminalctl
2
3 import (
4 "context"
5 "time"
6
7 "github.com/fluxcd/pkg/ssa"
8 "github.com/go-logr/logr"
9 "sigs.k8s.io/cli-utils/pkg/kstatus/polling"
10 ctrl "sigs.k8s.io/controller-runtime"
11 "sigs.k8s.io/controller-runtime/pkg/client"
12 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
13 "sigs.k8s.io/controller-runtime/pkg/event"
14 "sigs.k8s.io/controller-runtime/pkg/predicate"
15
16 "edge-infra.dev/pkg/k8s/meta/status"
17 "edge-infra.dev/pkg/k8s/runtime/conditions"
18 "edge-infra.dev/pkg/k8s/runtime/controller/metrics"
19 "edge-infra.dev/pkg/k8s/runtime/controller/reconcile"
20 "edge-infra.dev/pkg/k8s/runtime/controller/reconcile/recerr"
21 "edge-infra.dev/pkg/k8s/runtime/inventory"
22 "edge-infra.dev/pkg/k8s/runtime/patch"
23 v1ien "edge-infra.dev/pkg/sds/ien/k8s/apis/v1"
24 "edge-infra.dev/pkg/sds/k8s/controllers/terminalctl/pkg/plugins"
25 )
26
27 const (
28 ErrPluginFailed = "plugin failed"
29 ErrPluginFinalizerFailed = "plugin finalizer failed"
30 ErrInvalidTerminal = "invalid Terminal spec"
31 ErrAddingFinalizer = "unable to register finalizer"
32 ErrTerminalStatusUpdate = "unable to update Terminal status"
33 )
34
35 var terminalConditions = reconcile.Conditions{
36 Target: status.ReadyCondition,
37 Owned: []string{
38 string(v1ien.IENController),
39 },
40 Summarize: []string{
41 string(v1ien.IENController),
42 },
43 NegativePolarity: []string{},
44 }
45
46
47 type TerminalReconciler struct {
48 client.Client
49 Log logr.Logger
50 Metrics metrics.Metrics
51 Config *Config
52 DefaultRequeue time.Duration
53 Conditions reconcile.Conditions
54
55
56 ResourceManager *ssa.ResourceManager
57 Name string
58 }
59
60 func terminalReconcilerPredicate() predicate.Predicate {
61 return predicate.Funcs{
62 UpdateFunc: func(e event.UpdateEvent) bool {
63 return !e.ObjectNew.GetDeletionTimestamp().IsZero() || (e.ObjectNew.GetGeneration() != e.ObjectOld.GetGeneration())
64 },
65 CreateFunc: func(_ event.CreateEvent) bool {
66 return true
67 },
68 DeleteFunc: func(_ event.DeleteEvent) bool {
69 return false
70 },
71 }
72 }
73
74
75 func (r *TerminalReconciler) SetupWithManager(mgr ctrl.Manager) error {
76 return ctrl.NewControllerManagedBy(mgr).
77 For(&v1ien.IENode{}).
78 WithEventFilter(terminalReconcilerPredicate()).
79 Complete(r)
80 }
81
82 func (r *TerminalReconciler) PatchOpts() []patch.Option {
83 return []patch.Option{
84 patch.WithOwnedConditions{Conditions: r.Conditions.Owned},
85 patch.WithFieldOwner(r.Name),
86 }
87 }
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102 func (r *TerminalReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, recErr error) {
103 reconcileStart := time.Now()
104 log := ctrl.LoggerFrom(ctx)
105 r.setResourceManager()
106
107 var result = reconcile.ResultEmpty
108
109 terminal := &v1ien.IENode{}
110 if err := r.Client.Get(ctx, req.NamespacedName, terminal); client.IgnoreNotFound(err) != nil {
111 log.Error(err, "failed to get Terminal")
112 return ctrl.Result{}, client.IgnoreNotFound(err)
113 }
114
115 patcher := patch.NewSerialPatcher(terminal, r.Client)
116
117 defer func() {
118 summarizer := reconcile.NewSummarizer(patcher)
119 res, recErr = summarizer.SummarizeAndPatch(ctx, terminal, []reconcile.SummarizeOption{
120 reconcile.WithConditions(r.Conditions),
121 reconcile.WithResult(result),
122 reconcile.WithError(recErr),
123 reconcile.WithIgnoreNotFound(),
124 reconcile.WithProcessors(
125 reconcile.RecordReconcileReq,
126 reconcile.RecordResult,
127 ),
128 reconcile.WithFieldOwner(r.Name),
129 }...)
130
131 r.Metrics.RecordDuration(ctx, terminal, reconcileStart)
132 r.Metrics.RecordReconciling(ctx, terminal)
133 r.Metrics.RecordReadiness(ctx, terminal)
134 }()
135
136 var oldStatus v1ien.IENodeStatus
137 if terminal.Status != nil {
138 oldStatus = *terminal.Status.DeepCopy()
139 }
140 terminal = terminal.DeepCopy()
141
142 log = log.WithValues("name", terminal.ObjectMeta.Name, "spec", terminal.Spec)
143
144 ctx = logr.NewContext(ctx, log)
145 log.Info("reconciling started for terminal")
146
147
148 if !controllerutil.ContainsFinalizer(terminal, v1ien.Finalizer) {
149 controllerutil.AddFinalizer(terminal, v1ien.Finalizer)
150
151
152
153 result = reconcile.ResultRequeue
154 return
155 }
156
157
158 if !terminal.ObjectMeta.DeletionTimestamp.IsZero() {
159 if err := r.deleteTerminal(ctx, terminal); err != nil {
160 recErr = err
161 return
162 }
163 return
164 }
165
166 if err := reconcile.Progressing(ctx, terminal, patcher, r.PatchOpts()...); err != nil {
167 recErr = err
168 return
169 }
170
171
172 terminal.Status.Inventory = inventory.New()
173
174 err := plugins.Execute(ctx, r.ResourceManager, terminal)
175 if err != nil {
176 log.Error(err, "plugin error")
177 recErr = recerr.New(err, string(v1ien.PluginFailed))
178 return
179 }
180
181 if err := r.pruneInventory(ctx, oldStatus, terminal); err != nil {
182 log.Error(err, "error pruning inventory")
183 recErr = err
184 return
185 }
186
187 log.Info("reconciling finished")
188
189 conditions.MarkTrue(terminal, status.ReadyCondition, string(v1ien.Successful), "terminal reconciled successfully")
190 result = reconcile.ResultSuccess
191 return
192 }
193
194 func (r *TerminalReconciler) deleteTerminal(ctx context.Context, terminal *v1ien.IENode) error {
195 log := logr.FromContextOrDiscard(ctx)
196
197 err := plugins.ExecuteFinalizers(ctx, r.Client, terminal)
198 if err != nil {
199 log.Error(err, ErrPluginFinalizerFailed)
200 conditions.MarkFalse(terminal, status.ReadyCondition, string(v1ien.PluginFailed), ErrPluginFinalizerFailed)
201 return err
202 }
203
204 if terminal.Status != nil && terminal.Status.Inventory != nil {
205 inv, err := inventory.ListObjects(terminal.Status.Inventory)
206 if err != nil {
207 log.Error(err, "failed to get objects to cleanup from inventory", "terminal", terminal.Name)
208 return err
209 }
210 _, err = r.ResourceManager.DeleteAll(ctx, inv, ssa.DefaultDeleteOptions())
211 if err != nil {
212 log.Error(err, "failed to cleanup objects from inventory", "terminal", terminal.Name)
213 return err
214 }
215 }
216 controllerutil.RemoveFinalizer(terminal, v1ien.Finalizer)
217 log.Info("finalizer executed")
218 return nil
219 }
220
221
222
223 func (r *TerminalReconciler) pruneInventory(ctx context.Context, oldStatus v1ien.IENodeStatus, terminal *v1ien.IENode) error {
224 log := logr.FromContextOrDiscard(ctx)
225
226 if oldStatus.Inventory == nil {
227 return nil
228 }
229
230 diff, err := inventory.Diff(oldStatus.Inventory, terminal.GetInventory())
231 if err != nil {
232 return err
233 }
234
235 if len(diff) == 0 {
236 return nil
237 }
238
239 changeSet, err := r.ResourceManager.DeleteAll(ctx, diff, ssa.DefaultDeleteOptions())
240 if err != nil {
241 return err
242 }
243 log.Info("pruned objects", "changeset", changeSet)
244 return nil
245 }
246
247 func (r *TerminalReconciler) setResourceManager() {
248 if r.ResourceManager == nil {
249 mgr := ssa.NewResourceManager(
250 r.Client,
251
252
253 polling.NewStatusPoller(r.Client, r.Client.RESTMapper(), polling.Options{}), ssa.Owner{Field: r.Name},
254 )
255 r.ResourceManager = mgr
256 }
257 }
258
View as plain text