Skip to content

Commit 857a904

Browse files
committed
Use SA from spec
1 parent bfd4142 commit 857a904

17 files changed

+526
-80
lines changed

Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ build-push-e2e-catalog: ## Build the testdata catalog used for e2e tests and pus
152152
test-e2e: KIND_CLUSTER_NAME := operator-controller-e2e
153153
test-e2e: KUSTOMIZE_BUILD_DIR := config/overlays/e2e
154154
test-e2e: GO_BUILD_FLAGS := -cover
155-
test-e2e: run image-registry build-push-e2e-catalog registry-load-bundles e2e e2e-coverage kind-clean #HELP Run e2e test suite on local kind cluster
155+
test-e2e: run image-registry build-push-e2e-catalog registry-load-bundles apply-rbac e2e e2e-coverage kind-clean #HELP Run e2e test suite on local kind cluster
156156

157157
.PHONY: extension-developer-e2e
158158
extension-developer-e2e: KIND_CLUSTER_NAME := operator-controller-ext-dev-e2e #EXHELP Run extension-developer e2e on local kind cluster
@@ -193,6 +193,9 @@ registry-load-bundles: ## Load selected e2e testdata container images created in
193193
testdata/bundles/registry-v1/build-push-e2e-bundle.sh ${E2E_REGISTRY_NAMESPACE} $(REGISTRY_ROOT)/bundles/registry-v1/prometheus-operator:v1.2.0 prometheus-operator.v1.2.0 prometheus-operator.v1.0.0
194194
testdata/bundles/registry-v1/build-push-e2e-bundle.sh ${E2E_REGISTRY_NAMESPACE} $(REGISTRY_ROOT)/bundles/registry-v1/prometheus-operator:v2.0.0 prometheus-operator.v2.0.0 prometheus-operator.v1.0.0
195195

196+
apply-rbac: ## Apply RBAC expected when using service account from spec
197+
kubectl apply -f testdata/rbac/prometheus-operator-bundle-rbac.yaml -n default
198+
196199
#SECTION Build
197200

198201
ifeq ($(origin VERSION), undefined)
@@ -238,7 +241,7 @@ run: docker-build kind-cluster kind-load kind-deploy #HELP Build the operator-co
238241

239242
.PHONY: docker-build
240243
docker-build: build-linux #EXHELP Build docker image for operator-controller with GOOS=linux and local GOARCH.
241-
$(CONTAINER_RUNTIME) build -t $(IMG) -f Dockerfile ./bin/linux
244+
$(CONTAINER_RUNTIME) build -t $(IMG) -f Dockerfile ./bin/linux --load
242245

243246
#SECTION Release
244247
ifeq ($(origin ENABLE_RELEASE_PIPELINE), undefined)

api/v1alpha1/clusterextension_types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ type ClusterExtensionSpec struct {
7878
// the bundle may contain resources that are cluster-scoped or that are
7979
// installed in a different namespace. This namespace is expected to exist.
8080
InstallNamespace string `json:"installNamespace"`
81+
82+
//+kubebuilder:validation:Pattern:=^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
83+
//+kubebuilder:validation:MaxLength:=253
84+
// ServiceAccountName is the name of the ServiceAccount to use to manage the resources in the bundle.
85+
// The service account is expected to exist in the InstallNamespace.
86+
ServiceAccountName string `json:"serviceAccountName"`
8187
}
8288

