Skip to content

Commit 4e32b8a

Browse files
committed
validate runner secrets before projection
1 parent 781d11c commit 4e32b8a

4 files changed

Lines changed: 177 additions & 29 deletions

File tree

src/runner-org-sync/internal/k8sstate/k8sstate.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ const (
3535
SecretTokenKey = "token"
3636
)
3737

38+
// RegistrationSecretState describes whether a per-org runner registration
39+
// Secret is safe for the ConfigMap to reference.
40+
type RegistrationSecretState string
41+
42+
const (
43+
RegistrationSecretMissing RegistrationSecretState = "missing"
44+
RegistrationSecretValid RegistrationSecretState = "valid"
45+
RegistrationSecretInvalid RegistrationSecretState = "invalid"
46+
)
47+
3848
// Store is the package's only entry point for cluster I/O.
3949
type Store struct {
4050
client kubernetes.Interface
@@ -78,6 +88,30 @@ func (s *Store) SecretExists(ctx context.Context, name string) (bool, error) {
7888
return false, fmt.Errorf("k8sstate: get secret %s: %w", name, err)
7989
}
8090

91+
// RegistrationSecretStatus reports whether the named Secret exists and has
92+
// the ownership labels and token data expected for the given org.
93+
func (s *Store) RegistrationSecretStatus(ctx context.Context, name, org string) (RegistrationSecretState, error) {
94+
sec, err := s.client.CoreV1().Secrets(s.namespace).Get(ctx, name, metav1.GetOptions{})
95+
if apierrors.IsNotFound(err) {
96+
return RegistrationSecretMissing, nil
97+
}
98+
if err != nil {
99+
return "", fmt.Errorf("k8sstate: get registration secret %s: %w", name, err)
100+
}
101+
if sec.Type != "" && sec.Type != corev1.SecretTypeOpaque {
102+
return RegistrationSecretInvalid, nil
103+
}
104+
if sec.Labels[LabelManagedBy] != ManagedBy ||
105+
sec.Labels[LabelComponent] != ComponentRegToken ||
106+
sec.Labels[LabelOrg] != org {
107+
return RegistrationSecretInvalid, nil
108+
}
109+
if len(sec.Data[SecretTokenKey]) == 0 {
110+
return RegistrationSecretInvalid, nil
111+
}
112+
return RegistrationSecretValid, nil
113+
}
114+
81115
// CreateRegistrationSecret creates an Opaque Secret carrying the
82116
// registration token at key "token", labelled with ManagedBy / Component /
83117
// Org. Returns the underlying error verbatim so callers can use apierrors.IsAlreadyExists.

src/runner-org-sync/internal/k8sstate/k8sstate_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,72 @@ func TestSecretExists(t *testing.T) {
6868
}
6969
}
7070

