Skip to content

Commit

Permalink
Use SA from spec
Browse files Browse the repository at this point in the history
  • Loading branch information
skattoju committed Jun 15, 2024
1 parent 40e1bac commit ced5993
Show file tree
Hide file tree
Showing 16 changed files with 439 additions and 73 deletions.
8 changes: 5 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,18 @@ kind-clean: $(KIND) #EXHELP Delete the kind cluster.

.PHONY: kind-load-test-artifacts
kind-load-test-artifacts: $(KIND) #EXHELP Load the e2e testdata container images into a kind cluster.
$(CONTAINER_RUNTIME) build testdata/bundles/registry-v1/prometheus-operator.v1.0.0 -t localhost/testdata/bundles/registry-v1/prometheus-operator:v1.0.0
$(CONTAINER_RUNTIME) build testdata/bundles/registry-v1/prometheus-operator.v1.0.0 -t localhost/testdata/bundles/registry-v1/prometheus-operator:v1.0.0 --load
$(CONTAINER_RUNTIME) tag localhost/testdata/bundles/registry-v1/prometheus-operator:v1.0.0 localhost/testdata/bundles/registry-v1/prometheus-operator:v1.0.1
$(CONTAINER_RUNTIME) tag localhost/testdata/bundles/registry-v1/prometheus-operator:v1.0.0 localhost/testdata/bundles/registry-v1/prometheus-operator:v1.2.0
$(CONTAINER_RUNTIME) tag localhost/testdata/bundles/registry-v1/prometheus-operator:v1.0.0 localhost/testdata/bundles/registry-v1/prometheus-operator:v2.0.0
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v1.0.0 --name $(KIND_CLUSTER_NAME)
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v1.0.1 --name $(KIND_CLUSTER_NAME)
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v1.2.0 --name $(KIND_CLUSTER_NAME)
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v2.0.0 --name $(KIND_CLUSTER_NAME)
$(CONTAINER_RUNTIME) build testdata/bundles/registry-v1/package-with-webhooks.v1.0.0 -t localhost/testdata/bundles/registry-v1/package-with-webhooks:v1.0.0
$(CONTAINER_RUNTIME) build testdata/bundles/registry-v1/package-with-webhooks.v1.0.0 -t localhost/testdata/bundles/registry-v1/package-with-webhooks:v1.0.0 --load
$(KIND) load docker-image localhost/testdata/bundles/registry-v1/package-with-webhooks:v1.0.0 --name $(KIND_CLUSTER_NAME)
kubectl apply -f testdata/rbac/prometheus-operator-bundle-rbac.yaml -n default
kubectl apply -f testdata/rbac/package-with-webhooks-rbac.yaml -n default


#SECTION Build
Expand Down Expand Up @@ -245,7 +247,7 @@ run: docker-build kind-cluster kind-load kind-deploy #HELP Build the operator-co

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

#SECTION Release
ifeq ($(origin ENABLE_RELEASE_PIPELINE), undefined)
Expand Down
6 changes: 6 additions & 0 deletions api/v1alpha1/clusterextension_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ type ClusterExtensionSpec struct {
// the bundle may contain resources that are cluster-scoped or that are
// installed in a different namespace. This namespace is expected to exist.
InstallNamespace string `json:"installNamespace"`

//+kubebuilder:validation:Pattern:=^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
//+kubebuilder:validation:MaxLength:=253
// ServiceAccountName is the name of the ServiceAccount to use to manage the resources in the bundle.
// The service account is expected to exist in the InstallNamespace.
ServiceAccountName string `json:"serviceAccountName"`
}

