diff --git a/cmd/preflight/BUILD.bazel b/cmd/preflight/BUILD.bazel index b8258daea5..e3a76240ba 100644 --- a/cmd/preflight/BUILD.bazel +++ b/cmd/preflight/BUILD.bazel @@ -7,6 +7,7 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pkg/cli", + "//pkg/config", "//svc/preflight", ], ) diff --git a/cmd/preflight/main.go b/cmd/preflight/main.go index b82a24456c..fac40ac53a 100644 --- a/cmd/preflight/main.go +++ b/cmd/preflight/main.go @@ -2,9 +2,9 @@ package preflight import ( "context" - "time" "github.com/unkeyed/unkey/pkg/cli" + "github.com/unkeyed/unkey/pkg/config" "github.com/unkeyed/unkey/svc/preflight" ) @@ -14,52 +14,17 @@ var Cmd = &cli.Command{ Name: "preflight", Usage: "Run the pod mutation webhook for secrets and credentials injection", Flags: []cli.Flag{ - cli.Int("http-port", "HTTP port for the webhook server. Default: 8443", - cli.Default(8443), cli.EnvVar("UNKEY_HTTP_PORT")), - cli.String("tls-cert-file", "Path to TLS certificate file", - cli.Required(), cli.EnvVar("UNKEY_TLS_CERT_FILE")), - cli.String("tls-key-file", "Path to TLS private key file", - cli.Required(), cli.EnvVar("UNKEY_TLS_KEY_FILE")), - cli.String("inject-image", "Container image for inject binary", - cli.Default("inject:latest"), cli.EnvVar("UNKEY_INJECT_IMAGE")), - cli.String("inject-image-pull-policy", "Image pull policy (Always, IfNotPresent, Never)", - cli.Default("IfNotPresent"), cli.EnvVar("UNKEY_INJECT_IMAGE_PULL_POLICY")), - cli.String("krane-endpoint", "Endpoint for Krane secrets service", - cli.Default("http://krane.unkey.svc.cluster.local:8070"), cli.EnvVar("UNKEY_KRANE_ENDPOINT")), - cli.String("depot-token", "Depot API token for fetching on-demand pull tokens (optional)", - cli.EnvVar("UNKEY_DEPOT_TOKEN")), - cli.StringSlice("insecure-registries", "Comma-separated list of insecure (HTTP) registries", - cli.EnvVar("UNKEY_INSECURE_REGISTRIES")), - cli.StringSlice("registry-aliases", "Comma-separated list of registry aliases (from=to)", - cli.EnvVar("UNKEY_REGISTRY_ALIASES")), - // Logging Sampler Configuration - cli.Float("log-sample-rate", "Baseline probability (0.0-1.0) of emitting log events. Default: 1.0", - cli.Default(1.0), cli.EnvVar("UNKEY_LOG_SAMPLE_RATE")), - cli.Duration("log-slow-threshold", "Duration threshold for slow event sampling. Default: 1s", - cli.Default(time.Second), cli.EnvVar("UNKEY_LOG_SLOW_THRESHOLD")), + cli.String("config", "Path to a TOML config file", + cli.Default("unkey.toml"), cli.EnvVar("UNKEY_CONFIG")), }, Action: action, } func action(ctx context.Context, cmd *cli.Command) error { - config := preflight.Config{ - HttpPort: cmd.Int("http-port"), - TLSCertFile: cmd.RequireString("tls-cert-file"), - TLSKeyFile: cmd.RequireString("tls-key-file"), - InjectImage: cmd.String("inject-image"), - InjectImagePullPolicy: cmd.String("inject-image-pull-policy"), - KraneEndpoint: cmd.String("krane-endpoint"), - DepotToken: cmd.String("depot-token"), - InsecureRegistries: cmd.StringSlice("insecure-registries"), - RegistryAliases: cmd.StringSlice("registry-aliases"), - // Logging sampler configuration - LogSampleRate: cmd.Float("log-sample-rate"), - LogSlowThreshold: cmd.Duration("log-slow-threshold"), + cfg, err := config.Load[preflight.Config](cmd.String("config")) + if err != nil { + return cli.Exit("Failed to load config: "+err.Error(), 1) } - if err := config.Validate(); err != nil { - return cli.Exit("Invalid configuration: "+err.Error(), 1) - } - - return preflight.Run(ctx, config) + return preflight.Run(ctx, cfg) } diff --git a/cmd/sentinel/BUILD.bazel b/cmd/sentinel/BUILD.bazel index a447fbd543..af919e1181 100644 --- a/cmd/sentinel/BUILD.bazel +++ b/cmd/sentinel/BUILD.bazel @@ -7,7 +7,7 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pkg/cli", - "//pkg/uid", + "//pkg/config", "//svc/sentinel", ], ) diff --git a/cmd/sentinel/main.go b/cmd/sentinel/main.go index db7341b6a2..f249b6169f 100644 --- a/cmd/sentinel/main.go +++ b/cmd/sentinel/main.go @@ -2,10 +2,9 @@ package sentinel import ( "context" - "time" "github.com/unkeyed/unkey/pkg/cli" - "github.com/unkeyed/unkey/pkg/uid" + "github.com/unkeyed/unkey/pkg/config" "github.com/unkeyed/unkey/svc/sentinel" ) @@ -19,72 +18,17 @@ var Cmd = &cli.Command{ Name: "sentinel", Usage: "Run the Unkey Sentinel server (deployment proxy)", Flags: []cli.Flag{ - // Server Configuration - cli.Int("http-port", "HTTP port for the Sentinel server to listen on. Default: 8080", - cli.Default(8080), cli.EnvVar("UNKEY_HTTP_PORT")), - - // Instance Identification - cli.String("sentinel-id", "Unique identifier for this sentinel instance. Auto-generated if not provided.", - cli.Default(uid.New("sentinel", 4)), cli.EnvVar("UNKEY_SENTINEL_ID")), - - cli.String("workspace-id", "Workspace ID this sentinel serves. Required.", - cli.Required(), cli.EnvVar("UNKEY_WORKSPACE_ID")), - - cli.String("environment-id", "Environment ID this sentinel serves (handles all deployments in this environment). Required.", - cli.Required(), cli.EnvVar("UNKEY_ENVIRONMENT_ID")), - - cli.String("region", "Geographic region identifier. Used for logging. Default: unknown", - cli.Default("unknown"), cli.EnvVar("UNKEY_REGION")), - - // Database Configuration - cli.String("database-primary", "MySQL connection string for primary database. Required.", - cli.Required(), cli.EnvVar("UNKEY_DATABASE_PRIMARY")), - - cli.String("database-replica", "MySQL connection string for read-replica.", - cli.EnvVar("UNKEY_DATABASE_REPLICA")), - - cli.String("clickhouse-url", "ClickHouse connection string. Optional.", - cli.EnvVar("UNKEY_CLICKHOUSE_URL")), - - // Observability - cli.Bool("otel", "Enable OpenTelemetry tracing and metrics", - cli.EnvVar("UNKEY_OTEL")), - cli.Float("otel-trace-sampling-rate", "Sampling rate for OpenTelemetry traces (0.0-1.0). Default: 0.25", - cli.Default(0.25), cli.EnvVar("UNKEY_OTEL_TRACE_SAMPLING_RATE")), - cli.Int("prometheus-port", "Enable Prometheus /metrics endpoint on specified port. Set to 0 to disable.", cli.EnvVar("UNKEY_PROMETHEUS_PORT")), - - // Logging Sampler Configuration - cli.Float("log-sample-rate", "Baseline probability (0.0-1.0) of emitting log events. Default: 1.0", - cli.Default(1.0), cli.EnvVar("UNKEY_LOG_SAMPLE_RATE")), - cli.Duration("log-slow-threshold", "Duration threshold for slow event sampling. Default: 1s", - cli.Default(time.Second), cli.EnvVar("UNKEY_LOG_SLOW_THRESHOLD")), + cli.String("config", "Path to a TOML config file", + cli.Default("unkey.toml"), cli.EnvVar("UNKEY_CONFIG")), }, Action: action, } func action(ctx context.Context, cmd *cli.Command) error { - return sentinel.Run(ctx, sentinel.Config{ - // Instance identification - SentinelID: cmd.String("sentinel-id"), - WorkspaceID: cmd.String("workspace-id"), - EnvironmentID: cmd.String("environment-id"), - Region: cmd.String("region"), - - // HTTP configuration - HttpPort: cmd.Int("http-port"), - - // Database configuration - DatabasePrimary: cmd.String("database-primary"), - DatabaseReadonlyReplica: cmd.String("database-replica"), - ClickhouseURL: cmd.String("clickhouse-url"), - - // Observability - OtelEnabled: cmd.Bool("otel"), - OtelTraceSamplingRate: cmd.Float("otel-trace-sampling-rate"), - PrometheusPort: cmd.Int("prometheus-port"), + cfg, err := config.Load[sentinel.Config](cmd.String("config")) + if err != nil { + return cli.Exit("Failed to load config: "+err.Error(), 1) + } - // Logging sampler configuration - LogSampleRate: cmd.Float("log-sample-rate"), - LogSlowThreshold: cmd.Duration("log-slow-threshold"), - }) + return sentinel.Run(ctx, cfg) } diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 92ab4b1836..43f4db3d9c 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -63,7 +63,7 @@ services: deploy: replicas: 3 endpoint_mode: vip - command: ["run", "api"] + command: ["run", "api", "--config", "/etc/unkey/api.toml"] build: context: ../ dockerfile: ./Dockerfile @@ -80,22 +80,8 @@ services: condition: service_started ctrl-api: condition: service_started - environment: - UNKEY_HTTP_PORT: 7070 - UNKEY_REDIS_URL: "redis://redis:6379" - UNKEY_DATABASE_PRIMARY: "unkey:password@tcp(mysql:3306)/unkey?parseTime=true" - UNKEY_CLICKHOUSE_URL: "clickhouse://default:password@clickhouse:9000?secure=false&skip_verify=true" - UNKEY_CHPROXY_AUTH_TOKEN: "chproxy-test-token-123" - UNKEY_OTEL: false - UNKEY_VAULT_URL: "http://vault:8060" - UNKEY_VAULT_TOKEN: "vault-test-token-123" - UNKEY_KAFKA_BROKERS: "kafka:9092" - UNKEY_CLICKHOUSE_ANALYTICS_URL: "http://clickhouse:8123/default" - UNKEY_CTRL_URL: "http://ctrl-api:7091" - UNKEY_CTRL_TOKEN: "your-local-dev-key" - UNKEY_PPROF_ENABLED: "true" - UNKEY_PPROF_USERNAME: "admin" - UNKEY_PPROF_PASSWORD: "password" + volumes: + - ./config/api.toml:/etc/unkey/api.toml:ro redis: networks: @@ -126,20 +112,14 @@ services: build: context: ../ dockerfile: Dockerfile - command: ["run", "vault"] + command: ["run", "vault", "--config", "/etc/unkey/vault.toml"] ports: - "8060:8060" depends_on: s3: condition: service_healthy - environment: - UNKEY_HTTP_PORT: "8060" - UNKEY_S3_URL: "http://s3:3902" - UNKEY_S3_BUCKET: "vault" - UNKEY_S3_ACCESS_KEY_ID: "minio_root_user" - UNKEY_S3_ACCESS_KEY_SECRET: "minio_root_password" - UNKEY_MASTER_KEYS: "Ch9rZWtfMmdqMFBJdVhac1NSa0ZhNE5mOWlLSnBHenFPENTt7an5MRogENt9Si6wms4pQ2XIvqNSIgNpaBenJmXgcInhu6Nfv2U=" - UNKEY_BEARER_TOKEN: "vault-test-token-123" + volumes: + - ./config/vault.toml:/etc/unkey/vault.toml:ro healthcheck: test: ["CMD", "/unkey", "healthcheck", "http://localhost:8060/health/live"] timeout: 10s @@ -210,29 +190,17 @@ services: args: VERSION: "latest" container_name: krane - command: ["run", "krane"] + command: ["run", "krane", "--config", "/etc/unkey/krane.toml"] ports: - "8070:8070" volumes: # Mount Docker socket for Docker backend support - /var/run/docker.sock:/var/run/docker.sock + - ./config/krane.toml:/etc/unkey/krane.toml:ro depends_on: vault: condition: service_healthy environment: - # Server configuration - UNKEY_REGION: "local.dev" # currently required to receive filtered events from ctrl - UNKEY_CONTROL_PLANE_URL: "http://ctrl-api:7091" - UNKEY_CONTROL_PLANE_BEARER: "your-local-dev-key" - - # Backend configuration - use Docker backend for development - UNKEY_KRANE_BACKEND: "docker" - UNKEY_DOCKER_SOCKET: "/var/run/docker.sock" - - # Vault configuration for secrets decryption - UNKEY_VAULT_URL: "http://vault:8060" - UNKEY_VAULT_TOKEN: "vault-test-token-123" - UNKEY_REGISTRY_URL: "${UNKEY_REGISTRY_URL:-}" UNKEY_REGISTRY_USERNAME: "${UNKEY_REGISTRY_USERNAME:-}" UNKEY_REGISTRY_PASSWORD: "${UNKEY_REGISTRY_PASSWORD:-}" @@ -266,7 +234,7 @@ services: args: VERSION: "latest" container_name: ctrl-api - command: ["run", "ctrl", "api"] + command: ["run", "ctrl", "api", "--config", "/etc/unkey/ctrl-api.toml"] ports: - "7091:7091" depends_on: @@ -282,30 +250,10 @@ services: clickhouse: condition: service_healthy required: true + volumes: + - ./config/ctrl-api.toml:/etc/unkey/ctrl-api.toml:ro environment: - UNKEY_DATABASE_PRIMARY: "unkey:password@tcp(mysql:3306)/unkey?parseTime=true&interpolateParams=true" - - # Control plane configuration - UNKEY_HTTP_PORT: "7091" - - # Restate configuration (ctrl api only needs ingress client, not server) - UNKEY_RESTATE_INGRESS_URL: "http://restate:8080" - UNKEY_RESTATE_ADMIN_URL: "http://restate:9070" - UNKEY_RESTATE_API_KEY: "" - - # Build configuration (for presigned URLs) - UNKEY_BUILD_S3_URL: "${UNKEY_BUILD_S3_URL:-http://s3:3902}" - UNKEY_BUILD_S3_EXTERNAL_URL: "${UNKEY_BUILD_S3_EXTERNAL_URL:-http://localhost:3902}" - UNKEY_BUILD_S3_BUCKET: "build-contexts" - UNKEY_BUILD_S3_ACCESS_KEY_ID: "${UNKEY_BUILD_S3_ACCESS_KEY_ID:-minio_root_user}" - UNKEY_BUILD_S3_ACCESS_KEY_SECRET: "${UNKEY_BUILD_S3_ACCESS_KEY_SECRET:-minio_root_password}" - - # API key for simple authentication - UNKEY_AUTH_TOKEN: "your-local-dev-key" - - # Certificate bootstrap - UNKEY_DEFAULT_DOMAIN: "unkey.local" - UNKEY_CNAME_DOMAIN: "unkey.local" + UNKEY_GITHUB_APP_WEBHOOK_SECRET: "${UNKEY_GITHUB_APP_WEBHOOK_SECRET:-}" ctrl-worker: networks: @@ -316,7 +264,7 @@ services: args: VERSION: "latest" container_name: ctrl-worker - command: ["run", "ctrl", "worker"] + command: ["run", "ctrl", "worker", "--config", "/etc/unkey/ctrl-worker.toml"] env_file: - .env.depot ports: @@ -345,42 +293,7 @@ services: required: true volumes: - /var/run/docker.sock:/var/run/docker.sock - environment: - UNKEY_DATABASE_PRIMARY: "unkey:password@tcp(mysql:3306)/unkey?parseTime=true&interpolateParams=true" - - # Domain configuration (used by deploy and routing services) - UNKEY_DEFAULT_DOMAIN: "unkey.local" - UNKEY_CNAME_DOMAIN: "unkey.local" - - # Restate configuration - UNKEY_RESTATE_ADMIN_URL: "http://restate:9070" - UNKEY_RESTATE_HTTP_PORT: "9080" - UNKEY_RESTATE_REGISTER_AS: "http://ctrl-worker:9080" - - # Vault service for secret encryption - UNKEY_VAULT_URL: "http://vault:8060" - UNKEY_VAULT_TOKEN: "vault-test-token-123" - - # Build configuration (loaded from .env.depot) - UNKEY_BUILD_S3_BUCKET: "build-contexts" - - # Build configuration - UNKEY_BUILD_PLATFORM: "linux/amd64" - - # Registry configuration (UNKEY_REGISTRY_PASSWORD loaded from .env.depot) - UNKEY_REGISTRY_URL: "registry.depot.dev" - UNKEY_REGISTRY_USERNAME: "x-token" - - # Depot-specific configuration - UNKEY_DEPOT_API_URL: "https://api.depot.dev" - UNKEY_DEPOT_PROJECT_REGION: "us-east-1" - - # ClickHouse - UNKEY_CLICKHOUSE_URL: "clickhouse://default:password@clickhouse:9000?secure=false&skip_verify=true" - UNKEY_CLICKHOUSE_ADMIN_URL: "clickhouse://unkey_user_admin:C57RqT5EPZBqCJkMxN9mEZZEzMPcw9yBlwhIizk99t7kx6uLi9rYmtWObsXzdl@clickhouse:9000?secure=false&skip_verify=true" - - # Sentinel image for deployments - UNKEY_SENTINEL_IMAGE: "unkey/sentinel:latest" + - ./config/ctrl-worker.toml:/etc/unkey/ctrl-worker.toml:ro otel: networks: diff --git a/dev/k8s/manifests/preflight.yaml b/dev/k8s/manifests/preflight.yaml index 64bdc18a66..9ea7922242 100644 --- a/dev/k8s/manifests/preflight.yaml +++ b/dev/k8s/manifests/preflight.yaml @@ -39,6 +39,30 @@ subjects: name: preflight namespace: unkey --- +# ConfigMap for the preflight +apiVersion: v1 +kind: ConfigMap +metadata: + name: preflight-config + namespace: unkey +data: + unkey.toml: | + http_port = 8443 + krane_endpoint = "http://krane.unkey.svc.cluster.local:8070" + depot_token = "${UNKEY_DEPOT_TOKEN}" + + [tls] + cert_file = "/certs/tls.crt" + key_file = "/certs/tls.key" + + [inject] + image = "inject:latest" + image_pull_policy = "Never" + + [registry] + insecure_registries = ["ctlptl-registry.unkey.svc.cluster.local:5000"] + aliases = ["ctlptl-registry:5000=ctlptl-registry.unkey.svc.cluster.local:5000"] +--- # Deployment for the preflight apiVersion: apps/v1 kind: Deployment @@ -89,25 +113,9 @@ spec: containers: - name: webhook image: unkey/go:latest - args: ["run", "preflight"] + args: ["run", "preflight", "--config", "/etc/unkey/unkey.toml"] imagePullPolicy: IfNotPresent env: - - name: UNKEY_HTTP_PORT - value: "8443" - - name: UNKEY_TLS_CERT_FILE - value: "/certs/tls.crt" - - name: UNKEY_TLS_KEY_FILE - value: "/certs/tls.key" - - name: UNKEY_INJECT_IMAGE - value: "inject:latest" - - name: UNKEY_INJECT_IMAGE_PULL_POLICY - value: "Never" # Local dev uses pre-loaded images; use IfNotPresent in prod - - name: UNKEY_KRANE_ENDPOINT - value: "http://krane.unkey.svc.cluster.local:8070" - - name: UNKEY_INSECURE_REGISTRIES - value: "ctlptl-registry.unkey.svc.cluster.local:5000" - - name: UNKEY_REGISTRY_ALIASES - value: "ctlptl-registry:5000=ctlptl-registry.unkey.svc.cluster.local:5000" - name: UNKEY_DEPOT_TOKEN valueFrom: secretKeyRef: @@ -121,6 +129,9 @@ spec: - name: tls-certs mountPath: /certs readOnly: true + - name: config + mountPath: /etc/unkey + readOnly: true resources: requests: cpu: 50m @@ -172,6 +183,9 @@ spec: volumes: - name: tls-certs emptyDir: {} + - name: config + configMap: + name: preflight-config --- # Service for the webhook apiVersion: v1 diff --git a/pkg/db/deployment_topology_update_desired_status.sql_generated.go b/pkg/db/deployment_topology_update_desired_status.sql_generated.go index f172fd2363..136f832cc1 100644 --- a/pkg/db/deployment_topology_update_desired_status.sql_generated.go +++ b/pkg/db/deployment_topology_update_desired_status.sql_generated.go @@ -31,6 +31,12 @@ type UpdateDeploymentTopologyDesiredStatusParams struct { // SET desired_status = ?, version = ?, updated_at = ? // WHERE deployment_id = ? AND region = ? func (q *Queries) UpdateDeploymentTopologyDesiredStatus(ctx context.Context, db DBTX, arg UpdateDeploymentTopologyDesiredStatusParams) error { - _, err := db.ExecContext(ctx, updateDeploymentTopologyDesiredStatus, arg.DesiredStatus, arg.Version, arg.UpdatedAt, arg.DeploymentID, arg.Region) + _, err := db.ExecContext(ctx, updateDeploymentTopologyDesiredStatus, + arg.DesiredStatus, + arg.Version, + arg.UpdatedAt, + arg.DeploymentID, + arg.Region, + ) return err } diff --git a/pkg/db/querier_generated.go b/pkg/db/querier_generated.go index 310908c977..0ec1ce628b 100644 --- a/pkg/db/querier_generated.go +++ b/pkg/db/querier_generated.go @@ -2412,13 +2412,6 @@ type Querier interface { // SET desired_state = ?, updated_at = ? // WHERE id = ? UpdateDeploymentDesiredState(ctx context.Context, db DBTX, arg UpdateDeploymentDesiredStateParams) error - //UpdateDeploymentTopologyDesiredStatus updates the desired_status and version of a topology entry. - // A new version is required so that WatchDeployments picks up the change. - // - // UPDATE `deployment_topology` - // SET desired_status = ?, version = ?, updated_at = ? - // WHERE deployment_id = ? AND region = ? - UpdateDeploymentTopologyDesiredStatus(ctx context.Context, db DBTX, arg UpdateDeploymentTopologyDesiredStatusParams) error //UpdateDeploymentImage // // UPDATE deployments @@ -2437,6 +2430,13 @@ type Querier interface { // SET status = ?, updated_at = ? // WHERE id = ? UpdateDeploymentStatus(ctx context.Context, db DBTX, arg UpdateDeploymentStatusParams) error + // UpdateDeploymentTopologyDesiredStatus updates the desired_status and version of a topology entry. + // A new version is required so that WatchDeployments picks up the change. + // + // UPDATE `deployment_topology` + // SET desired_status = ?, version = ?, updated_at = ? + // WHERE deployment_id = ? AND region = ? + UpdateDeploymentTopologyDesiredStatus(ctx context.Context, db DBTX, arg UpdateDeploymentTopologyDesiredStatusParams) error //UpdateFrontlineRouteDeploymentId // // UPDATE frontline_routes diff --git a/svc/preflight/BUILD.bazel b/svc/preflight/BUILD.bazel index 0d050223e8..6936ecee7f 100644 --- a/svc/preflight/BUILD.bazel +++ b/svc/preflight/BUILD.bazel @@ -9,7 +9,7 @@ go_library( importpath = "github.com/unkeyed/unkey/svc/preflight", visibility = ["//visibility:public"], deps = [ - "//pkg/assert", + "//pkg/config", "//pkg/logger", "//pkg/runner", "//pkg/tls", diff --git a/svc/preflight/config.go b/svc/preflight/config.go index 0419ff36c4..ad5a75a084 100644 --- a/svc/preflight/config.go +++ b/svc/preflight/config.go @@ -1,41 +1,79 @@ package preflight import ( - "time" - - "github.com/unkeyed/unkey/pkg/assert" + "github.com/unkeyed/unkey/pkg/config" ) -var validImagePullPolicies = map[string]bool{ - "Always": true, - "IfNotPresent": true, - "Never": true, +// TLSConfig holds filesystem paths for the TLS certificate and private key +// used by the webhook HTTPS server. +type TLSConfig struct { + // CertFile is the path to a PEM-encoded TLS certificate. + CertFile string `toml:"cert_file" config:"required,nonempty"` + + // KeyFile is the path to a PEM-encoded TLS private key. + KeyFile string `toml:"key_file" config:"required,nonempty"` +} + +// InjectConfig controls the container image injected into mutated pods by the +// admission webhook. +type InjectConfig struct { + // Image is the container image reference for the inject binary. + Image string `toml:"image" config:"default=inject:latest"` + + // ImagePullPolicy is the Kubernetes image pull policy applied to the + // injected init container. + ImagePullPolicy string `toml:"image_pull_policy" config:"default=IfNotPresent,oneof=Always|IfNotPresent|Never"` +} + +// RegistryConfig configures container registry behavior for the preflight +// webhook, including insecure registries and alias mappings. +type RegistryConfig struct { + // InsecureRegistries is a list of registry hostnames that should be + // contacted over plain HTTP instead of HTTPS. + InsecureRegistries []string `toml:"insecure_registries"` + + // Aliases is a list of registry alias mappings in "from=to" format. + // The webhook rewrites image references matching the left-hand side to + // the right-hand side before pulling. + Aliases []string `toml:"aliases"` } +// Config holds the complete configuration for the preflight admission webhook +// server. It is designed to be loaded from a TOML file using [config.Load]: +// +// cfg, err := config.Load[preflight.Config]("/etc/unkey/preflight.toml") +// +// Environment variables are expanded in file values using ${VAR} or +// ${VAR:-default} syntax before parsing. type Config struct { - HttpPort int - TLSCertFile string - TLSKeyFile string - InjectImage string - InjectImagePullPolicy string - KraneEndpoint string - DepotToken string - InsecureRegistries []string - RegistryAliases []string - - // --- Logging sampler configuration --- - - // LogSampleRate is the baseline probability (0.0-1.0) of emitting log events. - LogSampleRate float64 - - // LogSlowThreshold defines what duration qualifies as "slow" for sampling. - LogSlowThreshold time.Duration + // HttpPort is the TCP port the webhook HTTPS server binds to. + HttpPort int `toml:"http_port" config:"default=8443,min=1,max=65535"` + + // KraneEndpoint is the URL of the Krane secrets provider service. + KraneEndpoint string `toml:"krane_endpoint" config:"default=http://krane.unkey.svc.cluster.local:8070"` + + // DepotToken is an optional Depot API token for fetching on-demand + // container registry pull tokens. + DepotToken string `toml:"depot_token"` + + // TLS provides filesystem paths for HTTPS certificate and key. + // See [TLSConfig]. + TLS TLSConfig `toml:"tls"` + + // Inject controls the container image injected into mutated pods. + // See [InjectConfig]. + Inject InjectConfig `toml:"inject"` + + // Registry configures container registry behavior. See [RegistryConfig]. + Registry RegistryConfig `toml:"registry"` + + // Logging configures log sampling. See [config.LoggingConfig]. + Logging config.LoggingConfig `toml:"logging"` } +// Validate implements [config.Validator] so that [config.Load] calls it +// automatically after tag-level validation. All constraints are expressed +// through struct tags, so this method is a no-op. func (c *Config) Validate() error { - if c.HttpPort == 0 { - c.HttpPort = 8443 - } - - return assert.True(validImagePullPolicies[c.InjectImagePullPolicy], "inject-image-pull-policy must be one of: Always, IfNotPresent, Never") + return nil } diff --git a/svc/preflight/run.go b/svc/preflight/run.go index fed4357d9e..a125cec438 100644 --- a/svc/preflight/run.go +++ b/svc/preflight/run.go @@ -25,8 +25,8 @@ func Run(ctx context.Context, cfg Config) error { } logger.SetSampler(logger.TailSampler{ - SlowThreshold: cfg.LogSlowThreshold, - SampleRate: cfg.LogSampleRate, + SlowThreshold: cfg.Logging.SlowThreshold, + SampleRate: cfg.Logging.SampleRate, }) r := runner.New() @@ -55,11 +55,11 @@ func Run(ctx context.Context, cfg Config) error { reg := registry.New(registry.Config{ Clientset: clientset, Credentials: credentialsManager, - InsecureRegistries: cfg.InsecureRegistries, - RegistryAliases: cfg.RegistryAliases, + InsecureRegistries: cfg.Registry.InsecureRegistries, + RegistryAliases: cfg.Registry.Aliases, }) - tlsConfig, err := tls.NewFromFiles(cfg.TLSCertFile, cfg.TLSKeyFile) + tlsConfig, err := tls.NewFromFiles(cfg.TLS.CertFile, cfg.TLS.KeyFile) if err != nil { return fmt.Errorf("failed to load TLS certificates: %w", err) } @@ -78,8 +78,8 @@ func Run(ctx context.Context, cfg Config) error { Registry: reg, Clientset: clientset, Credentials: credentialsManager, - InjectImage: cfg.InjectImage, - InjectImagePullPolicy: cfg.InjectImagePullPolicy, + InjectImage: cfg.Inject.Image, + InjectImagePullPolicy: cfg.Inject.ImagePullPolicy, DefaultProviderEndpoint: cfg.KraneEndpoint, }) diff --git a/svc/sentinel/BUILD.bazel b/svc/sentinel/BUILD.bazel index 2f9243327f..cef40f0f30 100644 --- a/svc/sentinel/BUILD.bazel +++ b/svc/sentinel/BUILD.bazel @@ -9,9 +9,9 @@ go_library( importpath = "github.com/unkeyed/unkey/svc/sentinel", visibility = ["//visibility:public"], deps = [ - "//pkg/assert", "//pkg/clickhouse", "//pkg/clock", + "//pkg/config", "//pkg/db", "//pkg/logger", "//pkg/otel", diff --git a/svc/sentinel/config.go b/svc/sentinel/config.go index 992ac4e0e9..84d11b174b 100644 --- a/svc/sentinel/config.go +++ b/svc/sentinel/config.go @@ -3,53 +3,68 @@ package sentinel import ( "fmt" "slices" - "time" - "github.com/unkeyed/unkey/pkg/assert" + "github.com/unkeyed/unkey/pkg/config" ) +// ClickHouseConfig configures connections to ClickHouse for analytics storage. +// When URL is empty, a no-op analytics backend is used. +type ClickHouseConfig struct { + // URL is the ClickHouse connection string. + // Example: "clickhouse://default:password@clickhouse:9000?secure=false&skip_verify=true" + URL string `toml:"url"` +} + +// Config holds the complete configuration for the Sentinel server. It is +// designed to be loaded from a TOML file using [config.Load]: +// +// cfg, err := config.Load[sentinel.Config]("/etc/unkey/sentinel.toml") +// +// Environment variables are expanded in file values using ${VAR} or +// ${VAR:-default} syntax before parsing. Struct tag defaults are applied to +// any field left at its zero value after parsing, and validation runs +// automatically via [Config.Validate]. type Config struct { - SentinelID string + // SentinelID identifies this particular sentinel instance. Used in log + // attribution and request tracing. + SentinelID string `toml:"sentinel_id"` - WorkspaceID string + // WorkspaceID is the workspace this sentinel serves. + WorkspaceID string `toml:"workspace_id" config:"required,nonempty"` - // EnvironmentID identifies which environment this sentinel serves + // EnvironmentID identifies which environment this sentinel serves. // A single environment may have multiple deployments, and this sentinel - // handles all of them based on the deployment ID passed in each request - EnvironmentID string + // handles all of them based on the deployment ID passed in each request. + EnvironmentID string `toml:"environment_id" config:"required,nonempty"` - Region string + // Region is the geographic region identifier (e.g. "us-east-1.aws"). + // Included in structured logs and used for routing decisions. + Region string `toml:"region" config:"required,oneof=local.dev|us-east-1.aws|us-east-2.aws|us-west-1.aws|us-west-2.aws|eu-central-1.aws"` - HttpPort int + // HttpPort is the TCP port the sentinel server binds to. + HttpPort int `toml:"http_port" config:"default=8080,min=1,max=65535"` - DatabasePrimary string - DatabaseReadonlyReplica string + // PrometheusPort starts a Prometheus /metrics HTTP endpoint on the + // specified port. Set to 0 (the default) to disable the endpoint entirely. + PrometheusPort int `toml:"prometheus_port"` - ClickhouseURL string + // Database configures MySQL connections. See [config.DatabaseConfig]. + Database config.DatabaseConfig `toml:"database"` - OtelEnabled bool - OtelTraceSamplingRate float64 - PrometheusPort int + // ClickHouse configures analytics storage. See [ClickHouseConfig]. + ClickHouse ClickHouseConfig `toml:"clickhouse"` - // --- Logging sampler configuration --- + // Otel configures OpenTelemetry export. See [config.OtelConfig]. + Otel config.OtelConfig `toml:"otel"` - // LogSampleRate is the baseline probability (0.0-1.0) of emitting log events. - LogSampleRate float64 - - // LogSlowThreshold defines what duration qualifies as "slow" for sampling. - LogSlowThreshold time.Duration + // Logging configures log sampling. See [config.LoggingConfig]. + Logging config.LoggingConfig `toml:"logging"` } -func (c Config) Validate() error { - err := assert.All( - assert.NotEmpty(c.WorkspaceID, "workspace ID is required"), - assert.NotEmpty(c.EnvironmentID, "environment ID is required"), - ) - - if err != nil { - return err - } - +// Validate checks cross-field constraints that cannot be expressed through +// struct tags alone. It implements [config.Validator] so that [config.Load] +// calls it automatically after tag-level validation. +func (c *Config) Validate() error { validRegions := []string{ "local.dev", "us-east-1.aws", @@ -61,7 +76,6 @@ func (c Config) Validate() error { if !slices.Contains(validRegions, c.Region) { return fmt.Errorf("invalid region: %s, must be one of %v", c.Region, validRegions) - } return nil diff --git a/svc/sentinel/run.go b/svc/sentinel/run.go index 4d38a7b7d1..8de088059c 100644 --- a/svc/sentinel/run.go +++ b/svc/sentinel/run.go @@ -30,21 +30,21 @@ func Run(ctx context.Context, cfg Config) error { } logger.SetSampler(logger.TailSampler{ - SlowThreshold: cfg.LogSlowThreshold, - SampleRate: cfg.LogSampleRate, + SlowThreshold: cfg.Logging.SlowThreshold, + SampleRate: cfg.Logging.SampleRate, }) clk := clock.New() // Initialize OTEL before creating logger so the logger picks up the OTLP handler var shutdownGrafana func(context.Context) error - if cfg.OtelEnabled { + if cfg.Otel.Enabled { shutdownGrafana, err = otel.InitGrafana(ctx, otel.Config{ Application: "sentinel", Version: version.Version, InstanceID: cfg.SentinelID, CloudRegion: cfg.Region, - TraceSampleRate: cfg.OtelTraceSamplingRate, + TraceSampleRate: cfg.Otel.TraceSamplingRate, }) if err != nil { return fmt.Errorf("unable to init grafana: %w", err) @@ -87,8 +87,8 @@ func Run(ctx context.Context, cfg Config) error { } database, err := db.New(db.Config{ - PrimaryDSN: cfg.DatabasePrimary, - ReadOnlyDSN: cfg.DatabaseReadonlyReplica, + PrimaryDSN: cfg.Database.Primary, + ReadOnlyDSN: cfg.Database.ReadonlyReplica, }) if err != nil { return fmt.Errorf("unable to create db: %w", err) @@ -96,9 +96,9 @@ func Run(ctx context.Context, cfg Config) error { r.Defer(database.Close) var ch clickhouse.ClickHouse = clickhouse.NewNoop() - if cfg.ClickhouseURL != "" { + if cfg.ClickHouse.URL != "" { ch, err = clickhouse.New(clickhouse.Config{ - URL: cfg.ClickhouseURL, + URL: cfg.ClickHouse.URL, }) if err != nil { return fmt.Errorf("unable to create clickhouse: %w", err)