diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 773a2ef..c5b99fd 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -168,8 +168,8 @@ jobs: - name: Build and push image uses: docker/build-push-action@v5 with: - file: ./docker/mock-proxy/Dockerfile - context: . + file: Dockerfile + context: ./docker/mock-proxy push: true platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} diff --git a/Makefile b/Makefile index 0dea80d..fcdc98a 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,9 @@ dev-docker-compose-up: ## Start Docker compose dev-docker-compose-down: ## Stop Docker compose docker compose -f docker/docker-compose.yaml down +.PHONY: dev-docker-compose-restart +dev-docker-compose-restart: dev-docker-compose-down dev-docker-compose-up + .PHONY: lt lt: lint test ## Run linters and tests (always do this!) diff --git a/adapters/secrets/hashicorp_vault.go b/adapters/secrets/hashicorp_vault.go new file mode 100644 index 0000000..e0cdcc6 --- /dev/null +++ b/adapters/secrets/hashicorp_vault.go @@ -0,0 +1,162 @@ +package secrets + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "time" + + vault "github.com/hashicorp/vault/api" + authkubernetes "github.com/hashicorp/vault/api/auth/kubernetes" +) + +type hashicorpVaultService struct { + client *vault.Client + secretPath string + mountPath string + log *slog.Logger +} + +type VaultConfig struct { + Address string // Vault server address (e.g., http://localhost:8200) + Token string // Vault token for authentication (used when AuthMethod=="token") + SecretPrefix string // Path prefix for secrets (e.g., "secrets/builder-hub") + MountPath string // Vault KV v2 mount path (e.g., "secret", defaults to "secret") + AuthMethod string // "token" (default) or "kubernetes" + Role string // Role name for Kubernetes auth (required if AuthMethod=="kubernetes") + Jwt string // ServiceAccount JWT for Kubernetes auth (required if AuthMethod=="kubernetes") +} + +func NewHashicorpVaultService(ctx context.Context, log *slog.Logger, cfg VaultConfig) (*hashicorpVaultService, error) { + if cfg.MountPath == "" { + cfg.MountPath = "secret" + } + + if cfg.AuthMethod != "token" && cfg.AuthMethod != "kubernetes" && cfg.AuthMethod != "" { + return nil, fmt.Errorf("unsupported AuthMethod %s", cfg.AuthMethod) + } + + vcfg := vault.DefaultConfig() + vcfg.Address = cfg.Address + client, err := vault.NewClient(vcfg) + if err != nil { + return nil, fmt.Errorf("failed to create Vault client: %w", err) + } + + svc := &hashicorpVaultService{ + client: client, + secretPath: cfg.SecretPrefix, + mountPath: cfg.MountPath, + log: log, + } + + if cfg.AuthMethod == "kubernetes" { + if cfg.Jwt == "" { + return nil, fmt.Errorf("JWT is required for Kubernetes auth") + } + k8sAuth, err := authkubernetes.NewKubernetesAuth(cfg.Role, authkubernetes.WithServiceAccountToken(cfg.Jwt)) + if err != nil { + return nil, fmt.Errorf("failed to initialize Kubernetes auth: %w", err) + } + authInfo, err := client.Auth().Login(ctx, k8sAuth) + if err != nil { + return nil, fmt.Errorf("kubernetes auth failed: %w", err) + } + watcher, err := client.NewLifetimeWatcher(&vault.LifetimeWatcherInput{Secret: authInfo}) + if err != nil { + return nil, fmt.Errorf("failed to create token lifetime watcher: %w", err) + } + go svc.watchTokenRenewal(ctx, watcher) + } else { + if cfg.Token == "" { + return nil, errors.New("token is required for vault auth") + } + client.SetToken(cfg.Token) + } + + // Verify connection by attempting a read (404 is fine — path may not exist yet) + verifyCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + _, verifyErr := client.KVv2(cfg.MountPath).Get(verifyCtx, cfg.SecretPrefix) + if verifyErr != nil && !isVault404(verifyErr) { + return nil, fmt.Errorf("failed to verify Vault connection: %w", verifyErr) + } + + return svc, nil +} + +func (s *hashicorpVaultService) watchTokenRenewal(ctx context.Context, watcher *vault.LifetimeWatcher) { + go watcher.Start() + defer watcher.Stop() + + for { + select { + case <-ctx.Done(): + return + case err := <-watcher.DoneCh(): + if err != nil { + s.log.Error("vault token renewal stopped", "err", err) + } + return + case <-watcher.RenewCh(): + s.log.Debug("vault token renewed") + } + } +} + +func isVault404(err error) bool { + var responseErr *vault.ResponseError + return errors.As(err, &responseErr) && responseErr.StatusCode == 404 +} + +func (s *hashicorpVaultService) secretKVPath(builderName string) string { + if s.secretPath == "" { + return builderName + } + return fmt.Sprintf("%s/%s", s.secretPath, builderName) +} + +// GetSecretValues retrieves secrets for a specific builder from Vault KV v2. +// Implements application.SecretAccessor interface. +func (s *hashicorpVaultService) GetSecretValues(ctx context.Context, builderName string) (json.RawMessage, error) { + path := s.secretKVPath(builderName) + + secret, err := s.client.KVv2(s.mountPath).Get(ctx, path) + if err != nil { + if isVault404(err) { + return json.RawMessage("{}"), nil + } + return nil, fmt.Errorf("failed to read secret from Vault: %w", err) + } + + if secret == nil || secret.Data == nil { + return json.RawMessage("{}"), nil + } + + secretJSON, err := json.Marshal(secret.Data) + if err != nil { + return nil, fmt.Errorf("failed to marshal Vault secret: %w", err) + } + + return json.RawMessage(secretJSON), nil +} + +// SetSecretValues stores secrets for a specific builder in Vault KV v2. +// Implements ports.AdminSecretService interface. +func (s *hashicorpVaultService) SetSecretValues(ctx context.Context, builderName string, values json.RawMessage) error { + path := s.secretKVPath(builderName) + + var dataMap map[string]any + if err := json.Unmarshal(values, &dataMap); err != nil { + return fmt.Errorf("failed to unmarshal secret values: %w", err) + } + + _, err := s.client.KVv2(s.mountPath).Put(ctx, path, dataMap) + if err != nil { + return fmt.Errorf("failed to write secret to Vault: %w", err) + } + + return nil +} diff --git a/adapters/secrets/service.go b/adapters/secrets/service.go index 7452ff2..2d4545f 100644 --- a/adapters/secrets/service.go +++ b/adapters/secrets/service.go @@ -1,7 +1,8 @@ -// Package secrets contains logic for adapter to aws secrets manager +// Package secrets implements secrets storage backends package secrets import ( + "context" "encoding/json" "errors" @@ -9,14 +10,15 @@ import ( "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/secretsmanager" + "github.com/flashbots/builder-hub/application" ) -type Service struct { +type awsSecretsService struct { sm *secretsmanager.SecretsManager secretPrefix string } -func NewService(secretPrefix string) (*Service, error) { +func NewAWSSecretsManagerService(secretPrefix string) (*awsSecretsService, error) { sess, err := session.NewSession(&aws.Config{ Region: aws.String("us-east-2"), }) @@ -27,21 +29,19 @@ func NewService(secretPrefix string) (*Service, error) { // Create a Secrets Manager client svc := secretsmanager.New(sess) - return &Service{sm: svc, secretPrefix: secretPrefix}, nil + return &awsSecretsService{sm: svc, secretPrefix: secretPrefix}, nil } -var ErrMissingSecret = errors.New("missing secret for builder") - -func (s *Service) secretName(builderName string) string { +func (s *awsSecretsService) secretName(builderName string) string { return s.secretPrefix + "/" + builderName } -func (s *Service) GetSecretValues(builderName string) (json.RawMessage, error) { +func (s *awsSecretsService) GetSecretValues(ctx context.Context, builderName string) (json.RawMessage, error) { input := &secretsmanager.GetSecretValueInput{ SecretId: aws.String(s.secretName(builderName)), } - result, err := s.sm.GetSecretValue(input) + result, err := s.sm.GetSecretValueWithContext(ctx, input) if err != nil { // If the secret doesn't exist, return empty JSON for new builders var awsErr awserr.Error @@ -58,19 +58,19 @@ func (s *Service) GetSecretValues(builderName string) (json.RawMessage, error) { builderSecret, ok := secretData[builderName] if !ok { - return nil, ErrMissingSecret + return nil, application.ErrMissingSecret } return builderSecret, nil } -func (s *Service) SetSecretValues(builderName string, values json.RawMessage) error { +func (s *awsSecretsService) SetSecretValues(ctx context.Context, builderName string, values json.RawMessage) error { secretName := s.secretName(builderName) input := &secretsmanager.GetSecretValueInput{ SecretId: aws.String(secretName), } - result, err := s.sm.GetSecretValue(input) + result, err := s.sm.GetSecretValueWithContext(ctx, input) var secretData map[string]json.RawMessage if err != nil { @@ -88,7 +88,7 @@ func (s *Service) SetSecretValues(builderName string, values json.RawMessage) er Name: aws.String(secretName), SecretString: aws.String(string(newSecretString)), } - _, createErr := s.sm.CreateSecret(createInput) + _, createErr := s.sm.CreateSecretWithContext(ctx, createInput) return createErr } return err @@ -111,7 +111,7 @@ func (s *Service) SetSecretValues(builderName string, values json.RawMessage) er SecretId: aws.String(secretName), SecretString: aws.String(string(newSecretString)), } - _, err = s.sm.PutSecretValue(sv) + _, err = s.sm.PutSecretValueWithContext(ctx, sv) if err != nil { return err } diff --git a/application/service.go b/application/service.go index 6dd3432..02f4734 100644 --- a/application/service.go +++ b/application/service.go @@ -3,6 +3,7 @@ package application import ( "context" "encoding/json" + "errors" "fmt" "net" @@ -19,8 +20,10 @@ type BuilderDataAccessor interface { LogEvent(ctx context.Context, eventName, builderName, name string) error } +var ErrMissingSecret = errors.New("missing secret for builder") + type SecretAccessor interface { - GetSecretValues(builderName string) (json.RawMessage, error) + GetSecretValues(ctx context.Context, builderName string) (json.RawMessage, error) } type BuilderHub struct { @@ -53,7 +56,7 @@ func (b *BuilderHub) GetConfigWithSecrets(ctx context.Context, builderName strin if err != nil { return nil, fmt.Errorf("failing to fetch config for builder %s %w", builderName, err) } - secr, err := b.secretAccessor.GetSecretValues(builderName) + secr, err := b.secretAccessor.GetSecretValues(ctx, builderName) if err != nil { return nil, fmt.Errorf("failing to fetch secrets for builder %s %w", builderName, err) } diff --git a/cmd/httpserver/main.go b/cmd/httpserver/main.go index d442b6e..aeefacb 100644 --- a/cmd/httpserver/main.go +++ b/cmd/httpserver/main.go @@ -1,6 +1,8 @@ package main import ( + "context" + "fmt" "log" "os" "os/signal" @@ -101,16 +103,66 @@ var flags = []cli.Flag{ Usage: "Postgres DSN", EnvVars: []string{"POSTGRES_DSN"}, }, + // AWS Secrets Manager configuration &cli.StringFlag{ Name: "secret-prefix", Value: "", Usage: "AWS Secret name", EnvVars: []string{"AWS_BUILDER_CONFIGS_SECRET_NAME", "AWS_BUILDER_CONFIGS_SECRET_PREFIX"}, }, + // HashiCorp Vault configuration + &cli.StringFlag{ + Name: "vault-address", + Value: "http://localhost:8200", + Usage: "HashiCorp Vault server address (use with --vault-enabled)", + EnvVars: []string{"VAULT_ADDR"}, + }, + &cli.StringFlag{ + Name: "vault-token", + Value: "", + Usage: "HashiCorp Vault token for authentication (use with --vault-enabled)", + EnvVars: []string{"VAULT_TOKEN"}, + }, + &cli.StringFlag{ + Name: "vault-auth-method", + Value: "token", + Usage: "Vault authentication method", + EnvVars: []string{"VAULT_AUTH_METHOD"}, + }, + &cli.StringFlag{ + Name: "vault-kubernetes-role", + Value: "", + Usage: "Vault role name for Kubernetes auth", + EnvVars: []string{"VAULT_KUBERNETES_ROLE"}, + }, + &cli.StringFlag{ + Name: "vault-kubernetes-jwt-path", + Value: "/var/run/secrets/kubernetes.io/serviceaccount/token", + Usage: "Path to ServiceAccount JWT for Kubernetes auth", + EnvVars: []string{"VAULT_KUBERNETES_JWT_PATH"}, + }, + &cli.StringFlag{ + Name: "vault-secret-path", + Value: "secrets/builder-hub", + Usage: "Vault KV path for builder secrets (use with --vault-enabled)", + EnvVars: []string{"VAULT_SECRET_PATH"}, + }, + &cli.StringFlag{ + Name: "vault-mount-path", + Value: "secret", + Usage: "Vault secrets mount path (e.g., 'secret', 'kv', 'kv-v2')", + EnvVars: []string{"VAULT_MOUNT_PATH"}, + }, + &cli.BoolFlag{ + Name: "vault-enabled", + Value: false, + Usage: "Use HashiCorp Vault for secrets storage (overrides secret-prefix)", + EnvVars: []string{"VAULT_ENABLED"}, + }, &cli.BoolFlag{ Name: "mock-secrets", Value: false, - Usage: "Use inmemory secrets service for testing", + Usage: "Use inmemory secrets service for testing (overrides other secret backends)", EnvVars: []string{"MOCK_SECRETS"}, }, } @@ -130,6 +182,9 @@ func main() { } func runCli(cCtx *cli.Context) error { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + listenAddr := cCtx.String("listen-addr") adminAddr := cCtx.String("admin-addr") internalAddr := cCtx.String("internal-addr") @@ -144,6 +199,14 @@ func runCli(cCtx *cli.Context) error { adminBasicUser := cCtx.String("admin-basic-user") adminPasswordBcrypt := cCtx.String("admin-basic-password-bcrypt") disableAdminAuth := cCtx.Bool("disable-admin-auth") + vaultEnabled := cCtx.Bool("vault-enabled") + vaultToken := cCtx.String("vault-token") + vaultAddress := cCtx.String("vault-address") + vaultSecretPath := cCtx.String("vault-secret-path") + vaultMountPath := cCtx.String("vault-mount-path") + vaultAuthMethod := cCtx.String("vault-auth-method") + vaultRole := cCtx.String("vault-kubernetes-role") + vaultJwtPath := cCtx.String("vault-kubernetes-jwt-path") logTags := map[string]string{ "version": common.Version, @@ -175,15 +238,52 @@ func runCli(cCtx *cli.Context) error { var sm ports.AdminSecretService + // Determine secret backend: mock > vault > aws-secrets-manager if mockSecretsStorage { - log.Info("using mock secrets storage") + log.Info("using mock secrets storage (in-memory)") sm = domain.NewMockSecretService() - } else { - sm, err = secrets.NewService(cCtx.String("secret-prefix")) + } else if vaultEnabled { + log.Info("using HashiCorp Vault for secrets", + "address", vaultAddress, + "secret_path", vaultSecretPath, + "mount_path", vaultMountPath, + "auth_method", vaultAuthMethod) + + var vaultJwt string + if vaultAuthMethod == "kubernetes" { + jwtBytes, err := os.ReadFile(vaultJwtPath) + if err != nil { + log.Error("failed to read Vault JWT file", "path", vaultJwtPath, "err", err) + return err + } + vaultJwt = string(jwtBytes) + } + + vaultConfig := secrets.VaultConfig{ + Address: vaultAddress, + Token: vaultToken, + SecretPrefix: vaultSecretPath, + MountPath: vaultMountPath, + AuthMethod: vaultAuthMethod, + Role: vaultRole, + Jwt: vaultJwt, + } + + sm, err = secrets.NewHashicorpVaultService(ctx, log.Logger, vaultConfig) + if err != nil { + log.Error("failed to create Vault secrets service", "err", err) + return err + } + } else if cCtx.String("secret-prefix") != "" { + log.Info("using AWS Secrets Manager for secrets", "prefix", cCtx.String("secret-prefix")) + sm, err = secrets.NewAWSSecretsManagerService(cCtx.String("secret-prefix")) if err != nil { log.Error("failed to create secrets manager", "err", err) return err } + } else { + log.Error("no secrets backend configured: set --vault-enabled or --secret-prefix for production, or use --mock-secrets for local development") + return fmt.Errorf("no secrets backend configured") } builderHub := application.NewBuilderHub(db, sm) @@ -214,10 +314,8 @@ func runCli(cCtx *cli.Context) error { return err } - exit := make(chan os.Signal, 1) - signal.Notify(exit, os.Interrupt, syscall.SIGTERM) srv.RunInBackground() - <-exit + <-ctx.Done() // Shutdown server once termination signal is received srv.Shutdown() diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 582e742..05af61e 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -18,6 +18,52 @@ services: start_period: 2s timeout: 5s + vault: + image: hashicorp/vault:1.21 + ports: + - 127.0.0.1:8200:8200 + environment: + VAULT_DEV_ROOT_TOKEN_ID: "root-token" + VAULT_DEV_LISTEN_ADDRESS: "0.0.0.0:8200" + command: server -dev -dev-root-token-id="root-token" -dev-listen-address="0.0.0.0:8200" + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8200/v1/sys/health?standbyok=true&perfstandbyok=true || exit 1"] + interval: 5s + retries: 10 + start_period: 5s + timeout: 5s + cap_add: + - IPC_LOCK + + # Python server that always returns authenticated: true for any TokenReview request + # (Vault requires an asymmetric JWT algorithm like RS256, but delegates actual validation to this mock) + mock-k8s: + build: + context: ../. + dockerfile: docker/mock-k8s/Dockerfile + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8443/"] + interval: 5s + retries: 5 + start_period: 2s + timeout: 5s + + # Generates a fresh RSA key + RS256-signed SA JWT, writes it to the shared volume, + # configures Vault kubernetes auth pointing at mock-k8s, creates a builder-hub policy with secret/* access + vault-init: + build: + context: ../. + dockerfile: docker/vault-init/Dockerfile + depends_on: + vault: + condition: service_healthy + mock-k8s: + condition: service_healthy + volumes: + # JWT for builder-hub to read - prepared in this container + - vault-jwt:/vault-jwt + restart: "no" + builder-hub-api: image: flashbots/builder-hub build: @@ -27,15 +73,32 @@ services: db: condition: service_healthy restart: true + vault: + condition: service_healthy + restart: true + vault-init: + condition: service_completed_successfully links: - "db:database" + - "vault:vault" + volumes: + # builder-hub reads this JWT, logs in via Vault kubernetes auth, + # gets a token with the builder-hub policy and then can read/write KV secrets + - vault-jwt:/var/run/secrets/kubernetes.io/serviceaccount ports: - 127.0.0.1:8080:8080 - 127.0.0.1:8081:8081 - 127.0.0.1:8082:8082 - 127.0.0.1:8090:8090 environment: - MOCK_SECRETS: true + MOCK_SECRETS: false + VAULT_ENABLED: true + VAULT_ADDR: "http://vault:8200" + VAULT_AUTH_METHOD: "kubernetes" + VAULT_KUBERNETES_ROLE: "builder-hub" + VAULT_KUBERNETES_JWT_PATH: "/var/run/secrets/kubernetes.io/serviceaccount/token" + VAULT_MOUNT_PATH: "secret" + VAULT_SECRET_PATH: "" POSTGRES_DSN: "postgres://postgres:postgres@db:5432/postgres?sslmode=disable" LISTEN_ADDR: "0.0.0.0:8080" ADMIN_ADDR: "0.0.0.0:8081" @@ -44,10 +107,15 @@ services: DISABLE_ADMIN_AUTH: "1" # local dev only; do not use in production proxy: - image: flashbots/builder-hub-mock-proxy + build: + context: ./mock-proxy + dockerfile: Dockerfile links: - "builder-hub-api:builder-hub-api" ports: - 127.0.0.1:8888:8888 environment: TARGET: "http://builder-hub-api:8080" + +volumes: + vault-jwt: diff --git a/docker/mock-k8s/Dockerfile b/docker/mock-k8s/Dockerfile new file mode 100644 index 0000000..16b5a30 --- /dev/null +++ b/docker/mock-k8s/Dockerfile @@ -0,0 +1,4 @@ +FROM python:3.12-alpine +RUN apk add --no-cache wget +COPY docker/mock-k8s/server.py /server.py +CMD ["python3", "/server.py"] diff --git a/docker/mock-k8s/server.py b/docker/mock-k8s/server.py new file mode 100644 index 0000000..51c7a46 --- /dev/null +++ b/docker/mock-k8s/server.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Minimal Kubernetes TokenReview API mock for local Vault Kubernetes auth testing.""" +from http.server import HTTPServer, BaseHTTPRequestHandler +import json + + +class Handler(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.end_headers() + + def do_POST(self): + content_length = int(self.headers.get("Content-Length", 0)) + self.rfile.read(content_length) + body = json.dumps({ + "apiVersion": "authentication.k8s.io/v1", + "kind": "TokenReview", + "status": { + "authenticated": True, + "user": { + "username": "system:serviceaccount:default:builder-hub", + "uid": "builder-hub-dev-uid", + "groups": [ + "system:serviceaccounts", + "system:serviceaccounts:default", + "system:authenticated", + ], + }, + }, + }).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, fmt, *args): + pass + + +HTTPServer(("0.0.0.0", 8443), Handler).serve_forever() diff --git a/docker/mock-proxy/Dockerfile b/docker/mock-proxy/Dockerfile index ed8fb91..c818148 100644 --- a/docker/mock-proxy/Dockerfile +++ b/docker/mock-proxy/Dockerfile @@ -1,5 +1,4 @@ # BuilderHub expects measurements headers. For testing purposes, we just mock them with # this nginx-based proxy container. FROM nginx:1.27 -COPY ./docker/mock-proxy/nginx-default.conf /etc/nginx/conf.d/default.conf - +COPY nginx-default.conf /etc/nginx/conf.d/default.conf diff --git a/docker/vault-init/Dockerfile b/docker/vault-init/Dockerfile new file mode 100644 index 0000000..e866bf5 --- /dev/null +++ b/docker/vault-init/Dockerfile @@ -0,0 +1,7 @@ +FROM hashicorp/vault:1.21 +USER root +RUN apk add --no-cache python3 py3-pip openssl +RUN pip3 install --no-cache-dir --break-system-packages PyJWT cryptography +COPY docker/vault-init/init.sh /init.sh +RUN chmod +x /init.sh +ENTRYPOINT ["/bin/sh", "/init.sh"] diff --git a/docker/vault-init/init.sh b/docker/vault-init/init.sh new file mode 100644 index 0000000..b85661c --- /dev/null +++ b/docker/vault-init/init.sh @@ -0,0 +1,55 @@ +#!/bin/sh +set -e + +export VAULT_ADDR="http://vault:8200" +export VAULT_TOKEN="root-token" + +# Generate RSA key pair for signing the SA JWT (RS256 required by Vault k8s auth) +openssl genrsa -out /tmp/sa.key 2048 +openssl rsa -in /tmp/sa.key -pubout -out /tmp/sa.pub + +# Create a k8s-style SA JWT signed with the RSA key, valid for 10 years +# Vault validates the RS256 algorithm, then delegates to mock-k8s for TokenReview +python3 - <<'EOF' +import jwt, time + +payload = { + "iss": "kubernetes/serviceaccount", + "sub": "system:serviceaccount:default:builder-hub", + "kubernetes.io/serviceaccount/namespace": "default", + "kubernetes.io/serviceaccount/service-account.name": "builder-hub", + "kubernetes.io/serviceaccount/service-account.uid": "builder-hub-dev-uid", + "exp": int(time.time()) + 86400 * 365 * 10, + "iat": int(time.time()), +} +key = open("/tmp/sa.key", "rb").read() +token = jwt.encode(payload, key, algorithm="RS256") +open("/vault-jwt/token", "w").write(token) +print("SA JWT written to /vault-jwt/token") +EOF + +# Enable Kubernetes auth method +vault auth enable kubernetes + +# Configure Kubernetes auth to use mock-k8s for TokenReview (no real k8s API needed) +vault write auth/kubernetes/config \ + kubernetes_host="http://mock-k8s:8443" \ + disable_iss_validation=true \ + disable_local_ca_jwt=true + +# Create a policy granting read/write access to the KV secrets mount +vault policy write builder-hub - <<'POLICY' +path "secret/*" { + capabilities = ["create", "read", "update", "delete", "list"] +} +POLICY + +# Create a role for the builder-hub service account +vault write auth/kubernetes/role/builder-hub \ + bound_service_account_names="builder-hub" \ + bound_service_account_namespaces="default" \ + token_policies="default,builder-hub" \ + token_ttl="24h" \ + token_max_ttl="48h" + +echo "Vault Kubernetes auth configured." diff --git a/docs/api-docs/admin-api/Update builder config.bru b/docs/api-docs/admin-api/Update builder config.bru index d62e41e..9c64441 100644 --- a/docs/api-docs/admin-api/Update builder config.bru +++ b/docs/api-docs/admin-api/Update builder config.bru @@ -13,8 +13,8 @@ post { body:json { { "dns_name": "foobar-v1.a.b.c", - "rbuilder": { - "extra_data": "FooBar" + "foo": { + "bar": "baz" } } } diff --git a/docs/api-docs/admin-api/Update secrets config.bru b/docs/api-docs/admin-api/Update secrets config.bru index 8c9c78f..f99f756 100644 --- a/docs/api-docs/admin-api/Update secrets config.bru +++ b/docs/api-docs/admin-api/Update secrets config.bru @@ -11,5 +11,9 @@ post { } body:json { - {} + { + "rbuilder": { + "extra_data": "FooBar" + } + } } diff --git a/domain/inmemory_secret.go b/domain/inmemory_secret.go index 70a994f..ca406fe 100644 --- a/domain/inmemory_secret.go +++ b/domain/inmemory_secret.go @@ -1,6 +1,7 @@ package domain import ( + "context" "encoding/json" "sync" ) @@ -17,13 +18,13 @@ func NewMockSecretService() *InmemorySecretService { } } -func (mss *InmemorySecretService) GetSecretValues(builderName string) (json.RawMessage, error) { +func (mss *InmemorySecretService) GetSecretValues(ctx context.Context, builderName string) (json.RawMessage, error) { mss.mu.RLock() defer mss.mu.RUnlock() return mss.st[builderName], nil } -func (mss *InmemorySecretService) SetSecretValues(builderName string, values json.RawMessage) error { +func (mss *InmemorySecretService) SetSecretValues(ctx context.Context, builderName string, values json.RawMessage) error { mss.mu.Lock() defer mss.mu.Unlock() mss.st[builderName] = values diff --git a/go.mod b/go.mod index 0e3b794..f1c5053 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/flashbots/builder-hub -go 1.22 +go 1.23.0 require ( github.com/VictoriaMetrics/metrics v1.35.1 @@ -10,29 +10,48 @@ require ( github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/httplog/v2 v2.1.1 github.com/google/uuid v1.6.0 + github.com/hashicorp/vault/api v1.22.0 + github.com/hashicorp/vault/api/auth/kubernetes v0.10.0 github.com/jackc/pgtype v1.14.3 github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.10.9 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v2 v2.27.2 - go.uber.org/atomic v1.11.0 + go.uber.org/atomic v1.6.0 + golang.org/x/crypto v0.40.0 ) require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-jose/go-jose/v4 v4.1.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.7 // indirect + github.com/hashicorp/hcl v1.0.1-vault-7 // indirect github.com/holiman/uint256 v1.3.1 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect github.com/valyala/fastrand v1.1.0 // indirect github.com/valyala/histogram v1.2.0 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect - golang.org/x/crypto v0.27.0 // indirect - golang.org/x/sys v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/time v0.12.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 89c832d..2b8cbbf 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -19,19 +21,50 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ethereum/go-ethereum v1.14.11 h1:8nFDCUUE67rPc6AKxFj7JKaOa2W/W1Rse3oS6LvvxEY= github.com/ethereum/go-ethereum v1.14.11/go.mod h1:+l/fr42Mma+xBnhefL/+z11/hcmJ2egl+ScIVPjhc7E= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/httplog/v2 v2.1.1 h1:ojojiu4PIaoeJ/qAO4GWUxJqvYUTobeo7zmuHQJAxRk= github.com/go-chi/httplog/v2 v2.1.1/go.mod h1:/XXdxicJsp4BA5fapgIC3VuTD+z0Z/VzukoB3VDc1YE= +github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0= +github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= +github.com/hashicorp/vault/api/auth/kubernetes v0.10.0 h1:5rqWmUFxnu3S7XYq9dafURwBgabYDFzo2Wv+AMopPHs= +github.com/hashicorp/vault/api/auth/kubernetes v0.10.0/go.mod h1:cZZmhF6xboMDmDbMY52oj2DKW6gS0cQ9g0pJ5XIXQ5U= github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs= github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= @@ -111,11 +144,19 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -127,6 +168,8 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -145,8 +188,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= @@ -160,9 +203,8 @@ github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxt go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -182,8 +224,9 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -198,6 +241,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -218,8 +263,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -235,8 +280,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -248,6 +295,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/ports/admin_handler.go b/ports/admin_handler.go index 342271a..8644b25 100644 --- a/ports/admin_handler.go +++ b/ports/admin_handler.go @@ -23,7 +23,7 @@ type AdminBuilderService interface { } type AdminSecretService interface { - SetSecretValues(builderName string, message json.RawMessage) error + SetSecretValues(ctx context.Context, builderName string, message json.RawMessage) error application.SecretAccessor } @@ -57,24 +57,23 @@ func (s *AdminHandler) GetActiveConfigForBuilder(w http.ResponseWriter, r *http. } // GetFullConfigForBuilder returns the full config for a builder, including secrets -// Note this copies logic from GetConfigWithSecrets in BuilderHubService -// since we decided to avoid application layer here it probably makes sense unless -// logic gets more complicated here func (s *AdminHandler) GetFullConfigForBuilder(w http.ResponseWriter, r *http.Request) { builderName := chi.URLParam(r, "builderName") _, err := s.builderService.GetActiveConfigForBuilder(r.Context(), builderName) - if err != nil { - s.log.Error("failed to get config with secrets", "error", err) + if errors.Is(err, domain.ErrNotFound) { + w.WriteHeader(http.StatusNotFound) + return + } else if err != nil { + s.log.Error("failed to get config for builder", "error", err) w.WriteHeader(http.StatusInternalServerError) return } - secr, err := s.secretService.GetSecretValues(builderName) + secr, err := s.secretService.GetSecretValues(r.Context(), builderName) if err != nil { s.log.Error("failed to get secrets", "error", err) w.WriteHeader(http.StatusInternalServerError) return } - _, err = w.Write(secr) if err != nil { s.log.Error("failed to write response", "error", err) @@ -219,7 +218,7 @@ func (s *AdminHandler) SetSecrets(w http.ResponseWriter, r *http.Request) { return } - err = s.secretService.SetSecretValues(builderName, body) + err = s.secretService.SetSecretValues(r.Context(), builderName, body) if err != nil { s.log.Error("failed to set secret", "error", err) w.WriteHeader(http.StatusInternalServerError) diff --git a/scripts/ci/integration-test.sh b/scripts/ci/integration-test.sh index 19acfd4..362d89c 100755 --- a/scripts/ci/integration-test.sh +++ b/scripts/ci/integration-test.sh @@ -4,6 +4,10 @@ set -eu SCRIPT_DIR=$(dirname "$0") cd "$SCRIPT_DIR/../.." +# Stop previous containers +echo "Stopping previous Docker containers..." +docker compose -f docker/docker-compose.yaml down + # Build the Docker images echo "Building Docker images..." docker compose -f docker/docker-compose.yaml build @@ -25,7 +29,7 @@ echo "Running integration tests..." # Enable failing for this command set +e -hurl --test scripts/ci/e2e-test.hurl +hurl --test scripts/ci/e2e-test.hurl -v set -e # Cleanup after tests @@ -36,4 +40,4 @@ if [ $? -ne 0 ]; then fi echo "Integration tests completed successfully ✅" -docker compose -f docker/docker-compose.yaml down -v \ No newline at end of file +docker compose -f docker/docker-compose.yaml down -v