const (
Expand Down
43 changes: 39 additions & 4 deletions cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package main

import (
"context"
"crypto/x509"
"flag"
"fmt"
Expand All @@ -29,7 +30,10 @@ import (
"go.uber.org/zap/zapcore"
k8slabels "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
crcache "sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -42,6 +46,7 @@ import (
"github.com/operator-framework/rukpak/pkg/storage"

"github.com/operator-framework/operator-controller/api/v1alpha1"
"github.com/operator-framework/operator-controller/internal/authentication"
"github.com/operator-framework/operator-controller/internal/catalogmetadata/cache"
catalogclient "github.com/operator-framework/operator-controller/internal/catalogmetadata/client"
"github.com/operator-framework/operator-controller/internal/controllers"
Expand Down Expand Up @@ -155,14 +160,44 @@ func main() {
cl := mgr.GetClient()
catalogClient := catalogclient.New(cl, cache.NewFilesystemCache(cachePath, &http.Client{Timeout: 10 * time.Second}))

cfgGetter, err := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(), helmclient.StorageNamespaceMapper(func(o client.Object) (string, error) {
return systemNamespace, nil
}))
saGetter, err := corev1client.NewForConfig(ctrl.GetConfigOrDie())
if err != nil {
setupLog.Error(err, "unable to config for creating helm client")
setupLog.Error(err, "unable to create service account client")
os.Exit(1)
}

tg := authentication.NewTokenGetter(saGetter, 3600)
nsMapper := func(obj client.Object) (string, error) {
bd, ok := obj.(*v1alpha1.ClusterExtension)
if !ok {
return "", fmt.Errorf("cannot derive namespace from object of type %T", obj)
}
return bd.Spec.InstallNamespace, nil
}

rcm := func(ctx context.Context, obj client.Object, baseRestConfig *rest.Config) (*rest.Config, error) {
cfg := rest.AnonymousClientConfig(rest.CopyConfig(baseRestConfig))
ext, ok := obj.(*v1alpha1.ClusterExtension)
if !ok {
return cfg, nil
}
token, err := tg.Get(ctx, types.NamespacedName{Namespace: ext.Spec.InstallNamespace, Name: ext.Spec.ServiceAccountName})
if err != nil {
return nil, err
}
cfg.BearerToken = token
return cfg, nil
}

cfgGetter, err := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(),
helmclient.ClientNamespaceMapper(nsMapper),
helmclient.StorageNamespaceMapper(nsMapper),
helmclient.RestConfigMapper(rcm),
)
if err != nil {
setupLog.Error(err, "unable to config for creating helm client")
os.Exit(1)
}
acg, err := helmclient.NewActionClientGetter(cfgGetter)
if err != nil {
setupLog.Error(err, "unable to create helm client")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ spec:
maxLength: 48
pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
type: string
serviceAccountName:
description: |-
ServiceAccountName is the name of the ServiceAccount to use to manage the resources in the bundle.
The service account is expected to exist in the InstallNamespace.
maxLength: 253
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
upgradeConstraintPolicy:
default: Enforce
description: Defines the policy for how to handle upgrade constraints
Expand All @@ -77,6 +84,7 @@ spec:
required:
- installNamespace
- packageName
- serviceAccountName
type: object
status:
description: ClusterExtensionStatus defines the observed state of ClusterExtension
Expand Down
1 change: 1 addition & 0 deletions config/samples/olm_v1alpha1_clusterextension.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ metadata:
name: clusterextension-sample
spec:
installNamespace: default
serviceAccountName: default
packageName: argocd-operator
version: 0.6.0
97 changes: 97 additions & 0 deletions internal/authentication/tokengetter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package authentication

import (
"context"
"sync"
"time"

authenticationv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/utils/ptr"
)

type TokenGetter struct {
client corev1client.ServiceAccountsGetter
expirationSeconds int64
tokens map[types.NamespacedName]*authenticationv1.TokenRequestStatus
tokenLocks keyLock[types.NamespacedName]
mu sync.RWMutex
}

func NewTokenGetter(client corev1client.ServiceAccountsGetter, expirationSeconds int64) *TokenGetter {
return &TokenGetter{
client: client,
expirationSeconds: expirationSeconds,
tokenLocks: newKeyLock[types.NamespacedName](),
tokens: map[types.NamespacedName]*authenticationv1.TokenRequestStatus{},
}
}

type keyLock[K comparable] struct {
locks map[K]*sync.Mutex
mu sync.Mutex
}

func newKeyLock[K comparable]() keyLock[K] {
return keyLock[K]{locks: map[K]*sync.Mutex{}}
}

func (k *keyLock[K]) Lock(key K) {
k.getLock(key).Lock()
}

func (k *keyLock[K]) Unlock(key K) {
k.getLock(key).Unlock()
}

func (k *keyLock[K]) getLock(key K) *sync.Mutex {
k.mu.Lock()
defer k.mu.Unlock()

lock, ok := k.locks[key]
if !ok {
lock = &sync.Mutex{}
k.locks[key] = lock
}
return lock
}

func (t *TokenGetter) Get(ctx context.Context, key types.NamespacedName) (string, error) {
t.tokenLocks.Lock(key)
defer t.tokenLocks.Unlock(key)

t.mu.RLock()
token, ok := t.tokens[key]
t.mu.RUnlock()

expireTime := time.Time{}
if ok {
expireTime = token.ExpirationTimestamp.Time
}

fiveMinutesAfterNow := metav1.Now().Add(5 * time.Minute)
if expireTime.Before(fiveMinutesAfterNow) {
var err error
token, err = t.getToken(ctx, key)
if err != nil {
return "", err
}
t.mu.Lock()
t.tokens[key] = token
t.mu.Unlock()
}

return token.Token, nil
}

func (t *TokenGetter) getToken(ctx context.Context, key types.NamespacedName) (*authenticationv1.TokenRequestStatus, error) {
req, err := t.client.ServiceAccounts(key.Namespace).CreateToken(ctx, key.Name, &authenticationv1.TokenRequest{Spec: authenticationv1.TokenRequestSpec{
ExpirationSeconds: ptr.To[int64](3600),
}}, metav1.CreateOptions{})
if err != nil {
return nil, err
}
return &req.Status, nil
}
69 changes: 59 additions & 10 deletions internal/controllers/clusterextension_admission_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ func TestClusterExtensionAdmissionPackageName(t *testing.T) {
t.Parallel()
cl := newClient(t)
err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{
PackageName: tc.pkgName,
InstallNamespace: "default",
PackageName: tc.pkgName,
InstallNamespace: "default",
ServiceAccountName: "default",
}))
if tc.errMsg == "" {
require.NoError(t, err, "unexpected error for package name %q: %w", tc.pkgName, err)
Expand Down Expand Up @@ -131,9 +132,10 @@ func TestClusterExtensionAdmissionVersion(t *testing.T) {
t.Parallel()
cl := newClient(t)
err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{
PackageName: "package",
Version: tc.version,
InstallNamespace: "default",
PackageName: "package",
Version: tc.version,
InstallNamespace: "default",
ServiceAccountName: "default",
}))
if tc.errMsg == "" {
require.NoError(t, err, "unexpected error for version %q: %w", tc.version, err)
Expand Down Expand Up @@ -176,9 +178,10 @@ func TestClusterExtensionAdmissionChannel(t *testing.T) {
t.Parallel()
cl := newClient(t)
err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{
PackageName: "package",
Channel: tc.channelName,
InstallNamespace: "default",
PackageName: "package",
Channel: tc.channelName,
InstallNamespace: "default",
ServiceAccountName: "default",
}))
if tc.errMsg == "" {
require.NoError(t, err, "unexpected error for channel %q: %w", tc.channelName, err)
Expand Down Expand Up @@ -222,8 +225,9 @@ func TestClusterExtensionAdmissionInstallNamespace(t *testing.T) {
t.Parallel()
cl := newClient(t)
err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{
PackageName: "package",
InstallNamespace: tc.installNamespace,
PackageName: "package",
InstallNamespace: tc.installNamespace,
ServiceAccountName: "default",
}))
if tc.errMsg == "" {
require.NoError(t, err, "unexpected error for installNamespace %q: %w", tc.installNamespace, err)
Expand All @@ -235,6 +239,51 @@ func TestClusterExtensionAdmissionInstallNamespace(t *testing.T) {
}
}

func TestClusterExtensionAdmissionServiceAccountName(t *testing.T) {
tooLongError := "spec.serviceAccountName: Too long: may not be longer than 253"
regexMismatchError := "spec.serviceAccountName in body should match"

testCases := []struct {
name string
serviceAccountName string
errMsg string
}{
{"just alphanumeric", "justalphanumberic1", ""},
{"hypen-separated", "hyphenated-name", ""},
{"no service account name", "", regexMismatchError},
{"longest valid service account name", strings.Repeat("x", 253), ""},
{"too long service account name", strings.Repeat("x", 254), tooLongError},
{"spaces", "spaces spaces", regexMismatchError},
{"capitalized", "Capitalized", regexMismatchError},
{"camel case", "camelCase", regexMismatchError},
{"invalid characters", "many/invalid$characters+in_name", regexMismatchError},
{"starts with hyphen", "-start-with-hyphen", regexMismatchError},
{"ends with hyphen", "end-with-hyphen-", regexMismatchError},
{"starts with period", ".start-with-period", regexMismatchError},
{"ends with period", "end-with-period.", regexMismatchError},
}

t.Parallel()
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
cl := newClient(t)
err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{
PackageName: "package",
InstallNamespace: "default",
ServiceAccountName: tc.serviceAccountName,
}))
if tc.errMsg == "" {
require.NoError(t, err, "unexpected error for serviceAccountName %q: %w", tc.serviceAccountName, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tc.errMsg)
}
})
}
}

func buildClusterExtension(spec ocv1alpha1.ClusterExtensionSpec) *ocv1alpha1.ClusterExtension {
return &ocv1alpha1.ClusterExtension{
ObjectMeta: metav1.ObjectMeta{
Expand Down
Loading

0 comments on commit ced5993

Please sign in to comment.