Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions chart/templates/clusterrole-operator-manager-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ rules:
- ""
resources:
- configmaps
- secrets
verbs:
- create
- delete
Expand All @@ -33,15 +34,6 @@ rules:
- get
- list
- watch
- apiGroups:
- ""
resources:
- secrets
verbs:
- create
- get
- list
- watch
- apiGroups:
- ""
resources:
Expand Down
46 changes: 46 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import (

decositesv1alpha1 "github.com/deco-sites/decofile-operator/api/v1alpha1"
"github.com/deco-sites/decofile-operator/internal/build"
"github.com/deco-sites/decofile-operator/internal/buildsecrets"
"github.com/deco-sites/decofile-operator/internal/controller"
"github.com/deco-sites/decofile-operator/internal/valkey"
webhookv1 "github.com/deco-sites/decofile-operator/internal/webhook/v1"
Expand Down Expand Up @@ -81,6 +82,8 @@ func main() {
var valkeyAdminPassword string
var valkeyResyncPeriod time.Duration
var valkeyWatchFailover bool
var buildSecretsBackend string
var buildSecretsResyncPeriod time.Duration
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
Expand Down Expand Up @@ -114,6 +117,12 @@ func main() {
os.Getenv("VALKEY_WATCH_FAILOVER") != "false",
"Subscribe to Sentinel +switch-master events and trigger immediate ACL resync on failover. "+
"Enabled by default when VALKEY_SENTINEL_URLS is set. Set VALKEY_WATCH_FAILOVER=false to disable.")
flag.StringVar(&buildSecretsBackend, "build-secrets-backend",
getEnvOrDefault("BUILD_SECRETS_BACKEND", "aws"),
"Backend for tenant build-secrets sync. Supported: aws | disabled.")
flag.DurationVar(&buildSecretsResyncPeriod, "build-secrets-resync-period",
parseDuration(os.Getenv("BUILD_SECRETS_RESYNC_PERIOD"), controller.DefaultBuildSecretsResyncPeriod),
"How often to re-fetch upstream build-secrets even when nothing changed in the cluster.")
opts := zap.Options{
Development: false,
}
Expand Down Expand Up @@ -267,6 +276,28 @@ func main() {
setupLog.Error(err, "unable to create controller", "controller", "Namespace")
os.Exit(1)
}

buildSecretsSource, err := newBuildSecretsSource(context.Background(), buildSecretsBackend)
if err != nil {
setupLog.Error(err, "unable to initialize build-secrets source", "backend", buildSecretsBackend)
os.Exit(1)
}
if buildSecretsSource != nil {
bsReconciler := &controller.BuildSecretsReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Source: buildSecretsSource,
ResyncPeriod: buildSecretsResyncPeriod,
}
if err := bsReconciler.SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "BuildSecrets")
os.Exit(1)
}
setupLog.Info("Build-secrets sync enabled", "backend", buildSecretsBackend, "resyncPeriod", buildSecretsResyncPeriod)
} else {
setupLog.Info("Build-secrets sync disabled (BUILD_SECRETS_BACKEND=disabled)")
}

// Start Sentinel failover watcher if enabled and Sentinel is configured.
// leaderElectedRunnable ensures only the active leader subscribes — prevents
// redundant TriggerResyncAll calls from non-leader replicas.
Expand Down Expand Up @@ -411,3 +442,18 @@ func getEnvOrDefault(key, defaultVal string) string {
}
return defaultVal
}

// newBuildSecretsSource picks an upstream secret backend for the
// BuildSecretsReconciler. `disabled` returns (nil, nil) so callers can
// skip wiring the reconciler entirely — useful for local dev and for
// clusters where this feature is not yet enabled.
func newBuildSecretsSource(ctx context.Context, backend string) (buildsecrets.Source, error) {
switch backend {
case "aws":
return buildsecrets.NewAWSSource(ctx)
case "disabled", "":
return nil, nil
default:
return nil, fmt.Errorf("unknown build-secrets backend %q (supported: aws | disabled)", backend)
}
}
10 changes: 1 addition & 9 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ rules:
- ""
resources:
- configmaps
- secrets
verbs:
- create
- delete
Expand All @@ -34,15 +35,6 @@ rules:
- get
- list
- watch
- apiGroups:
- ""
resources:
- secrets
verbs:
- create
- get
- list
- watch
- apiGroups:
- ""
resources:
Expand Down
18 changes: 7 additions & 11 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ go 1.24.0

