Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 6b9446b

Browse files
committedMay 25, 2023
Renew Bridge installations
Issue: [sc-16285]
1 parent 6835886 commit 6b9446b

File tree

4 files changed

+417
-27
lines changed

4 files changed

+417
-27
lines changed
 

‎internal/bridge/client.go

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"bytes"
2020
"context"
2121
"encoding/json"
22+
"errors"
2223
"fmt"
2324
"io"
2425
"net/http"
@@ -32,6 +33,8 @@ import (
3233

3334
const defaultAPI = "https://api.crunchybridge.com"
3435

36+
var errAuthentication = errors.New("authentication failed")
37+
3538
type Client struct {
3639
http.Client
3740
wait.Backoff
@@ -179,6 +182,38 @@ func (c *Client) doWithRetry(
179182
return response, err
180183
}
181184

185+
func (c *Client) CreateAuthObject(ctx context.Context, authn AuthObject) (AuthObject, error) {
186+
var result AuthObject
187+
188+
response, err := c.doWithRetry(ctx, "POST", "/vendor/operator/auth-objects", nil, http.Header{
189+
"Accept": []string{"application/json"},
190+
"Authorization": []string{"Bearer " + authn.Secret},
191+
})
192+
193+
if err == nil {
194+
defer response.Body.Close()
195+
body, _ := io.ReadAll(response.Body)
196+
197+
switch {
198+
// 2xx, Successful
199+
case response.StatusCode >= 200 && response.StatusCode < 300:
200+
if err = json.Unmarshal(body, &result); err != nil {
201+
err = fmt.Errorf("%w: %s", err, body)
202+
}
203+
204+
// 401, Unauthorized
205+
case response.StatusCode == 401:
206+
err = fmt.Errorf("%w: %s", errAuthentication, body)
207+
208+
default:
209+
//nolint:goerr113 // This is intentionally dynamic.
210+
err = fmt.Errorf("%v: %s", response.Status, body)
211+
}
212+
}
213+
214+
return result, err
215+
}
216+
182217
func (c *Client) CreateInstallation(ctx context.Context) (Installation, error) {
183218
var result Installation
184219

@@ -188,20 +223,18 @@ func (c *Client) CreateInstallation(ctx context.Context) (Installation, error) {
188223

189224
if err == nil {
190225
defer response.Body.Close()
191-
192-
var body bytes.Buffer
193-
_, _ = io.Copy(&body, response.Body)
226+
body, _ := io.ReadAll(response.Body)
194227

195228
switch {
196229
// 2xx, Successful
197-
case 200 <= response.StatusCode && response.StatusCode < 300:
198-
if err = json.Unmarshal(body.Bytes(), &result); err != nil {
199-
err = fmt.Errorf("%w: %v", err, body.String())
230+
case response.StatusCode >= 200 && response.StatusCode < 300:
231+
if err = json.Unmarshal(body, &result); err != nil {
232+
err = fmt.Errorf("%w: %s", err, body)
200233
}
201234

202235
default:
203236
//nolint:goerr113 // This is intentionally dynamic.
204-
err = fmt.Errorf("%v: %v", response.Status, body.String())
237+
err = fmt.Errorf("%v: %s", response.Status, body)
205238
}
206239
}
207240

‎internal/bridge/client_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,87 @@ func TestClientDoWithRetry(t *testing.T) {
405405
})
406406
}
407407

408+
func TestClientCreateAuthObject(t *testing.T) {
409+
t.Run("Arguments", func(t *testing.T) {
410+
var requests []http.Request
411+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
412+
body, _ := io.ReadAll(r.Body)
413+
assert.Equal(t, len(body), 0)
414+
requests = append(requests, *r)
415+
}))
416+
t.Cleanup(server.Close)
417+
418+
client := NewClient(server.URL, "")
419+
assert.Equal(t, client.BaseURL.String(), server.URL)
420+
421+
ctx := context.Background()
422+
_, _ = client.CreateAuthObject(ctx, AuthObject{Secret: "sesame"})
423+
424+
assert.Equal(t, len(requests), 1)
425+
assert.Equal(t, requests[0].Header.Get("Authorization"), "Bearer sesame")
426+
})
427+
428+
t.Run("Unauthorized", func(t *testing.T) {
429+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
430+
w.WriteHeader(http.StatusUnauthorized)
431+
_, _ = w.Write([]byte(`some info`))
432+
}))
433+
t.Cleanup(server.Close)
434+
435+
client := NewClient(server.URL, "")
436+
assert.Equal(t, client.BaseURL.String(), server.URL)
437+
438+
_, err := client.CreateAuthObject(context.Background(), AuthObject{})
439+
assert.ErrorContains(t, err, "authentication")
440+
assert.ErrorContains(t, err, "some info")
441+
assert.ErrorIs(t, err, errAuthentication)
442+
})
443+
444+
t.Run("ErrorResponse", func(t *testing.T) {
445+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
446+
w.WriteHeader(http.StatusNotFound)
447+
_, _ = w.Write([]byte(`some message`))
448+
}))
449+
t.Cleanup(server.Close)
450+
451+
client := NewClient(server.URL, "")
452+
assert.Equal(t, client.BaseURL.String(), server.URL)
453+
454+
_, err := client.CreateAuthObject(context.Background(), AuthObject{})
455+
assert.ErrorContains(t, err, "404 Not Found")
456+
assert.ErrorContains(t, err, "some message")
457+
})
458+
459+
t.Run("NoResponseBody", func(t *testing.T) {
460+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
461+
w.WriteHeader(http.StatusOK)
462+
}))
463+
t.Cleanup(server.Close)
464+
465+
client := NewClient(server.URL, "")
466+
assert.Equal(t, client.BaseURL.String(), server.URL)
467+
468+
_, err := client.CreateAuthObject(context.Background(), AuthObject{})
469+
assert.ErrorContains(t, err, "unexpected end")
470+
assert.ErrorContains(t, err, "JSON")
471+
})
472+
473+
t.Run("ResponseNotJSON", func(t *testing.T) {
474+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
475+
w.WriteHeader(http.StatusOK)
476+
_, _ = w.Write([]byte(`asdf`))
477+
}))
478+
t.Cleanup(server.Close)
479+
480+
client := NewClient(server.URL, "")
481+
assert.Equal(t, client.BaseURL.String(), server.URL)
482+
483+
_, err := client.CreateAuthObject(context.Background(), AuthObject{})
484+
assert.ErrorContains(t, err, "invalid")
485+
assert.ErrorContains(t, err, "asdf")
486+
})
487+
}
488+
408489
func TestClientCreateInstallation(t *testing.T) {
409490
t.Run("ErrorResponse", func(t *testing.T) {
410491
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

‎internal/bridge/installation.go

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ package bridge
1818
import (
1919
"context"
2020
"encoding/json"
21+
"errors"
2122
"sync"
2223
"time"
2324

2425
corev1 "k8s.io/api/core/v1"
2526
apierrors "k8s.io/apimachinery/pkg/api/errors"
2627
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/apimachinery/pkg/util/wait"
2729
corev1apply "k8s.io/client-go/applyconfigurations/core/v1"
2830
"sigs.k8s.io/controller-runtime/pkg/builder"
2931
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -65,6 +67,9 @@ type InstallationReconciler struct {
6567
Patch(context.Context, client.Object, client.Patch, ...client.PatchOption) error
6668
}
6769

70+
// Refresh is the frequency at which AuthObjects should be renewed.
71+
Refresh time.Duration
72+
6873
// SecretRef is the name of the corev1.Secret in which to store Bridge tokens.
6974
SecretRef client.ObjectKey
7075

@@ -79,6 +84,7 @@ func ManagedInstallationReconciler(m manager.Manager, newClient func() *Client)
7984
Owner: naming.ControllerBridge,
8085
Reader: kubernetes,
8186
Writer: kubernetes,
87+
Refresh: 2 * time.Hour,
8288
SecretRef: naming.AsObjectKey(naming.OperatorConfigurationSecret()),
8389
NewClient: newClient,
8490
}
@@ -119,7 +125,7 @@ func (r *InstallationReconciler) Reconcile(
119125
// make it so.
120126
secret.Namespace, secret.Name = request.Namespace, request.Name
121127

122-
err = r.reconcile(ctx, secret)
128+
result.RequeueAfter, err = r.reconcile(ctx, secret)
123129
}
124130

125131
// TODO: Check for corev1.NamespaceTerminatingCause after
@@ -134,10 +140,14 @@ func (r *InstallationReconciler) Reconcile(
134140
return result, err
135141
}
136142

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+
) {
138148
write, err := corev1apply.ExtractSecret(read, string(r.Owner))
139149
if err != nil {
140-
return err
150+
return 0, err
141151
}
142152

143153
// 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
157167
// Secret which triggers another reconcile.
158168
if len(installation.ID) == 0 {
159169
if len(self.ID) == 0 {
160-
return r.register(ctx, write)
170+
return 0, r.register(ctx, write)
161171
}
162172

163173
data := map[string][]byte{}
164174
data[KeyBridgeToken], _ = json.Marshal(self.Installation) //nolint:errchkjson
165175

166-
return r.persist(ctx, write.WithData(data))
176+
return 0, r.persist(ctx, write.WithData(data))
167177
}
168178

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)
175190
}
176191

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
178194
}
179195

