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 d09d1b8

Browse files
committedJun 3, 2025··
Support serviceaccount pull secrets
Serviceaccounts reference pull secrets! * Determine our serviceaccount (via the new internal/shared/util/sa package). * Use a common pull_secret_controller * Update the pull_secret_controller to know about the service account * Update the pull_secret_controller to watch the namespace-local secrets * Update caching to include sa, and use filters for additional secrets * Add RBAC to access these secrets and sa * Update writing the auth.json file to handle dockercfg and dockerconfigjson * Update writing the auth.json file to include multiple secrets Signed-off-by: Todd Short <[email protected]>
1 parent 8f81c23 commit d09d1b8

File tree

16 files changed

+488
-369
lines changed

16 files changed

+488
-369
lines changed
 

‎cmd/catalogd/main.go

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ import (
6161
"github.com/operator-framework/operator-controller/internal/catalogd/serverutil"
6262
"github.com/operator-framework/operator-controller/internal/catalogd/storage"
6363
"github.com/operator-framework/operator-controller/internal/catalogd/webhook"
64+
sharedcontrollers "github.com/operator-framework/operator-controller/internal/shared/controllers"
6465
fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs"
6566
imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image"
67+
sautil "github.com/operator-framework/operator-controller/internal/shared/util/sa"
6668
"github.com/operator-framework/operator-controller/internal/shared/version"
6769
)
6870