require (
github.com/andybalholm/brotli v1.2.0
github.com/aws/aws-sdk-go-v2 v1.36.3
github.com/aws/aws-sdk-go-v2 v1.41.7
github.com/aws/aws-sdk-go-v2/config v1.29.14
github.com/aws/aws-sdk-go-v2/credentials v1.17.67
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.7
github.com/go-logr/logr v1.4.3
github.com/google/uuid v1.6.0
github.com/onsi/ginkgo/v2 v2.22.0
github.com/onsi/gomega v1.36.1
Expand All @@ -23,20 +23,17 @@ require (
require (
cel.dev/expr v0.24.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
github.com/aws/smithy-go v1.22.2 // indirect
github.com/aws/smithy-go v1.25.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/blendle/zapdriver v1.3.1 // indirect
Expand All @@ -49,7 +46,6 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
Expand Down
28 changes: 10 additions & 18 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,34 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=
github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 h1:jIiopHEV22b4yQP2q36Y0OmwLbsxNWdWwfZRR5QRRO4=
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.7 h1:JUGKqUnJHbXpS8uyuICP/zpQ+vXUIXW2zTEqjMLCqrY=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.7/go.mod h1:l/cqI7ujYqBuTR6Ll13d9/gG/uUdlVzJ1UDltEEBTOo=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
Expand Down
82 changes: 82 additions & 0 deletions internal/buildsecrets/aws_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
Copyright 2025.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0
*/

package buildsecrets

import (
"context"
"encoding/json"
"errors"
"fmt"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
smtypes "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
)

// awsSecretsManagerAPI captures the AWS Secrets Manager surface this
// package uses. Defined so tests can swap a mock without depending on
// the real SDK client.
type awsSecretsManagerAPI interface {
GetSecretValue(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error)
}

// AWSSource resolves keys against AWS Secrets Manager. Credentials come
// from the ambient environment (Pod Identity / IRSA in-cluster; default
// chain locally). Region is taken from the AWS_REGION env var or the
// default chain — usually set by the EKS Pod Identity webhook.
type AWSSource struct {
api awsSecretsManagerAPI
}

// NewAWSSource builds a Source backed by the live Secrets Manager
// client. The reconciler keeps the same instance for its lifetime.
func NewAWSSource(ctx context.Context) (*AWSSource, error) {
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return nil, fmt.Errorf("load AWS config: %w", err)
}
return &AWSSource{api: secretsmanager.NewFromConfig(cfg)}, nil
}

// Get fetches the JSON-encoded secret at `key` and decodes it into a
// flat map. A missing key returns (nil, false, nil) — the natural
// "not provisioned yet" state for tenants that have opted in but not
// supplied data.
func (s *AWSSource) Get(ctx context.Context, key string) (map[string]string, bool, error) {
out, err := s.api.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{
SecretId: aws.String(key),
})
if err != nil {
var nfe *smtypes.ResourceNotFoundException
if errors.As(err, &nfe) {
return nil, false, nil
}
return nil, false, fmt.Errorf("get secret %q: %w", key, err)
}

if out.SecretString == nil {
// Binary secrets are not supported — builds inject env vars,
// not files, so a JSON object of strings is the only shape.
return nil, false, fmt.Errorf("secret %q has no SecretString (binary secret?)", key)
}

var data map[string]string
if err := json.Unmarshal([]byte(*out.SecretString), &data); err != nil {
return nil, false, fmt.Errorf("parse %q as JSON object of strings: %w", key, err)
}
// Reject JSON `null` (Unmarshal leaves the map nil without erroring).
// An empty object {} is fine and yields a non-nil empty map.
if data == nil {
return nil, false, fmt.Errorf("secret %q payload is JSON null; expected an object of strings", key)
}
return data, true, nil
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}
Loading
Loading