180196
// persist uses Server-Side Apply to write config to Kubernetes. The Name and
@@ -198,6 +214,52 @@ func (r *InstallationReconciler) persist(
198214
return err
199215
}
200216

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+
201263
// register calls the Bridge API to register a new Installation. It stores the
202264
// result in the [self] singleton and the write object in Kubernetes. The Name
203265
// and Namespace fields of the latter cannot be nil.

‎internal/bridge/installation_test.go

Lines changed: 222 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import (
2222
"net/http"
2323
"net/http/httptest"
2424
"testing"
25+
"time"
2526

2627
"gotest.tools/v3/assert"
28+
cmpopt "gotest.tools/v3/assert/opt"
2729
corev1 "k8s.io/api/core/v1"
2830
corev1apply "k8s.io/client-go/applyconfigurations/core/v1"
2931
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -127,8 +129,9 @@ func TestInstallationReconcile(t *testing.T) {
127129
}
128130

129131
ctx := context.Background()
130-
err := reconciler.reconcile(ctx, secret)
132+
next, err := reconciler.reconcile(ctx, secret)
131133
assert.NilError(t, err)
134+
assert.Assert(t, next == 0)
132135

133136
// It calls the API.
134137
assert.Equal(t, len(requests), 1)
@@ -178,7 +181,7 @@ func TestInstallationReconcile(t *testing.T) {
178181
}
179182

180183
ctx := context.Background()
181-
err := reconciler.reconcile(ctx, secret)
184+
_, err := reconciler.reconcile(ctx, secret)
182185
assert.Equal(t, err, expected, "expected a Kubernetes error")
183186

184187
// It stores the API result in memory.
@@ -227,8 +230,9 @@ func TestInstallationReconcile(t *testing.T) {
227230
}
228231

229232
ctx := context.Background()
230-
err := reconciler.reconcile(ctx, secret)
233+
next, err := reconciler.reconcile(ctx, secret)
231234
assert.NilError(t, err)
235+
assert.Assert(t, next == 0)
232236

233237
assert.Equal(t, self.ID, "asdf", "expected no change to memory")
234238

@@ -254,15 +258,15 @@ func TestInstallationReconcile(t *testing.T) {
254258
}
255259

256260
ctx := context.Background()
257-
err := reconciler.reconcile(ctx, secret)
261+
_, err := reconciler.reconcile(ctx, secret)
258262
assert.Equal(t, err, expected, "expected a Kubernetes error")
259263
assert.Equal(t, self.ID, "asdf", "expected no change to memory")
260264
})
261265
})
262266