@@ -246,18 +248,40 @@ func run(ctx context.Context) error {
246248
cacheOptions := crcache.Options{
247249
ByObject: map[client.Object]crcache.ByObject{},
248250
}
249-
if cfg.globalPullSecretKey != nil {
250-
cacheOptions.ByObject[&corev1.Secret{}] = crcache.ByObject{
251-
Namespaces: map[string]crcache.Config{
252-
cfg.globalPullSecretKey.Namespace: {
253-
LabelSelector: k8slabels.Everything(),
254-
FieldSelector: fields.SelectorFromSet(map[string]string{
255-
"metadata.name": cfg.globalPullSecretKey.Name,
256-
}),
257-
},
251+
252+
saKey, err := sautil.GetServiceAccount()
253+
if err != nil {
254+
setupLog.Error(err, "Unable to get pod namesapce and serviceaccount")
255+
return err
256+
}
257+
258+
setupLog.Info("Read token", "serviceaccount", saKey)
259+
cacheOptions.ByObject[&corev1.ServiceAccount{}] = crcache.ByObject{
260+
Namespaces: map[string]crcache.Config{
261+
saKey.Namespace: {
262+
LabelSelector: k8slabels.Everything(),
263+
FieldSelector: fields.SelectorFromSet(map[string]string{
264+
"metadata.name": saKey.Name,
265+
}),
258266
},
267+
},
268+
}
269+
270+
secretCache := crcache.ByObject{}
271+
secretCache.Namespaces = make(map[string]crcache.Config, 2)
272+
secretCache.Namespaces[saKey.Namespace] = crcache.Config{
273+
LabelSelector: k8slabels.Everything(),
274+
FieldSelector: fields.Everything(),
275+
}
276+
if cfg.globalPullSecretKey != nil {
277+
secretCache.Namespaces[cfg.globalPullSecretKey.Namespace] = crcache.Config{
278+
LabelSelector: k8slabels.Everything(),
279+
FieldSelector: fields.SelectorFromSet(map[string]string{
280+
"metadata.name": cfg.globalPullSecretKey.Name,
281+
}),
259282
}
260283
}
284+
cacheOptions.ByObject[&corev1.Secret{}] = secretCache
261285

262286
// Create manager
263287
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
@@ -312,7 +336,7 @@ func run(ctx context.Context) error {
312336
DockerCertPath: cfg.pullCasDir,
313337
OCICertPath: cfg.pullCasDir,
314338
}
315-
if _, err := os.Stat(authFilePath); err == nil && cfg.globalPullSecretKey != nil {
339+
if _, err := os.Stat(authFilePath); err == nil {
316340
logger.Info("using available authentication information for pulling image")
317341
srcContext.AuthFilePath = authFilePath
318342
} else if os.IsNotExist(err) {
@@ -370,17 +394,16 @@ func run(ctx context.Context) error {
370394
return err
371395
}
372396

373-
if cfg.globalPullSecretKey != nil {
374-
setupLog.Info("creating SecretSyncer controller for watching secret", "Secret", cfg.globalPullSecret)
375-
err := (&corecontrollers.PullSecretReconciler{
376-
Client: mgr.GetClient(),
377-
AuthFilePath: authFilePath,
378-
SecretKey: *cfg.globalPullSecretKey,
379-
}).SetupWithManager(mgr)
380-
if err != nil {
381-
setupLog.Error(err, "unable to create controller", "controller", "SecretSyncer")
382-
return err
383-
}
397+
setupLog.Info("creating SecretSyncer controller for watching secret", "Secret", cfg.globalPullSecret)
398+
err = (&sharedcontrollers.PullSecretReconciler{
399+
Client: mgr.GetClient(),
400+
AuthFilePath: authFilePath,
401+
SecretKey: cfg.globalPullSecretKey,
402+
ServiceAccountKey: saKey,
403+
}).SetupWithManager(mgr)
404+
if err != nil {
405+
setupLog.Error(err, "unable to create controller", "controller", "SecretSyncer")
406+
return err
384407
}
385408
//+kubebuilder:scaffold:builder
386409

‎cmd/operator-controller/main.go

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,11 @@ import (
7171
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/certproviders"
7272
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1"
7373
"github.com/operator-framework/operator-controller/internal/operator-controller/scheme"
74+
sharedcontrollers "github.com/operator-framework/operator-controller/internal/shared/controllers"
7475
fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs"
7576
httputil "github.com/operator-framework/operator-controller/internal/shared/util/http"
7677
imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image"
78+
sautil "github.com/operator-framework/operator-controller/internal/shared/util/sa"
7779
"github.com/operator-framework/operator-controller/internal/shared/version"
7880
)
7981

@@ -217,18 +219,40 @@ func run() error {
217219
},
218220
DefaultLabelSelector: k8slabels.Nothing(),
219221
}
220-
if globalPullSecretKey != nil {
221-
cacheOptions.ByObject[&corev1.Secret{}] = crcache.ByObject{
222-
Namespaces: map[string]crcache.Config{
223-
globalPullSecretKey.Namespace: {
224-
LabelSelector: k8slabels.Everything(),
225-
FieldSelector: fields.SelectorFromSet(map[string]string{
226-
"metadata.name": globalPullSecretKey.Name,
227-
}),
228-
},
222+
223+
saKey, err := sautil.GetServiceAccount()
224+
if err != nil {
225+
setupLog.Error(err, "Unable to get pod namesapce and serviceaccount")
226+
return err
227+
}
228+
229+
setupLog.Info("Read token", "serviceaccount", saKey)
230+
cacheOptions.ByObject[&corev1.ServiceAccount{}] = crcache.ByObject{
231+
Namespaces: map[string]crcache.Config{
232+
saKey.Namespace: {
233+
LabelSelector: k8slabels.Everything(),
234+
FieldSelector: fields.SelectorFromSet(map[string]string{
235+
"metadata.name": saKey.Name,
236+
}),
229237
},
238+
},
239+
}
240+
241+
secretCache := crcache.ByObject{}
242+
secretCache.Namespaces = make(map[string]crcache.Config, 2)
243+
secretCache.Namespaces[saKey.Namespace] = crcache.Config{
244+
LabelSelector: k8slabels.Everything(),
245+
FieldSelector: fields.Everything(),
246+
}
247+
if globalPullSecretKey != nil {
248+
secretCache.Namespaces[globalPullSecretKey.Namespace] = crcache.Config{
249+
LabelSelector: k8slabels.Everything(),
250+
FieldSelector: fields.SelectorFromSet(map[string]string{
251+
"metadata.name": globalPullSecretKey.Name,
252+
}),
230253
}
231254
}
255+
cacheOptions.ByObject[&corev1.Secret{}] = secretCache
232256

233257
metricsServerOptions := server.Options{}
234258
if len(cfg.certFile) > 0 && len(cfg.keyFile) > 0 {
@@ -360,7 +384,7 @@ func run() error {
360384
OCICertPath: cfg.pullCasDir,
361385
}
362386
logger := log.FromContext(ctx)
363-
if _, err := os.Stat(authFilePath); err == nil && globalPullSecretKey != nil {
387+
if _, err := os.Stat(authFilePath); err == nil {
364388
logger.Info("using available authentication information for pulling image")
365389
srcContext.AuthFilePath = authFilePath
366390
} else if os.IsNotExist(err) {
@@ -482,17 +506,16 @@ func run() error {
482506
return err
483507
}
484508

485-
if globalPullSecretKey != nil {
486-
setupLog.Info("creating SecretSyncer controller for watching secret", "Secret", cfg.globalPullSecret)
487-
err := (&controllers.PullSecretReconciler{
488-
Client: mgr.GetClient(),
489-
AuthFilePath: authFilePath,
490-
SecretKey: *globalPullSecretKey,
491-
}).SetupWithManager(mgr)
492-
if err != nil {
493-
setupLog.Error(err, "unable to create controller", "controller", "SecretSyncer")
494-
return err
495-
}
509+
setupLog.Info("creating SecretSyncer controller for watching secret", "Secret", cfg.globalPullSecret)
510+
err = (&sharedcontrollers.PullSecretReconciler{
511+
Client: mgr.GetClient(),
512+
AuthFilePath: authFilePath,
513+
SecretKey: globalPullSecretKey,
514+
ServiceAccountKey: saKey,
515+
}).SetupWithManager(mgr)
516+
if err != nil {
517+
setupLog.Error(err, "unable to create controller", "controller", "SecretSyncer")
518+
return err
496519
}
497520

498521
//+kubebuilder:scaffold:builder

‎config/base/catalogd/rbac/role.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ kind: ClusterRole
44
metadata:
55
name: manager-role
66
rules:
7+
- apiGroups:
8+
- ""
9+
resources:
10+
- secrets
11+
verbs:
12+
- get
13+
- list
14+
- watch
715
- apiGroups:
816
- olm.operatorframework.io
917
resources:
@@ -30,3 +38,18 @@ rules:
3038
- get
3139
- patch
3240
- update
41+
---
42+
apiVersion: rbac.authorization.k8s.io/v1
43+
kind: Role
44+
metadata:
45+
name: manager-role
46+
namespace: system
47+
rules:
48+
- apiGroups:
49+
- ""
50+
resources:
51+
- serviceaccounts
52+
verbs:
53+
- get
54+
- list
55+
- watch

‎config/base/catalogd/rbac/role_binding.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,20 @@ subjects:
1313
- kind: ServiceAccount
1414
name: controller-manager
1515
namespace: system
16+
---
17+
apiVersion: rbac.authorization.k8s.io/v1
18+
kind: RoleBinding
19+
metadata:
20+
labels:
21+
app.kubernetes.io/part-of: olm
22+
app.kubernetes.io/name: catalogd
23+
name: manager-rolebinding
24+
namespace: system
25+
roleRef:
26+
apiGroup: rbac.authorization.k8s.io
27+
kind: Role
28+
name: manager-role
29+
subjects:
30+
- kind: ServiceAccount
31+
name: controller-manager
32+
namespace: system

‎config/base/operator-controller/rbac/role.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,11 @@ rules:
7777
- patch
7878
- update
7979
- watch
80+
- apiGroups:
81+
- ""
82+
resources:
83+
- serviceaccounts
84+
verbs:
85+
- get
86+
- list
87+
- watch

‎go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/containers/image/v5 v5.35.0
1212
github.com/fsnotify/fsnotify v1.9.0
1313
github.com/go-logr/logr v1.4.3
14+
github.com/golang-jwt/jwt/v5 v5.2.2
1415
github.com/google/go-cmp v0.7.0
1516
github.com/google/go-containerregistry v0.20.3
1617
github.com/gorilla/handlers v1.5.2

‎go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA
209209
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
210210
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
211211
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
212+
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
213+
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
212214
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
213215
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
214216
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=

‎internal/catalogd/controllers/core/clustercatalog_controller.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ type storedCatalogData struct {
7979
//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clustercatalogs,verbs=get;list;watch;create;update;patch;delete
8080
//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clustercatalogs/status,verbs=get;update;patch
8181
//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clustercatalogs/finalizers,verbs=update
82+
//+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch
83+
//+kubebuilder:rbac:namespace=system,groups=core,resources=serviceaccounts,verbs=get;list;watch
8284

8385
// Reconcile is part of the main kubernetes reconciliation loop which aims to
8486
// move the current state of the cluster closer to the desired state.

‎internal/catalogd/controllers/core/pull_secret_controller.go

Lines changed: 0 additions & 111 deletions
This file was deleted.

‎internal/operator-controller/controllers/clusterextension_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ type InstalledBundleGetter interface {
9595
//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions/finalizers,verbs=update
9696
//+kubebuilder:rbac:namespace=system,groups=core,resources=secrets,verbs=create;update;patch;delete;deletecollection;get;list;watch
9797
//+kubebuilder:rbac:groups=core,resources=serviceaccounts/token,verbs=create
98+
//+kubebuilder:rbac:namespace=system,groups=core,resources=serviceaccounts,verbs=get;list;watch
9899
//+kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get
99100
//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles;clusterrolebindings;roles;rolebindings,verbs=list;watch
100101

‎internal/operator-controller/controllers/pull_secret_controller.go

Lines changed: 0 additions & 111 deletions
This file was deleted.

‎internal/operator-controller/controllers/pull_secret_controller_test.go

Lines changed: 0 additions & 98 deletions
This file was deleted.
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
/*
2+
Copyright 2024.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
"os"
24+
25+
"github.com/go-logr/logr"
26+
corev1 "k8s.io/api/core/v1"
27+
apierrors "k8s.io/apimachinery/pkg/api/errors"
28+
"k8s.io/apimachinery/pkg/types"
29+
ctrl "sigs.k8s.io/controller-runtime"
30+
"sigs.k8s.io/controller-runtime/pkg/client"
31+
"sigs.k8s.io/controller-runtime/pkg/log"
32+
"sigs.k8s.io/controller-runtime/pkg/predicate"
33+
)
34+
35+
// PullSecretReconciler reconciles a specific Secret object
36+
// that contains global pull secrets for pulling Catalog images
37+
type PullSecretReconciler struct {
38+
client.Client
39+
SecretKey *types.NamespacedName
40+
ServiceAccountKey types.NamespacedName
41+
ServiceAccountPullSecrets []types.NamespacedName
42+
AuthFilePath string
43+
}
44+
45+
func (r *PullSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
46+
logger := log.FromContext(ctx).WithName("pull-secret-reconciler")
47+
48+
logger.Info("processing event", "name", req.NamespacedName)
49+
defer logger.Info("processed event", "name", req.NamespacedName)
50+
51+
secrets := []*corev1.Secret{}
52+
secret := &corev1.Secret{}
53+
54+
if r.SecretKey != nil {
55+
// Add the configured pull secret to the list of secrets
56+
err := r.Get(ctx, *r.SecretKey, secret)
57+
if err != nil {
58+
if apierrors.IsNotFound(err) {
59+
logger.Info("secret not found", "name", r.SecretKey)
60+
} else {
61+
logger.Error(err, "failed to get Secret", "name", r.SecretKey)
62+
return ctrl.Result{}, err
63+
}
64+
} else {
65+
logger.Info("global pull secret", "name", *r.SecretKey)
66+
secrets = append(secrets, secret)
67+
}
68+
}
69+
70+
// Grab all the pull secrets from the serviceaccount and add them to the list of secrets
71+
sa := &corev1.ServiceAccount{}
72+
logger.Info("serviceaccount", "name", r.ServiceAccountKey)
73+
err := r.Get(ctx, r.ServiceAccountKey, sa)
74+
if err != nil {
75+
logger.Error(err, "failed to get serviceaccount", "serviceaccount", r.ServiceAccountKey)
76+
return ctrl.Result{}, err
77+
}
78+
nn := types.NamespacedName{Namespace: r.ServiceAccountKey.Namespace}
79+
pullSecrets := []types.NamespacedName{}
80+
for _, ips := range sa.ImagePullSecrets {
81+
nn.Name = ips.Name
82+
secret := &corev1.Secret{}
83+
err = r.Get(ctx, nn, secret)
84+
if err != nil {
85+
if apierrors.IsNotFound(err) {
86+
logger.Info("serviceaccount pull secret not found", "secret", nn)
87+
} else {
88+
logger.Error(err, "failed to get serviceaccount secret", "secret", nn)
89+
return ctrl.Result{}, err
90+
}
91+
} else {
92+
pullSecrets = append(pullSecrets, nn)
93+
secrets = append(secrets, secret)
94+
}
95+
}
96+
// update list of pull secrets from service account
97+
logger.Info("updating list of pull secrets", "list", pullSecrets)
98+
r.ServiceAccountPullSecrets = pullSecrets
99+
100+
if len(secrets) == 0 {
101+
return r.deleteSecretFile(logger)
102+
}
103+
return r.writeSecretToFile(logger, secrets)
104+
}
105+
106+
// SetupWithManager sets up the controller with the Manager.
107+
func (r *PullSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
108+
_, err := ctrl.NewControllerManagedBy(mgr).
109+
For(&corev1.Secret{}).
110+
Named("pull-secret-controller").
111+
WithEventFilter(newSecretPredicate(r)).
112+
Build(r)
113+
if err != nil {
114+
return err
115+
}
116+
117+
_, err = ctrl.NewControllerManagedBy(mgr).
118+
For(&corev1.ServiceAccount{}).
119+
Named("service-account-controller").
120+
WithEventFilter(newNamespacedPredicate(r.ServiceAccountKey)).
121+
Build(r)
122+
123+
return err
124+
}
125+
126+
// Filters based on the global SecretKey, or any pull secret from the serviceaccount
127+
func newSecretPredicate(r *PullSecretReconciler) predicate.Predicate {
128+
return predicate.NewPredicateFuncs(func(obj client.Object) bool {
129+
nn := types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}
130+
if r.SecretKey != nil && nn == *r.SecretKey {
131+
return true
132+
}
133+
for _, ps := range r.ServiceAccountPullSecrets {
134+
if nn == ps {
135+
return true
136+
}
137+
}
138+
return false
139+
})
140+
}
141+
142+
func newNamespacedPredicate(key types.NamespacedName) predicate.Predicate {
143+
return predicate.NewPredicateFuncs(func(obj client.Object) bool {
144+
return obj.GetName() == key.Name && obj.GetNamespace() == key.Namespace
145+
})
146+
}
147+
148+
// Golang representation of the docker configuration - either dockerconfigjson or dockercfg formats.
149+
// This allows us to merge the two formats together, regardless of type, and dump it out as a
150+
// dockerconfigjson for use my contaners/images
151+
type dockerConfigJson struct {
152+
auths dockerCfg `json:"auths"`
153+
}
154+
155+
type dockerCfg map[string]authEntries
156+
157+
type authEntries struct {
158+
auth string `json:"auth"`
159+
email string `json:"email,omitempty"`
160+
}
161+
162+
// writeSecretToFile writes the secret data to the specified file
163+
func (r *PullSecretReconciler) writeSecretToFile(logger logr.Logger, secrets []*corev1.Secret) (ctrl.Result, error) {
164+
// image registry secrets are always stored with the key .dockerconfigjson or .dockercfg
165+
// ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials
166+
// expected format for auth.json
167+
// ref: https://github.com/containers/image/blob/main/docs/containers-auth.json.5.md
168+
169+
jsonData := dockerConfigJson{}
170+
jsonData.auths = make(dockerCfg)
171+
172+
for _, s := range secrets {
173+
if secretData, ok := s.Data[".dockerconfigjson"]; ok {
174+
// process as dockerconfigjson
175+
dcj := &dockerConfigJson{}
176+
if err := json.Unmarshal(secretData, dcj); err != nil {
177+
return ctrl.Result{}, err
178+
}
179+
for n, v := range dcj.auths {
180+
jsonData.auths[n] = v
181+
}
182+
continue
183+
}
184+
if secretData, ok := s.Data[".dockercfg"]; ok {
185+
// process as dockercfg, despite being a map, this has to be Unmarshal'd as a pointer
186+
dc := &dockerCfg{}
187+
if err := json.Unmarshal(secretData, dc); err != nil {
188+
return ctrl.Result{}, err
189+
}
190+
for n, v := range *dc {
191+
jsonData.auths[n] = v
192+
}
193+
continue
194+
}
195+
// Ignore the unknown secret
196+
logger.Info("expected secret.Data key not found", "secret", types.NamespacedName{Name: s.Name, Namespace: s.Namespace})
197+
}
198+
199+
data, err := json.Marshal(jsonData)
200+
if err != nil {
201+
return ctrl.Result{}, fmt.Errorf("failed to marshal secret data: %w", err)
202+
}
203+
err = os.WriteFile(r.AuthFilePath, data, 0600)
204+
if err != nil {
205+
return ctrl.Result{}, fmt.Errorf("failed to write secret data to file: %w", err)
206+
}
207+
logger.Info("saved global pull secret data locally")
208+
return ctrl.Result{}, nil
209+
}
210+
211+
// deleteSecretFile deletes the auth file if the secret is deleted
212+
func (r *PullSecretReconciler) deleteSecretFile(logger logr.Logger) (ctrl.Result, error) {
213+
logger.Info("deleting local auth file", "file", r.AuthFilePath)
214+
if err := os.Remove(r.AuthFilePath); err != nil {
215+
if os.IsNotExist(err) {
216+
logger.Info("auth file does not exist, nothing to delete")
217+
return ctrl.Result{}, nil
218+
}
219+
return ctrl.Result{}, fmt.Errorf("failed to delete secret file: %w", err)
220+
}
221+
logger.Info("auth file deleted successfully")
222+
return ctrl.Result{}, nil
223+
}

‎internal/catalogd/controllers/core/pull_secret_controller_test.go renamed to ‎internal/shared/controllers/pull_secret_controller_test.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package core
1+
package controllers
22

33
import (
44
"context"
@@ -15,7 +15,8 @@ import (
1515
)
1616

1717
func TestSecretSyncerReconciler(t *testing.T) {
18-
secretData := []byte(`{"auths":{"exampleRegistry": "exampledata"}}`)
18+
secretFullData := []byte(`{"auths":{"exampleRegistry": {"auth": "exampledata"}}}`)
19+
secretPartData := []byte(`{"exampleRegistry": {"auth": "exampledata"}}`)
1920
authFileName := "test-auth.json"
2021
for _, tt := range []struct {
2122
name string
@@ -26,14 +27,29 @@ func TestSecretSyncerReconciler(t *testing.T) {
2627
fileShouldExistAfter bool
2728
}{
2829
{
29-
name: "secret exists, content gets saved to authFile",
30+
name: "secret exists, dockerconfigjson content gets saved to authFile",
3031
secret: &corev1.Secret{
3132
ObjectMeta: metav1.ObjectMeta{
3233
Name: "test-secret",
3334
Namespace: "test-secret-namespace",
3435
},
3536
Data: map[string][]byte{
36-
".dockerconfigjson": secretData,
37+
".dockerconfigjson": secretFullData,
38+
},
39+
},
40+
addSecret: true,
41+
fileShouldExistBefore: false,
42+
fileShouldExistAfter: true,
43+
},
44+
{
45+
name: "secret exists, dockercfg content gets saved to authFile",
46+
secret: &corev1.Secret{
47+
ObjectMeta: metav1.ObjectMeta{
48+
Name: "test-secret",
49+
Namespace: "test-secret-namespace",
50+
},
51+
Data: map[string][]byte{
52+
".dockercfg": secretPartData,
3753
},
3854
},
3955
addSecret: true,
@@ -48,7 +64,7 @@ func TestSecretSyncerReconciler(t *testing.T) {
4864
Namespace: "test-secret-namespace",
4965
},
5066
Data: map[string][]byte{
51-
".dockerconfigjson": secretData,
67+
".dockerconfigjson": secretFullData,
5268
},
5369
},
5470
addSecret: false,
@@ -68,11 +84,11 @@ func TestSecretSyncerReconciler(t *testing.T) {
6884
secretKey := types.NamespacedName{Namespace: tt.secret.Namespace, Name: tt.secret.Name}
6985
r := &PullSecretReconciler{
7086
Client: cl,
71-
SecretKey: secretKey,
87+
SecretKey: &secretKey,
7288
AuthFilePath: tempAuthFile,
7389
}
7490
if tt.fileShouldExistBefore {
75-
err := os.WriteFile(tempAuthFile, secretData, 0600)
91+
err := os.WriteFile(tempAuthFile, secretFullData, 0600)
7692
require.NoError(t, err)
7793
}
7894
res, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: secretKey})
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
Copyright 2025.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package sa
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"strings"
23+
24+
"github.com/golang-jwt/jwt/v5"
25+
k8stypes "k8s.io/apimachinery/pkg/types"
26+
)
27+
28+
// Returns nameaspce/serviceaccount name
29+
func GetServiceAccount() (k8stypes.NamespacedName, error) {
30+
return getServiceAccountInternal(os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token"))
31+
}
32+
33+
func getServiceAccountInternal(data []byte, err error) (k8stypes.NamespacedName, error) {
34+
if err != nil {
35+
return k8stypes.NamespacedName{}, err
36+
}
37+
// Not verifying the token, we just want to extract the subject
38+
token, _, err := jwt.NewParser([]jwt.ParserOption{}...).ParseUnverified(string(data), jwt.MapClaims{})
39+
if err != nil {
40+
return k8stypes.NamespacedName{}, err
41+
}
42+
subject, err := token.Claims.GetSubject()
43+
if err != nil {
44+
return k8stypes.NamespacedName{}, err
45+
}
46+
subjects := strings.Split(subject, ":")
47+
if len(subjects) != 4 {
48+
return k8stypes.NamespacedName{}, fmt.Errorf("badly formatted subject: %s", subject)
49+
}
50+
return k8stypes.NamespacedName{Namespace: subjects[2], Name: subjects[3]}, nil
51+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
Copyright 2025.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package sa
18+
19+
import (
20+
"fmt"
21+
"testing"
22+
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
const (
27+
// taken from a kind run
28+
goodSa = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdyM3VIbkJ0VlRQVy1uWWlsSVFCV2pfQmdTS0RIdjZHNDBVT1hDSVFtZmcifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzgwNTEwMjAwLCJpYXQiOjE3NDg5NzQyMDAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNTQ2OThmZGYtNzg4NC00YzhkLWI5NzctYTg4YThiYmY3ODQxIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJvbG12MS1zeXN0ZW0iLCJub2RlIjp7Im5hbWUiOiJvcGVyYXRvci1jb250cm9sbGVyLWUyZS1jb250cm9sLXBsYW5lIiwidWlkIjoiZWY0YjdkNGQtZmUxZi00MThkLWIyZDAtM2ZmYWJmMWQ0ZDI3In0sInBvZCI6eyJuYW1lIjoib3BlcmF0b3ItY29udHJvbGxlci1jb250cm9sbGVyLW1hbmFnZXItNjU3Njg1ZGNkYy01cTZ0dCIsInVpZCI6IjE4MmFkNTkxLWUzYTktNDMyNC1hMjk4LTg0NzIxY2Q0OTAzYSJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoib3BlcmF0b3ItY29udHJvbGxlci1jb250cm9sbGVyLW1hbmFnZXIiLCJ1aWQiOiI3MDliZTA4OS00OTI1LTQ2NjYtYjA1Ny1iYWMyNmVmYWJjMGIifSwid2FybmFmdGVyIjoxNzQ4OTc3ODA3fSwibmJmIjoxNzQ4OTc0MjAwLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6b2xtdjEtc3lzdGVtOm9wZXJhdG9yLWNvbnRyb2xsZXItY29udHJvbGxlci1tYW5hZ2VyIn0.OjExhuNHdMZjdGwDXM0bWQnJKcfLNpEJ2S47BzlAa560uNw8EwMItlfpG970umQBbVPWhyhUBFimUD5XmXWAlrNvhFwpOLXw2W978Obs1mna5JWcHliC6IkwrOMCh5k9XReQ9-KBdw36QY1G2om77-7mNtPNPg9lg5TQaLuNGrIhX9EC_tucbflXSvB-SA243J_X004W4HkJirt6vVH5FoRg-MDohXm0C4bhTeaXfOtTW6fwsnpomCKso7apu_eOG9E2h8CXXYKhZg4Jrank_Ata8J1lANh06FuxRQK-vwqFrW3_9rscGxweM5CbeicZFOc6MDIuYtgR515YTHPbUA"
29+
badSa1 = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdyM3VIbkJ0VlRQVy1uWWlsSVFCV2pfQmdTS0RIdjZHNDBVT1hDSVFtZmcifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzgwNTEwMjAwLCJpYXQiOjE3NDg5NzQyMDAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNTQ2OThmZGYtNzg4NC00YzhkLWI5NzctYTg4YThiYmY3ODQxIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJvbG12MS1zeXN0ZW0iLCJub2RlIjp7Im5hbWUiOiJvcGVyYXRvci1jb250cm9sbGVyLWUyZS1jb250cm9sLXBsYW5lIiwidWlkIjoiZWY0YjdkNGQtZmUxZi00MThkLWIyZDAtM2ZmYWJmMWQ0ZDI3In0sInBvZCI6eyJuYW1lIjoib3BlcmF0b3ItY29udHJvbGxlci1jb250cm9sbGVyLW1hbmFnZXItNjU3Njg1ZGNkYy01cTZ0dCIsInVpZCI6IjE4MmFkNTkxLWUzYTktNDMyNC1hMjk4LTg0NzIxY2Q0OTAzYSJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoib3BlcmF0b3ItY29udHJvbGxlci1jb250cm9sbGVyLW1hbmFnZXIiLCJ1aWQiOiI3MDliZTA4OS00OTI1LTQ2NjYtYjA1Ny1iYWMyNmVmYWJjMGIifSwid2FybmFmdGVyIjoxNzQ4OTc3ODA3fSwibmJmIjoxNzQ4OTc0MjAwLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnRzOm9sbXYxLXN5c3RlbSJ9.OjExhuNHdMZjdGwDXM0bWQnJKcfLNpEJ2S47BzlAa560uNw8EwMItlfpG970umQBbVPWhyhUBFimUD5XmXWAlrNvhFwpOLXw2W978Obs1mna5JWcHliC6IkwrOMCh5k9XReQ9-KBdw36QY1G2om77-7mNtPNPg9lg5TQaLuNGrIhX9EC_tucbflXSvB-SA243J_X004W4HkJirt6vVH5FoRg-MDohXm0C4bhTeaXfOtTW6fwsnpomCKso7apu_eOG9E2h8CXXYKhZg4Jrank_Ata8J1lANh06FuxRQK-vwqFrW3_9rscGxweM5CbeicZFOc6MDIuYtgR515YTHPbUA"
30+
badSa2 = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdyM3VIbkJ0VlRQVy1uWWlsSVFCV2pfQmdTS0RIdjZHNDBVT1hDSVFtZmcifQ"
31+
)
32+
33+
func TestGetServiceAccount(t *testing.T) {
34+
nn, err := getServiceAccountInternal([]byte(goodSa), nil)
35+
require.NoError(t, err)
36+
require.Equal(t, "olmv1-system", nn.Namespace)
37+
require.Equal(t, "operator-controller-controller-manager", nn.Name)
38+
39+
_, err = getServiceAccountInternal([]byte{}, fmt.Errorf("this is a test error"))
40+
require.ErrorContains(t, err, "this is a test")
41+
42+
// Modified the subject to be invalid
43+
_, err = getServiceAccountInternal([]byte(badSa1), nil)
44+
require.ErrorContains(t, err, "badly formatted subject")
45+
46+
// Only includes a header
47+
_, err = getServiceAccountInternal([]byte(badSa2), nil)
48+
require.ErrorContains(t, err, "token is malformed")
49+
}

0 commit comments

Comments
 (0)
Please sign in to comment.