71+
func TestRegistrationSecretStatus(t *testing.T) {
72+
c := fake.NewSimpleClientset(
73+
&corev1.Secret{
74+
ObjectMeta: metav1.ObjectMeta{
75+
Name: "valid",
76+
Namespace: testNamespace,
77+
Labels: map[string]string{
78+
LabelManagedBy: ManagedBy,
79+
LabelComponent: ComponentRegToken,
80+
LabelOrg: "ttd",
81+
},
82+
},
83+
Type: corev1.SecretTypeOpaque,
84+
Data: map[string][]byte{SecretTokenKey: []byte("tok")},
85+
},
86+
&corev1.Secret{
87+
ObjectMeta: metav1.ObjectMeta{
88+
Name: "foreign",
89+
Namespace: testNamespace,
90+
Labels: map[string]string{LabelManagedBy: "someone-else"},
91+
},
92+
Type: corev1.SecretTypeOpaque,
93+
Data: map[string][]byte{SecretTokenKey: []byte("tok")},
94+
},
95+
&corev1.Secret{
96+
ObjectMeta: metav1.ObjectMeta{
97+
Name: "empty-token",
98+
Namespace: testNamespace,
99+
Labels: map[string]string{
100+
LabelManagedBy: ManagedBy,
101+
LabelComponent: ComponentRegToken,
102+
LabelOrg: "ttd",
103+
},
104+
},
105+
Type: corev1.SecretTypeOpaque,
106+
Data: map[string][]byte{SecretTokenKey: nil},
107+
},
108+
)
109+
s := NewStore(c, testNamespace)
110+
111+
tests := []struct {
112+
name string
113+
secretName string
114+
org string
115+
want RegistrationSecretState
116+
}{
117+
{name: "valid", secretName: "valid", org: "ttd", want: RegistrationSecretValid},
118+
{name: "missing", secretName: "missing", org: "ttd", want: RegistrationSecretMissing},
119+
{name: "foreign same name", secretName: "foreign", org: "ttd", want: RegistrationSecretInvalid},
120+
{name: "wrong org", secretName: "valid", org: "brg", want: RegistrationSecretInvalid},
121+
{name: "empty token", secretName: "empty-token", org: "ttd", want: RegistrationSecretInvalid},
122+
}
123+
124+
for _, tt := range tests {
125+
t.Run(tt.name, func(t *testing.T) {
126+
got, err := s.RegistrationSecretStatus(context.Background(), tt.secretName, tt.org)
127+
if err != nil {
128+
t.Fatalf("unexpected error: %v", err)
129+
}
130+
if got != tt.want {
131+
t.Errorf("RegistrationSecretStatus() = %q, want %q", got, tt.want)
132+
}
133+
})
134+
}
135+
}
136+
71137
func TestDeleteSecret_IdempotentOnMissing(t *testing.T) {
72138
c := fake.NewSimpleClientset()
73139
s := NewStore(c, testNamespace)

src/runner-org-sync/internal/reconcile/reconcile.go

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import (
1313
"errors"
1414
"fmt"
1515
"sort"
16-
"strings"
1716

1817
"altinn.studio/runner-org-sync/internal/cdn"
1918
"altinn.studio/runner-org-sync/internal/gitea"
2019
"altinn.studio/runner-org-sync/internal/k8sstate"
2120
corev1 "k8s.io/api/core/v1"
21+
"sigs.k8s.io/yaml"
2222
)
2323

2424
// Defaults used when the caller does not override.
@@ -30,9 +30,10 @@ const (
3030

3131
// Failure stages, surfaced on Report.FailedOrgs[*].Stage.
3232
const (
33-
StageMint = "mint"
34-
StageCreate = "create"
35-
StageDelete = "delete"
33+
StageValidate = "validate"
34+
StageMint = "mint"
35+
StageCreate = "create"
36+
StageDelete = "delete"
3637
)
3738

3839
// OrgSource produces the discovered org population (typically the CDN client).
@@ -50,7 +51,7 @@ type TokenMinter interface {
5051
// SecretStore is the cluster I/O surface the Reconciler needs.
5152
type SecretStore interface {
5253
ListManagedSecrets(ctx context.Context) ([]corev1.Secret, error)
53-
SecretExists(ctx context.Context, name string) (bool, error)
54+
RegistrationSecretStatus(ctx context.Context, name, org string) (k8sstate.RegistrationSecretState, error)
5455
CreateRegistrationSecret(ctx context.Context, name, org, token string) error
5556
DeleteSecret(ctx context.Context, name string) error
5657
ApplyConfigMap(ctx context.Context, name string, data map[string]string) (bool, error)
@@ -171,16 +172,24 @@ func (r *Reconciler) Run(ctx context.Context) (Report, error) {
171172
orgHasSecret := make(map[string]bool, len(desired))
172173
for _, org := range desired {
173174
name := r.secretNameFor(org.Code)
174-
exists, err := r.store.SecretExists(ctx, name)
175+
status, err := r.store.RegistrationSecretStatus(ctx, name, org.Code)
175176
if err != nil {
176-
// SecretExists hitting a transient apiserver error is fatal for
177+
// The lookup hitting a transient apiserver error is fatal for
177178
// this run — without this lookup we cannot decide mint-or-skip.
178-
return report, fmt.Errorf("reconcile: check secret %s: %w", name, err)
179+
return report, fmt.Errorf("reconcile: check registration secret %s: %w", name, err)
179180
}
180-
if exists {
181+
switch status {
182+
case k8sstate.RegistrationSecretValid:
181183
report.SecretsSkipped = append(report.SecretsSkipped, org.Code)
182184
orgHasSecret[org.Code] = true
183185
continue
186+
case k8sstate.RegistrationSecretInvalid:
187+
report.FailedOrgs = append(report.FailedOrgs, OrgFailure{
188+
Org: org.Code,
189+
Stage: StageValidate,
190+
Err: fmt.Errorf("registration secret %s exists but is not a valid runner token secret", name),
191+
})
192+
continue
184193
}
185194
token, err := r.minter.MintRegistrationToken(ctx, org.Code)
186195
if err != nil {
@@ -287,15 +296,26 @@ func (r *Reconciler) filter(orgs []cdn.Org, report *Report) []cdn.Org {
287296
// on the consumer side, so a runner-org-sync-supplied replicas field would
288297
// be ignored at best and misleading at worst.
289298
func renderRunners(orgs []string, secretNameFor func(org string) string) string {
290-
if len(orgs) == 0 {
291-
return "[]\n"
292-
}
293-
var b strings.Builder
299+
runners := make([]runnerConfig, 0, len(orgs))
294300
for _, org := range orgs {
295-
fmt.Fprintf(&b, "- name: %s\n", org)
296-
fmt.Fprintf(&b, " registrationTokenSecretName: %s\n", secretNameFor(org))
301+
runners = append(runners, runnerConfig{
302+
Name: org,
303+
RegistrationTokenSecretName: secretNameFor(org),
304+
})
305+
}
306+
out, err := yaml.Marshal(runners)
307+
if err != nil {
308+
// The input is a simple slice of strings rendered into a static struct;
309+
// yaml.Marshal should not fail. Keep the historical empty-list output
310+
// if it ever does, so the chart does not reference stale runners.
311+
return "[]\n"
297312
}
298-
return b.String()
313+
return string(out)
314+
}
315+
316+
type runnerConfig struct {
317+
Name string `json:"name"`
318+
RegistrationTokenSecretName string `json:"registrationTokenSecretName"`
299319
}
300320

301321
func orgCodes(orgs []cdn.Org) []string {

src/runner-org-sync/internal/reconcile/reconcile_test.go

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"altinn.studio/runner-org-sync/internal/cdn"
1212
"altinn.studio/runner-org-sync/internal/gitea"
13+
"altinn.studio/runner-org-sync/internal/k8sstate"
1314
corev1 "k8s.io/api/core/v1"
1415
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1516
)
@@ -43,7 +44,7 @@ func (m *stubMinter) MintRegistrationToken(_ context.Context, org string) (strin
4344

4445
type stubStore struct {
4546
managed []corev1.Secret
46-
existsByName map[string]bool
47+
statusByName map[string]k8sstate.RegistrationSecretState
4748
createErr map[string]error
4849
deleteErr map[string]error
4950
applyCMErr error
@@ -58,7 +59,7 @@ type stubStore struct {
5859

5960
func newStubStore() *stubStore {
6061
return &stubStore{
61-
existsByName: map[string]bool{},
62+
statusByName: map[string]k8sstate.RegistrationSecretState{},
6263
createErr: map[string]error{},
6364
deleteErr: map[string]error{},
6465
createdOrgs: map[string]string{},
@@ -70,11 +71,14 @@ func (s *stubStore) ListManagedSecrets(_ context.Context) ([]corev1.Secret, erro
7071
return s.managed, s.listErr
7172
}
7273

73-
func (s *stubStore) SecretExists(_ context.Context, name string) (bool, error) {
74+
func (s *stubStore) RegistrationSecretStatus(_ context.Context, name, _ string) (k8sstate.RegistrationSecretState, error) {
7475
if s.existsErr != nil {
75-
return false, s.existsErr
76+
return "", s.existsErr
7677
}
77-
return s.existsByName[name], nil
78+
if status, ok := s.statusByName[name]; ok {
79+
return status, nil
80+
}
81+
return k8sstate.RegistrationSecretMissing, nil
7882
}
7983

8084
func (s *stubStore) CreateRegistrationSecret(_ context.Context, name, org, _ string) error {
@@ -83,7 +87,7 @@ func (s *stubStore) CreateRegistrationSecret(_ context.Context, name, org, _ str
8387
}
8488
s.createdSecrets = append(s.createdSecrets, name)
8589
s.createdOrgs[name] = org
86-
s.existsByName[name] = true
90+
s.statusByName[name] = k8sstate.RegistrationSecretValid
8791
return nil
8892
}
8993

@@ -190,8 +194,8 @@ func TestRun_IdempotentReRun(t *testing.T) {
190194
minter := &stubMinter{}
191195
store := newStubStore()
192196
// pre-populate existing state — secrets exist for both orgs and we own them.
193-
store.existsByName["altinn-gitea-runner-ttd-secret"] = true
194-
store.existsByName["altinn-gitea-runner-brg-secret"] = true
197+
store.statusByName["altinn-gitea-runner-ttd-secret"] = k8sstate.RegistrationSecretValid
198+
store.statusByName["altinn-gitea-runner-brg-secret"] = k8sstate.RegistrationSecretValid
195199
store.managed = []corev1.Secret{
196200
managedSecret("altinn-gitea-runner-ttd-secret", "ttd"),
197201
managedSecret("altinn-gitea-runner-brg-secret", "brg"),
@@ -226,8 +230,8 @@ func TestRun_OrgAdded(t *testing.T) {
226230
}}
227231
minter := &stubMinter{}
228232
store := newStubStore()
229-
store.existsByName["altinn-gitea-runner-ttd-secret"] = true
230-
store.existsByName["altinn-gitea-runner-brg-secret"] = true
233+
store.statusByName["altinn-gitea-runner-ttd-secret"] = k8sstate.RegistrationSecretValid
234+
store.statusByName["altinn-gitea-runner-brg-secret"] = k8sstate.RegistrationSecretValid
231235
store.managed = []corev1.Secret{
232236
managedSecret("altinn-gitea-runner-ttd-secret", "ttd"),
233237
managedSecret("altinn-gitea-runner-brg-secret", "brg"),
@@ -254,8 +258,8 @@ func TestRun_OrgRemoved(t *testing.T) {
254258
}}
255259
minter := &stubMinter{}
256260
store := newStubStore()
257-
store.existsByName["altinn-gitea-runner-ttd-secret"] = true
258-
store.existsByName["altinn-gitea-runner-brg-secret"] = true
261+
store.statusByName["altinn-gitea-runner-ttd-secret"] = k8sstate.RegistrationSecretValid
262+
store.statusByName["altinn-gitea-runner-brg-secret"] = k8sstate.RegistrationSecretValid
259263
store.managed = []corev1.Secret{
260264
managedSecret("altinn-gitea-runner-ttd-secret", "ttd"),
261265
managedSecret("altinn-gitea-runner-brg-secret", "brg"),
@@ -347,6 +351,30 @@ func TestRun_GiteaPartialFailure(t *testing.T) {
347351
}
348352
}
349353

354+
func TestRun_InvalidExistingSecretIsNotProjected(t *testing.T) {
355+
src := &stubSource{orgs: []cdn.Org{
356+
{Code: "ttd", Environments: []string{"tt02"}},
357+
}}
358+
minter := &stubMinter{}
359+
store := newStubStore()
360+
store.statusByName["altinn-gitea-runner-ttd-secret"] = k8sstate.RegistrationSecretInvalid
361+
362+
rep := runReconciler(t, src, minter, store, []string{"ttd"}, false)
363+
364+
if rep.Outcome != OutcomePartial {
365+
t.Errorf("outcome = %v, want partial", rep.Outcome)
366+
}
367+
if len(rep.FailedOrgs) != 1 || rep.FailedOrgs[0].Org != "ttd" || rep.FailedOrgs[0].Stage != StageValidate {
368+
t.Errorf("FailedOrgs = %v, want [{ttd validate ...}]", rep.FailedOrgs)
369+
}
370+
if len(minter.calls) != 0 {
371+
t.Errorf("minter should not be called when same-name invalid secret exists; got %v", minter.calls)
372+
}
373+
if got := store.appliedCMData[ConfigMapDataKey]; got != "[]\n" {
374+
t.Errorf("ConfigMap body = %q, want empty runner list", got)
375+
}
376+
}
377+
350378
// --- additional coverage ----------------------------------------------------
351379

352380
// Auth failures hit every org with the same PAT — failing fast avoids a
@@ -449,7 +477,7 @@ func TestRun_SyncAllSkipsWhitelist(t *testing.T) {
449477
func TestRun_UnlabelledManagedSecretIsSkippedOnDelete(t *testing.T) {
450478
src := &stubSource{orgs: []cdn.Org{{Code: "ttd", Environments: []string{"tt02"}}}}
451479
store := newStubStore()
452-
store.existsByName["altinn-gitea-runner-ttd-secret"] = true
480+
store.statusByName["altinn-gitea-runner-ttd-secret"] = k8sstate.RegistrationSecretValid
453481
store.managed = []corev1.Secret{
454482
managedSecret("altinn-gitea-runner-ttd-secret", "ttd"),
455483
// drift: managed-by label but no org label

0 commit comments

Comments
 (0)