263267
// Scenario:
264268
// When there is a Secret but no Installation in memory,
265-
// Then Reconcile should store it in memory.
269+
// Then Reconcile should verify it in the API and store it in memory.
266270
//
267271
t.Run("Restart", func(t *testing.T) {
268272
var reconciler *InstallationReconciler
@@ -271,18 +275,228 @@ func TestInstallationReconcile(t *testing.T) {
271275
beforeEach := func() {
272276
reconciler = new(InstallationReconciler)
273277
secret = new(corev1.Secret)
274-
secret.Data = map[string][]byte{KeyBridgeToken: []byte(`{"id":"xyz"}`)}
278+
secret.Data = map[string][]byte{
279+
KeyBridgeToken: []byte(`{
280+
"id":"xyz", "auth_object":{
281+
"secret":"abc",
282+
"expires_at":"2020-10-28T05:06:07Z"
283+
}
284+
}`),
285+
}
275286
self.Installation = Installation{}
276287
}
277288

278-
t.Run("ItLoads", func(t *testing.T) {
289+
t.Run("ItVerifies", func(t *testing.T) {
279290
beforeEach()
280291

292+
// API double; spy on requests.
293+
var requests []http.Request
294+
{
295+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
296+
requests = append(requests, *r)
297+
_ = json.NewEncoder(w).Encode(map[string]any{"secret": "def"})
298+
}))
299+
t.Cleanup(server.Close)
300+
301+
reconciler.NewClient = func() *Client {
302+
c := NewClient(server.URL, "")
303+
c.Backoff.Steps = 1
304+
assert.Equal(t, c.BaseURL.String(), server.URL)
305+
return c
306+
}
307+
}
308+
309+
// Kubernetes double; spy on SSA patches.
310+
var applies []string
311+
{
312+
reconciler.Writer = runtime.ClientPatch(func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
313+
assert.Equal(t, string(patch.Type()), "application/apply-patch+yaml")
314+
315+
data, err := patch.Data(obj)
316+
applies = append(applies, string(data))
317+
return err
318+
})
319+
}
320+
281321
ctx := context.Background()
282-
err := reconciler.reconcile(ctx, secret)
322+
next, err := reconciler.reconcile(ctx, secret)
283323
assert.NilError(t, err)
324+
assert.Assert(t, next == 0)
284325

326+
assert.Equal(t, len(requests), 1)
327+
assert.Equal(t, requests[0].Header.Get("Authorization"), "Bearer abc")
328+
assert.Equal(t, requests[0].Method, "POST")
329+
assert.Equal(t, requests[0].URL.Path, "/vendor/operator/auth-objects")
330+
331+
// It stores the result in memory.
285332
assert.Equal(t, self.ID, "xyz")
333+
assert.Equal(t, self.AuthObject.Secret, "def")
334+
335+
// It stores the memory in Kubernetes.
336+
assert.Equal(t, len(applies), 1)
337+
assert.Assert(t, cmp.Contains(applies[0], `"kind":"Secret"`))
338+
339+
var decoded corev1.Secret
340+
assert.NilError(t, yaml.Unmarshal([]byte(applies[0]), &decoded))
341+
assert.Assert(t, cmp.Contains(string(decoded.Data["bridge-token"]), `"id":"xyz"`))
342+
assert.Assert(t, cmp.Contains(string(decoded.Data["bridge-token"]), `"secret":"def"`))
343+
})
344+
345+
t.Run("Expired", func(t *testing.T) {
346+
beforeEach()
347+
348+
// API double; authentication error.
349+
{
350+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
351+
w.WriteHeader(http.StatusUnauthorized)
352+
}))
353+
t.Cleanup(server.Close)
354+
355+
reconciler.NewClient = func() *Client {
356+
c := NewClient(server.URL, "")
357+
c.Backoff.Steps = 1
358+
assert.Equal(t, c.BaseURL.String(), server.URL)
359+
return c
360+
}
361+
}
362+
363+
// Kubernetes double; spy on SSA patches.
364+
var applies []string
365+
{
366+
reconciler.Writer = runtime.ClientPatch(func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
367+
assert.Equal(t, string(patch.Type()), "application/apply-patch+yaml")
368+
369+
data, err := patch.Data(obj)
370+
applies = append(applies, string(data))
371+
return err
372+
})
373+
}
374+
375+
ctx := context.Background()
376+
next, err := reconciler.reconcile(ctx, secret)
377+
assert.NilError(t, err)
378+
assert.Assert(t, next == 0)
379+
380+
assert.DeepEqual(t, self.Installation, Installation{})
381+
382+
// It archives the expired one.
383+
assert.Equal(t, len(applies), 1)
384+
assert.Assert(t, cmp.Contains(applies[0], `"kind":"Secret"`))
385+
386+
var decoded corev1.Secret
387+
assert.NilError(t, yaml.Unmarshal([]byte(applies[0]), &decoded))
388+
assert.Equal(t, len(decoded.Data["bridge-token"]), 0)
389+
390+
archived := string(decoded.Data["bridge-token--2020-10-28"])
391+
assert.Assert(t, cmp.Contains(archived, `"id":"xyz"`))
392+
assert.Assert(t, cmp.Contains(archived, `"secret":"abc"`))
393+
})
394+
})
395+
396+
// Scenario:
397+
// When there is an Installation in the Secret and in memory,
398+
// Then Reconcile should refresh it periodically.
399+
//
400+
t.Run("Refresh", func(t *testing.T) {
401+
var reconciler *InstallationReconciler
402+
var secret *corev1.Secret
403+
404+
beforeEach := func(timestamp []byte) {
405+
reconciler = new(InstallationReconciler)
406+
reconciler.Refresh = time.Minute
407+
408+
secret = new(corev1.Secret)
409+
secret.Data = map[string][]byte{
410+
KeyBridgeToken: []byte(`{"id":"ddd", "auth_object":{"secret":"eee"}}`),
411+
KeyBridgeLocalTime: timestamp,
412+
}
413+
414+
self.Installation = Installation{ID: "ddd"}
415+
}
416+
417+
for _, tt := range []struct {
418+
Name string
419+
Timestamp []byte
420+
}{
421+
{Name: "NoTimestamp", Timestamp: nil},
422+
{Name: "BadTimestamp", Timestamp: []byte(`asdf`)},
423+
{Name: "OldTimestamp", Timestamp: []byte(`"2020-10-10T20:20:20Z"`)},
424+
{Name: "FutureTimestamp", Timestamp: []byte(`"2030-10-10T20:20:20Z"`)},
425+
} {
426+
t.Run(tt.Name, func(t *testing.T) {
427+
beforeEach(tt.Timestamp)
428+
429+
// API double; spy on requests.
430+
var requests []http.Request
431+
{
432+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
433+
requests = append(requests, *r)
434+
_ = json.NewEncoder(w).Encode(map[string]any{"secret": "fresh"})
435+
}))
436+
t.Cleanup(server.Close)
437+
438+
reconciler.NewClient = func() *Client {
439+
c := NewClient(server.URL, "")
440+
c.Backoff.Steps = 1
441+
assert.Equal(t, c.BaseURL.String(), server.URL)
442+
return c
443+
}
444+
}
445+
446+
// Kubernetes double; spy on SSA patches.
447+
var applies []string
448+
{
449+
reconciler.Writer = runtime.ClientPatch(func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
450+
assert.Equal(t, string(patch.Type()), "application/apply-patch+yaml")
451+
452+
data, err := patch.Data(obj)
453+
applies = append(applies, string(data))
454+
return err
455+
})
456+
}
457+
458+
ctx := context.Background()
459+
next, err := reconciler.reconcile(ctx, secret)
460+
assert.NilError(t, err)
461+
assert.Assert(t, next == 0)
462+
463+
assert.Equal(t, len(requests), 1)
464+
assert.Equal(t, requests[0].Header.Get("Authorization"), "Bearer eee")
465+
assert.Equal(t, requests[0].Method, "POST")
466+
assert.Equal(t, requests[0].URL.Path, "/vendor/operator/auth-objects")
467+
468+
// It stores the result in memory.
469+
assert.Equal(t, self.ID, "ddd")
470+
assert.Equal(t, self.AuthObject.Secret, "fresh")
471+
472+
// It stores the memory in Kubernetes.
473+
assert.Equal(t, len(applies), 1)
474+
assert.Assert(t, cmp.Contains(applies[0], `"kind":"Secret"`))
475+
476+
var decoded corev1.Secret
477+
assert.NilError(t, yaml.Unmarshal([]byte(applies[0]), &decoded))
478+
assert.Assert(t, cmp.Contains(string(decoded.Data["bridge-token"]), `"id":"ddd"`))
479+
assert.Assert(t, cmp.Contains(string(decoded.Data["bridge-token"]), `"secret":"fresh"`))
480+
})
481+
}
482+
483+
t.Run("CurrentTimestamp", func(t *testing.T) {
484+
current := time.Now().Add(-15 * time.Minute)
485+
currentJSON, _ := current.UTC().MarshalJSON()
486+
487+
beforeEach(currentJSON)
488+
reconciler.Refresh = time.Hour
489+
490+
// Any API calls would panic because no spies are configured here.
491+
492+
ctx := context.Background()
493+
next, err := reconciler.reconcile(ctx, secret)
494+
assert.NilError(t, err)
495+
496+
// The next reconcile is scheduled around (60 - 15 =) 45 minutes
497+
// from now, plus or minus (60 * 10% =) 6 minutes of jitter.
498+
assert.DeepEqual(t, next, 45*time.Minute,
499+
cmpopt.DurationWithThreshold(6*time.Minute))
286500
})
287501
})
288502
}

0 commit comments

Comments
 (0)
Please sign in to comment.