8389
const (

cmd/manager/main.go

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ import (
2828
"go.uber.org/zap/zapcore"
2929
k8slabels "k8s.io/apimachinery/pkg/labels"
3030
"k8s.io/apimachinery/pkg/selection"
31+
"k8s.io/apimachinery/pkg/types"
32+
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
3133
_ "k8s.io/client-go/plugin/pkg/client/auth"
34+
"k8s.io/client-go/rest"
3235
ctrl "sigs.k8s.io/controller-runtime"
3336
crcache "sigs.k8s.io/controller-runtime/pkg/cache"
3437
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -44,6 +47,7 @@ import (
4447
"github.com/operator-framework/rukpak/pkg/storage"
4548

4649
ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1"
50+
"github.com/operator-framework/operator-controller/internal/authentication"
4751
"github.com/operator-framework/operator-controller/internal/catalogmetadata/cache"
4852
catalogclient "github.com/operator-framework/operator-controller/internal/catalogmetadata/client"
4953
"github.com/operator-framework/operator-controller/internal/controllers"
@@ -159,19 +163,44 @@ func main() {
159163
cl := mgr.GetClient()
160164
catalogClient := catalogclient.New(cl, cache.NewFilesystemCache(cachePath, httpClient))
161165

162-
installNamespaceMapper := helmclient.ObjectToStringMapper(func(obj client.Object) (string, error) {
163-
ext := obj.(*ocv1alpha1.ClusterExtension)
166+
saGetter, err := corev1client.NewForConfig(ctrl.GetConfigOrDie())
167+
if err != nil {
168+
setupLog.Error(err, "unable to create service account client")
169+
os.Exit(1)
170+
}
171+
172+
tg := authentication.NewTokenGetter(saGetter, 3600)
173+
nsMapper := func(obj client.Object) (string, error) {
174+
ext, ok := obj.(*ocv1alpha1.ClusterExtension)
175+
if !ok {
176+
return "", fmt.Errorf("cannot derive namespace from object of type %T", obj)
177+
}
164178
return ext.Spec.InstallNamespace, nil
165-
})
179+
}
180+
181+
rcm := func(ctx context.Context, obj client.Object, baseRestConfig *rest.Config) (*rest.Config, error) {
182+
cfg := rest.AnonymousClientConfig(rest.CopyConfig(baseRestConfig))
183+
ext, ok := obj.(*ocv1alpha1.ClusterExtension)
184+
if !ok {
185+
return cfg, nil
186+
}
187+
token, err := tg.Get(ctx, types.NamespacedName{Namespace: ext.Spec.InstallNamespace, Name: ext.Spec.ServiceAccountName})
188+
if err != nil {
189+
return nil, err
190+
}
191+
cfg.BearerToken = token
192+
return cfg, nil
193+
}
194+
166195
cfgGetter, err := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(),
167-
helmclient.StorageNamespaceMapper(installNamespaceMapper),
168-
helmclient.ClientNamespaceMapper(installNamespaceMapper),
196+
helmclient.ClientNamespaceMapper(nsMapper),
197+
helmclient.StorageNamespaceMapper(nsMapper),
198+
helmclient.RestConfigMapper(rcm),
169199
)
170200
if err != nil {
171201
setupLog.Error(err, "unable to config for creating helm client")
172202
os.Exit(1)
173203
}
174-
175204
acg, err := helmclient.NewActionClientGetter(cfgGetter)
176205
if err != nil {
177206
setupLog.Error(err, "unable to create helm client")

config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ spec:
5656
maxLength: 48
5757
pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
5858
type: string
59+
serviceAccountName:
60+
description: |-
61+
ServiceAccountName is the name of the ServiceAccount to use to manage the resources in the bundle.
62+
The service account is expected to exist in the InstallNamespace.
63+
maxLength: 253
64+
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
65+
type: string
5966
upgradeConstraintPolicy:
6067
default: Enforce
6168
description: Defines the policy for how to handle upgrade constraints
@@ -77,6 +84,7 @@ spec:
7784
required:
7885
- installNamespace
7986
- packageName
87+
- serviceAccountName
8088
type: object
8189
status:
8290
description: ClusterExtensionStatus defines the observed state of ClusterExtension

config/base/rbac/role.yaml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ rules:
99
resources:
1010
- '*'
1111
verbs:
12-
- '*'
12+
- list
13+
- watch
1314
- apiGroups:
1415
- catalogd.operatorframework.io
1516
resources:
@@ -27,15 +28,16 @@ rules:
2728
- apiGroups:
2829
- ""
2930
resources:
30-
- secrets
31+
- configmaps
3132
verbs:
32-
- create
33-
- delete
34-
- get
3533
- list
36-
- patch
37-
- update
3834
- watch
35+
- apiGroups:
36+
- ""
37+
resources:
38+
- serviceaccounts/token
39+
verbs:
40+
- create
3941
- apiGroups:
4042
- olm.operatorframework.io
4143
resources:

config/samples/olm_v1alpha1_clusterextension.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ metadata:
44
name: clusterextension-sample
55
spec:
66
installNamespace: default
7+
serviceAccountName: default
78
packageName: argocd-operator
89
version: 0.6.0
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package authentication
2+
3+
import (
4+
"context"
5+
"sync"
6+
"time"
7+
8+
authenticationv1 "k8s.io/api/authentication/v1"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/types"
11+
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
12+
"k8s.io/utils/ptr"
13+
)
14+
15+
type TokenGetter struct {
16+
client corev1client.ServiceAccountsGetter
17+
expirationSeconds int64
18+
tokens map[types.NamespacedName]*authenticationv1.TokenRequestStatus
19+
tokenLocks keyLock[types.NamespacedName]
20+
mu sync.RWMutex
21+
}
22+
23+
func NewTokenGetter(client corev1client.ServiceAccountsGetter, expirationSeconds int64) *TokenGetter {
24+
return &TokenGetter{
25+
client: client,
26+
expirationSeconds: expirationSeconds,
27+
tokenLocks: newKeyLock[types.NamespacedName](),
28+
tokens: map[types.NamespacedName]*authenticationv1.TokenRequestStatus{},
29+
}
30+
}
31+
32+
type keyLock[K comparable] struct {
33+
locks map[K]*sync.Mutex
34+
mu sync.Mutex
35+
}
36+
37+
func newKeyLock[K comparable]() keyLock[K] {
38+
return keyLock[K]{locks: map[K]*sync.Mutex{}}
39+
}
40+
41+
func (k *keyLock[K]) Lock(key K) {
42+
k.getLock(key).Lock()
43+
}
44+
45+
func (k *keyLock[K]) Unlock(key K) {
46+
k.getLock(key).Unlock()
47+
}
48+
49+
func (k *keyLock[K]) getLock(key K) *sync.Mutex {
50+
k.mu.Lock()
51+
defer k.mu.Unlock()
52+
53+
lock, ok := k.locks[key]
54+
if !ok {
55+
lock = &sync.Mutex{}
56+
k.locks[key] = lock
57+
}
58+
return lock
59+
}
60+
61+
func (t *TokenGetter) Get(ctx context.Context, key types.NamespacedName) (string, error) {
62+
t.tokenLocks.Lock(key)
63+
defer t.tokenLocks.Unlock(key)
64+
65+
t.mu.RLock()
66+
token, ok := t.tokens[key]
67+
t.mu.RUnlock()
68+
69+
expireTime := time.Time{}
70+
if ok {
71+
expireTime = token.ExpirationTimestamp.Time
72+
}
73+
74+
fiveMinutesAfterNow := metav1.Now().Add(5 * time.Minute)
75+
if expireTime.Before(fiveMinutesAfterNow) {
76+
var err error
77+
token, err = t.getToken(ctx, key)
78+
if err != nil {
79+
return "", err
80+
}
81+
t.mu.Lock()
82+
t.tokens[key] = token
83+
t.mu.Unlock()
84+
}
85+
86+
return token.Token, nil
87+
}
88+
89+
func (t *TokenGetter) getToken(ctx context.Context, key types.NamespacedName) (*authenticationv1.TokenRequestStatus, error) {
90+
req, err := t.client.ServiceAccounts(key.Namespace).CreateToken(ctx, key.Name, &authenticationv1.TokenRequest{Spec: authenticationv1.TokenRequestSpec{
91+
ExpirationSeconds: ptr.To[int64](3600),
92+
}}, metav1.CreateOptions{})
93+
if err != nil {
94+
return nil, err
95+
}
96+
return &req.Status, nil
97+
}

internal/controllers/clusterextension_admission_test.go

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ func TestClusterExtensionAdmissionPackageName(t *testing.T) {
4343
t.Parallel()
4444
cl := newClient(t)
4545
err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{
46-
PackageName: tc.pkgName,
47-
InstallNamespace: "default",
46+
PackageName: tc.pkgName,
47+
InstallNamespace: "default",
48+
ServiceAccountName: "default",
4849
}))
4950
if tc.errMsg == "" {
5051
require.NoError(t, err, "unexpected error for package name %q: %w", tc.pkgName, err)
@@ -131,9 +132,10 @@ func TestClusterExtensionAdmissionVersion(t *testing.T) {
131132
t.Parallel()
132133
cl := newClient(t)
133134
err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{
134-
PackageName: "package",
135-
Version: tc.version,
136-
InstallNamespace: "default",
135+
PackageName: "package",
136+
Version: tc.version,
137+
InstallNamespace: "default",
138+
ServiceAccountName: "default",
137139
}))
138140
if tc.errMsg == "" {
139141
require.NoError(t, err, "unexpected error for version %q: %w", tc.version, err)
@@ -176,9 +178,10 @@ func TestClusterExtensionAdmissionChannel(t *testing.T) {
176178
t.Parallel()
177179
cl := newClient(t)
178180
err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{
179-
PackageName: "package",
180-
Channel: tc.channelName,
181-
InstallNamespace: "default",
181+
PackageName: "package",
182+
Channel: tc.channelName,
183+
InstallNamespace: "default",
184+
ServiceAccountName: "default",
182185
}))
183186
if tc.errMsg == "" {
184187
require.NoError(t, err, "unexpected error for channel %q: %w", tc.channelName, err)
@@ -222,8 +225,9 @@ func TestClusterExtensionAdmissionInstallNamespace(t *testing.T) {
222225
t.Parallel()
223226
cl := newClient(t)
224227
err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{
225-
PackageName: "package",
226-
InstallNamespace: tc.installNamespace,
228+
PackageName: "package",
229+
InstallNamespace: tc.installNamespace,
230+
ServiceAccountName: "default",
227231
}))
228232
if tc.errMsg == "" {
229233
require.NoError(t, err, "unexpected error for installNamespace %q: %w", tc.installNamespace, err)
@@ -235,6 +239,51 @@ func TestClusterExtensionAdmissionInstallNamespace(t *testing.T) {
235239
}
236240
}
237241

242+
func TestClusterExtensionAdmissionServiceAccountName(t *testing.T) {
243+
tooLongError := "spec.serviceAccountName: Too long: may not be longer than 253"
244+
regexMismatchError := "spec.serviceAccountName in body should match"
245+
246+
testCases := []struct {
247+
name string
248+
serviceAccountName string
249+
errMsg string
250+
}{
251+
{"just alphanumeric", "justalphanumberic1", ""},
252+
{"hypen-separated", "hyphenated-name", ""},
253+
{"no service account name", "", regexMismatchError},
254+
{"longest valid service account name", strings.Repeat("x", 253), ""},
255+
{"too long service account name", strings.Repeat("x", 254), tooLongError},
256+
{"spaces", "spaces spaces", regexMismatchError},
257+
{"capitalized", "Capitalized", regexMismatchError},
258+
{"camel case", "camelCase", regexMismatchError},
259+
{"invalid characters", "many/invalid$characters+in_name", regexMismatchError},
260+
{"starts with hyphen", "-start-with-hyphen", regexMismatchError},
261+
{"ends with hyphen", "end-with-hyphen-", regexMismatchError},
262+
{"starts with period", ".start-with-period", regexMismatchError},
263+
{"ends with period", "end-with-period.", regexMismatchError},
264+
}
265+
266+
t.Parallel()
267+
for _, tc := range testCases {
268+
tc := tc
269+
t.Run(tc.name, func(t *testing.T) {
270+
t.Parallel()
271+
cl := newClient(t)
272+
err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{
273+
PackageName: "package",
274+
InstallNamespace: "default",
275+
ServiceAccountName: tc.serviceAccountName,
276+
}))
277+
if tc.errMsg == "" {
278+
require.NoError(t, err, "unexpected error for serviceAccountName %q: %w", tc.serviceAccountName, err)
279+
} else {
280+
require.Error(t, err)
281+
require.Contains(t, err.Error(), tc.errMsg)
282+
}
283+
})
284+
}
285+
}
286+
238287
func buildClusterExtension(spec ocv1alpha1.ClusterExtensionSpec) *ocv1alpha1.ClusterExtension {
239288
return &ocv1alpha1.ClusterExtension{
240289
ObjectMeta: metav1.ObjectMeta{

internal/controllers/clusterextension_controller.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,15 @@ const (
103103
//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions,verbs=get;list;watch
104104
//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions/status,verbs=update;patch
105105
//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions/finalizers,verbs=update
106-
//+kubebuilder:rbac:groups=core,resources=secrets,verbs=create;update;patch;delete;get;list;watch
107-
//+kubebuilder:rbac:groups=*,resources=*,verbs=*
106+
107+
//+kubebuilder:rbac:groups=core,resources=configmaps,verbs=list;watch
108+
//+kubebuilder:rbac:groups=core,resources=serviceaccounts/token,verbs=create
108109

109110
//+kubebuilder:rbac:groups=catalogd.operatorframework.io,resources=clustercatalogs,verbs=list;watch
110111
//+kubebuilder:rbac:groups=catalogd.operatorframework.io,resources=catalogmetadata,verbs=list;watch
111112

113+
//+kubebuilder:rbac:groups=*,resources=*,verbs=list;watch
114+
112115
// The operator controller needs to watch all the bundle objects and reconcile accordingly. Though not ideal, but these permissions are required.
113116
// This has been taken from rukpak, and an issue was created before to discuss it: https://github.com/operator-framework/rukpak/issues/800.
114117
func (r *ClusterExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {

0 commit comments

Comments
 (0)