@@ -18,12 +18,14 @@ package bridge
18
18
import (
19
19
"context"
20
20
"encoding/json"
21
+ "errors"
21
22
"sync"
22
23
"time"
23
24
24
25
corev1 "k8s.io/api/core/v1"
25
26
apierrors "k8s.io/apimachinery/pkg/api/errors"
26
27
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28
+ "k8s.io/apimachinery/pkg/util/wait"
27
29
corev1apply "k8s.io/client-go/applyconfigurations/core/v1"
28
30
"sigs.k8s.io/controller-runtime/pkg/builder"
29
31
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -65,6 +67,9 @@ type InstallationReconciler struct {
65
67
Patch (context.Context , client.Object , client.Patch , ... client.PatchOption ) error
66
68
}
67
69
70
+ // Refresh is the frequency at which AuthObjects should be renewed.
71
+ Refresh time.Duration
72
+
68
73
// SecretRef is the name of the corev1.Secret in which to store Bridge tokens.
69
74
SecretRef client.ObjectKey
70
75
@@ -79,6 +84,7 @@ func ManagedInstallationReconciler(m manager.Manager, newClient func() *Client)
79
84
Owner : naming .ControllerBridge ,
80
85
Reader : kubernetes ,
81
86
Writer : kubernetes ,
87
+ Refresh : 2 * time .Hour ,
82
88
SecretRef : naming .AsObjectKey (naming .OperatorConfigurationSecret ()),
83
89
NewClient : newClient ,
84
90
}
@@ -119,7 +125,7 @@ func (r *InstallationReconciler) Reconcile(
119
125
// make it so.
120
126
secret .Namespace , secret .Name = request .Namespace , request .Name
121
127
122
- err = r .reconcile (ctx , secret )
128
+ result . RequeueAfter , err = r .reconcile (ctx , secret )
123
129
}
124
130
125
131
// TODO: Check for corev1.NamespaceTerminatingCause after
@@ -134,10 +140,14 @@ func (r *InstallationReconciler) Reconcile(
134
140
return result , err
135
141
}
136
142
137
- func (r * InstallationReconciler ) reconcile (ctx context.Context , read * corev1.Secret ) error {
143
+ // reconcile looks for an Installation in read and stores it or another in
144
+ // the [self] singleton after a successful response from the Bridge API.
145
+ func (r * InstallationReconciler ) reconcile (
146
+ ctx context.Context , read * corev1.Secret ) (next time.Duration , err error ,
147
+ ) {
138
148
write , err := corev1apply .ExtractSecret (read , string (r .Owner ))
139
149
if err != nil {
140
- return err
150
+ return 0 , err
141
151
}
142
152
143
153
// We GET-extract-PATCH the Secret and do not build it up from scratch.
@@ -157,24 +167,30 @@ func (r *InstallationReconciler) reconcile(ctx context.Context, read *corev1.Sec
157
167
// Secret which triggers another reconcile.
158
168
if len (installation .ID ) == 0 {
159
169
if len (self .ID ) == 0 {
160
- return r .register (ctx , write )
170
+ return 0 , r .register (ctx , write )
161
171
}
162
172
163
173
data := map [string ][]byte {}
164
174
data [KeyBridgeToken ], _ = json .Marshal (self .Installation ) //nolint:errchkjson
165
175
166
- return r .persist (ctx , write .WithData (data ))
176
+ return 0 , r .persist (ctx , write .WithData (data ))
167
177
}
168
178
169
- // When the Secret has an Installation, store it in memory.
170
- // TODO: Validate it first; perhaps refresh the AuthObject.
171
- if len (self .ID ) == 0 {
172
- self .Lock ()
173
- self .Installation = installation
174
- self .Unlock ()
179
+ // Read the timestamp from the Secret, if any.
180
+ var touched time.Time
181
+ if yaml .Unmarshal (read .Data [KeyBridgeLocalTime ], & touched ) != nil {
182
+ touched = time.Time {}
183
+ }
184
+
185
+ // Refresh the AuthObject when there is no Installation in memory,
186
+ // there is no timestamp, or the timestamp is far away. This writes to
187
+ // the Secret which triggers another reconcile.
188
+ if len (self .ID ) == 0 || time .Since (touched ) > r .Refresh || time .Until (touched ) > r .Refresh {
189
+ return 0 , r .refresh (ctx , installation , write )
175
190
}
176
191
177
- return nil
192
+ // Trigger another reconcile one interval after the stored timestamp.
193
+ return wait .Jitter (time .Until (touched .Add (r .Refresh )), 0.1 ), nil
178
194
}
179
195
180
196
// persist uses Server-Side Apply to write config to Kubernetes. The Name and
@@ -198,6 +214,52 @@ func (r *InstallationReconciler) persist(
198
214
return err
199
215
}
200
216
217
+ // refresh calls the Bridge API to refresh the AuthObject of installation. It
218
+ // combines the result with installation and stores that in the [self] singleton
219
+ // and the write object in Kubernetes. The Name and Namespace fields of the
220
+ // latter cannot be nil.
221
+ func (r * InstallationReconciler ) refresh (
222
+ ctx context.Context , installation Installation ,
223
+ write * corev1apply.SecretApplyConfiguration ,
224
+ ) error {
225
+ result , err := r .NewClient ().CreateAuthObject (ctx , installation .AuthObject )
226
+
227
+ // An authentication error means the installation is irrecoverably expired.
228
+ // Remove it from the singleton and move it to a dated entry in the Secret.
229
+ if err != nil && errors .Is (err , errAuthentication ) {
230
+ self .Lock ()
231
+ self .Installation = Installation {}
232
+ self .Unlock ()
233
+
234
+ keyExpiration := KeyBridgeToken +
235
+ installation .AuthObject .ExpiresAt .UTC ().Format ("--2006-01-02" )
236
+
237
+ data := make (map [string ][]byte , 2 )
238
+ data [KeyBridgeToken ] = nil
239
+ data [keyExpiration ], _ = json .Marshal (installation ) //nolint:errchkjson
240
+
241
+ return r .persist (ctx , write .WithData (data ))
242
+ }
243
+
244
+ if err == nil {
245
+ installation .AuthObject = result
246
+
247
+ // Store the new value in the singleton.
248
+ self .Lock ()
249
+ self .Installation = installation
250
+ self .Unlock ()
251
+
252
+ // Store the new value in the Secret along with the current time.
253
+ data := make (map [string ][]byte , 2 )
254
+ data [KeyBridgeLocalTime ], _ = metav1 .Now ().MarshalJSON ()
255
+ data [KeyBridgeToken ], _ = json .Marshal (installation ) //nolint:errchkjson
256
+
257
+ err = r .persist (ctx , write .WithData (data ))
258
+ }
259
+
260
+ return err
261
+ }
262
+
201
263
// register calls the Bridge API to register a new Installation. It stores the
202
264
// result in the [self] singleton and the write object in Kubernetes. The Name
203
265
// and Namespace fields of the latter cannot be nil.
0 commit comments