Skip to content

Commit 5184909

Browse files
authored
feat: add ServiceAccountRef support for ClusterAccess authentication (#129)
* feat: add ServiceAccountRef support for ClusterAccess authentication - Add extractServiceAccountAuth to metadata injector for storing SA reference info - Implement ServiceAccountRoundTripper for dynamic token generation via TokenRequest API - Extend AuthMetadata struct with service account fields - Initialize local k8s client in ClusterRegistry for token generation - Make Name and Namespace required fields in ServiceAccountRef Signed-off-by: Bastian Echterhölter <bastian.echterhoelter@sap.com> On-behalf-of: @SAP <bastian.echterhoelter@sap.com> * fix: add workaround for setup-envtest empty directory bug setup-envtest doesn't verify binaries exist if the directory is present. Check if etcd binary exists before deciding whether to force re-download. Signed-off-by: Bastian Echterhölter <bastian.echterhoelter@sap.com> On-behalf-of: @SAP <bastian.echterhoelter@sap.com> * fix: address PR review comments - Use Status.ExpirationTimestamp from TokenRequest API instead of requested TTL (API server may override the requested expiration) - Check both etcd and kube-apiserver executability in Taskfile envtest workaround Signed-off-by: Bastian Echterhölter <bastian.echterhoelter@sap.com> On-behalf-of: @SAP <bastian.echterhoelter@sap.com> * fix: make default ServiceAccount token expiration configurable Add listener-default-sa-expiration-seconds config option to allow customizing the default token expiration for ServiceAccount-based authentication when not explicitly set in the ClusterAccess spec. Signed-off-by: Bastian Echterhölter <bastian.echterhoelter@sap.com> On-behalf-of: @SAP <bastian.echterhoelter@sap.com> * feat: add docker:kind task to build and load images into kind cluster Add a new Taskfile task that: - Looks up the current image tag from the running deployment - Builds the container image with that tag - Loads it into the kind cluster - Restarts the deployment to pick up the new image Supports both docker and podman via CONTAINER_RUNTIME variable. Signed-off-by: Bastian Echterhölter <bastian.echterhoelter@sap.com> On-behalf-of: @SAP <bastian.echterhoelter@sap.com> * docs: update local_test.md with docker:kind task Replace manual build/tag/load steps with the new docker:kind task that automates the entire workflow. Signed-off-by: Bastian Echterhölter <bastian.echterhoelter@sap.com> On-behalf-of: @SAP <bastian.echterhoelter@sap.com> --------- Signed-off-by: Bastian Echterhölter <bastian.echterhoelter@sap.com>
1 parent c03d1f2 commit 5184909

File tree

17 files changed

+886
-90
lines changed

17 files changed

+886
-90
lines changed

Taskfile.yml

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,30 @@ tasks:
4646
docker:
4747
cmds:
4848
- docker build -t ghcr.io/platform-mesh/kubernetes-graphql-gateway .
49+
50+
docker:kind:
51+
desc: "Build container image with current tag from kind cluster and load it"
52+
vars:
53+
CONTAINER_RUNTIME: '{{.CONTAINER_RUNTIME | default "docker"}}'
54+
KIND_CLUSTER: '{{.KIND_CLUSTER | default "platform-mesh"}}'
55+
DEPLOYMENT_NAME: '{{.DEPLOYMENT_NAME | default "kubernetes-graphql-gateway"}}'
56+
DEPLOYMENT_NAMESPACE: '{{.DEPLOYMENT_NAMESPACE | default "platform-mesh-system"}}'
57+
IMAGE_TAG:
58+
sh: kubectl get deployment {{.DEPLOYMENT_NAME}} -n {{.DEPLOYMENT_NAMESPACE}} -o jsonpath='{.spec.template.spec.containers[0].image}' | cut -d':' -f2
59+
IMAGE_NAME: ghcr.io/platform-mesh/kubernetes-graphql-gateway:{{.IMAGE_TAG}}
60+
cmds:
61+
- echo "Building image with tag {{.IMAGE_TAG}} using {{.CONTAINER_RUNTIME}}"
62+
- "{{.CONTAINER_RUNTIME}} build -t {{.IMAGE_NAME}} ."
63+
- |
64+
if [ "{{.CONTAINER_RUNTIME}}" = "podman" ]; then
65+
{{.CONTAINER_RUNTIME}} save {{.IMAGE_NAME}} -o /tmp/kind-image.tar
66+
kind load image-archive /tmp/kind-image.tar --name {{.KIND_CLUSTER}}
67+
rm -f /tmp/kind-image.tar
68+
else
69+
kind load docker-image {{.IMAGE_NAME}} --name {{.KIND_CLUSTER}}
70+
fi
71+
- kubectl rollout restart deployment/{{.DEPLOYMENT_NAME}} -n {{.DEPLOYMENT_NAMESPACE}}
72+
- echo "Image loaded and deployment restarted"
4973
## Testing
5074
fmt:
5175
cmds:
@@ -59,7 +83,14 @@ tasks:
5983
deps: [setup:envtest]
6084
env:
6185
KUBEBUILDER_ASSETS:
62-
sh: $(pwd)/{{.LOCAL_BIN}}/setup-envtest use {{.ENVTEST_K8S_VERSION}} --bin-dir $(pwd)/{{.LOCAL_BIN}} -p path
86+
# Workaround for setup-envtest bug: it doesn't verify binaries exist if directory is present
87+
sh: |
88+
ASSETS_PATH=$(pwd)/{{.LOCAL_BIN}}/k8s/{{.ENVTEST_K8S_VERSION}}-$(go env GOOS)-$(go env GOARCH)
89+
if [ ! -x "$ASSETS_PATH/etcd" ] || [ ! -x "$ASSETS_PATH/kube-apiserver" ]; then
90+
$(pwd)/{{.LOCAL_BIN}}/setup-envtest use {{.ENVTEST_K8S_VERSION}} --bin-dir $(pwd)/{{.LOCAL_BIN}} --force -p path
91+
else
92+
echo "$ASSETS_PATH"
93+
fi
6394
cmds:
6495
- go test ./... {{.ADDITIONAL_COMMAND_ARGS}}
6596
test:

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func initConfig() {
6060
// Listener
6161
v.SetDefault("listener-apiexport-workspace", ":root")
6262
v.SetDefault("listener-apiexport-name", "kcp.io")
63+
v.SetDefault("listener-default-sa-expiration-seconds", 3600)
6364

6465
// Gateway
6566
v.SetDefault("gateway-port", "8080")

common/apis/v1alpha1/clusteraccess_types.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ type ClusterAccessStatus struct {
9191
}
9292

9393
type ServiceAccountRef struct {
94-
Name string `json:"name,omitempty"`
95-
Namespace string `json:"namespace,omitempty"`
94+
Name string `json:"name"`
95+
Namespace string `json:"namespace"`
9696
Audience []string `json:"audience,omitempty"`
9797
TokenExpiration *metav1.Duration `json:"token_expiration,omitempty"`
9898
}

common/auth/metadata_injector.go

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,30 +21,40 @@ import (
2121

2222
// MetadataInjectionConfig contains configuration for metadata injection
2323
type MetadataInjectionConfig struct {
24-
Host string
25-
Path string
26-
Auth *gatewayv1alpha1.AuthConfig
27-
CA *gatewayv1alpha1.CAConfig
28-
HostOverride string // For virtual workspaces
24+
Host string
25+
Path string
26+
Auth *gatewayv1alpha1.AuthConfig
27+
CA *gatewayv1alpha1.CAConfig
28+
HostOverride string // For virtual workspaces
29+
DefaultSAExpirationSeconds int64 // Default expiration for ServiceAccount tokens
2930
}
3031

3132
// MetadataInjector provides metadata injection services with structured logging
3233
type MetadataInjector struct {
33-
log *logger.Logger
34-
client client.Client
34+
log *logger.Logger
35+
client client.Client
36+
defaultSAExpirationSeconds int64
3537
}
3638

3739
// NewMetadataInjector creates a new MetadataInjector service
38-
func NewMetadataInjector(log *logger.Logger, client client.Client) *MetadataInjector {
40+
func NewMetadataInjector(log *logger.Logger, client client.Client, defaultSAExpirationSeconds int64) *MetadataInjector {
41+
if defaultSAExpirationSeconds <= 0 {
42+
defaultSAExpirationSeconds = 3600
43+
}
3944
return &MetadataInjector{
40-
log: log,
41-
client: client,
45+
log: log,
46+
client: client,
47+
defaultSAExpirationSeconds: defaultSAExpirationSeconds,
4248
}
4349
}
4450

4551
// InjectClusterMetadata injects cluster metadata into schema JSON
4652
// This unified function handles both KCP and ClusterAccess use cases
4753
func (m *MetadataInjector) InjectClusterMetadata(ctx context.Context, schemaJSON []byte, config MetadataInjectionConfig) ([]byte, error) {
54+
if config.DefaultSAExpirationSeconds > 0 {
55+
m.defaultSAExpirationSeconds = config.DefaultSAExpirationSeconds
56+
}
57+
4858
// Parse the existing schema JSON
4959
var schemaData map[string]any
5060
if err := json.Unmarshal(schemaJSON, &schemaData); err != nil {
@@ -146,6 +156,10 @@ func (m *MetadataInjector) extractAuthDataForMetadata(ctx context.Context, auth
146156
return m.extractKubeconfigAuth(ctx, auth.KubeconfigSecretRef)
147157
}
148158

159+
if auth.ServiceAccount != nil {
160+
return m.extractServiceAccountAuth(ctx, auth.ServiceAccount)
161+
}
162+
149163
if auth.ClientCertificateRef != nil {
150164
return m.extractClientCertAuth(ctx, auth.ClientCertificateRef)
151165
}
@@ -210,6 +224,30 @@ func (m *MetadataInjector) extractClientCertAuth(ctx context.Context, certRef *g
210224
}, nil
211225
}
212226

227+
// extractServiceAccountAuth stores service account reference info for dynamic token generation
228+
// The actual token generation happens at request time in the gateway's roundtripper
229+
func (m *MetadataInjector) extractServiceAccountAuth(_ context.Context, saRef *gatewayv1alpha1.ServiceAccountRef) (map[string]any, error) {
230+
namespace := saRef.Namespace
231+
232+
expirationSeconds := m.defaultSAExpirationSeconds
233+
if saRef.TokenExpiration != nil {
234+
expirationSeconds = int64(saRef.TokenExpiration.Seconds())
235+
}
236+
237+
result := map[string]any{
238+
"type": "serviceAccount",
239+
"serviceAccountName": saRef.Name,
240+
"serviceAccountNamespace": namespace,
241+
"tokenExpirationSeconds": expirationSeconds,
242+
}
243+
244+
if len(saRef.Audience) > 0 {
245+
result["audience"] = saRef.Audience
246+
}
247+
248+
return result, nil
249+
}
250+
213251
// getSecret is a helper function to retrieve secrets with namespace defaulting
214252
func (m *MetadataInjector) getSecret(ctx context.Context, name, namespace string) (*corev1.Secret, error) {
215253
if namespace == "" {
@@ -460,26 +498,26 @@ func (m *MetadataInjector) finalizeSchemaInjection(schemaData map[string]any, me
460498

461499
// InjectClusterMetadata is a legacy wrapper for backward compatibility
462500
func InjectClusterMetadata(ctx context.Context, schemaJSON []byte, config MetadataInjectionConfig, k8sClient client.Client, log *logger.Logger) ([]byte, error) {
463-
injector := NewMetadataInjector(log, k8sClient)
501+
injector := NewMetadataInjector(log, k8sClient, config.DefaultSAExpirationSeconds)
464502
return injector.InjectClusterMetadata(ctx, schemaJSON, config)
465503
}
466504

467505
// InjectKCPMetadataFromEnv is a legacy wrapper for backward compatibility
468506
func InjectKCPMetadataFromEnv(schemaJSON []byte, clusterPath string, log *logger.Logger, hostOverride ...string) ([]byte, error) {
469-
injector := NewMetadataInjector(log, nil)
507+
injector := NewMetadataInjector(log, nil, 0)
470508
return injector.InjectKCPMetadataFromEnv(schemaJSON, clusterPath, hostOverride...)
471509
}
472510

473511
// Test exports for internal testing - these expose internal methods for unit tests
474512

475513
// extractKubeconfigFromEnv is exported for testing
476514
func extractKubeconfigFromEnv(log *logger.Logger) ([]byte, string, error) {
477-
injector := NewMetadataInjector(log, nil)
515+
injector := NewMetadataInjector(log, nil, 0)
478516
return injector.extractKubeconfigFromEnv()
479517
}
480518

481519
// extractAuthDataForMetadata is exported for testing
482520
func extractAuthDataForMetadata(ctx context.Context, auth *gatewayv1alpha1.AuthConfig, k8sClient client.Client) (map[string]any, error) {
483-
injector := NewMetadataInjector(nil, k8sClient)
521+
injector := NewMetadataInjector(nil, k8sClient, 0)
484522
return injector.extractAuthDataForMetadata(ctx, auth)
485523
}

0 commit comments

Comments
 (0)