diff --git a/cmd/frontline/BUILD.bazel b/cmd/frontline/BUILD.bazel index 88be1fb428..24d7894dba 100644 --- a/cmd/frontline/BUILD.bazel +++ b/cmd/frontline/BUILD.bazel @@ -7,6 +7,7 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pkg/cli", + "//pkg/config", "//pkg/uid", "//svc/frontline", ], diff --git a/cmd/frontline/main.go b/cmd/frontline/main.go index 7023a1aeda..4ab72a3bdb 100644 --- a/cmd/frontline/main.go +++ b/cmd/frontline/main.go @@ -2,9 +2,9 @@ package frontline import ( "context" - "time" "github.com/unkeyed/unkey/pkg/cli" + "github.com/unkeyed/unkey/pkg/config" "github.com/unkeyed/unkey/pkg/uid" "github.com/unkeyed/unkey/svc/frontline" ) @@ -19,114 +19,19 @@ var Cmd = &cli.Command{ Name: "frontline", Usage: "Run the Unkey Frontline server (multi-tenant frontline)", Flags: []cli.Flag{ - // Server Configuration - cli.Int("http-port", "HTTP port for the Gate server to listen on. Default: 7070", - cli.Default(7070), cli.EnvVar("UNKEY_HTTP_PORT")), - - cli.Int("https-port", "HTTPS port for the Gate server to listen on. Default: 7443", - cli.Default(7443), cli.EnvVar("UNKEY_HTTPS_PORT")), - - cli.Bool("tls-enabled", "Enable TLS termination for the frontline. Default: true", - cli.Default(true), cli.EnvVar("UNKEY_TLS_ENABLED")), - - cli.String("tls-cert-file", "Path to TLS certificate file (dev mode)", - cli.EnvVar("UNKEY_TLS_CERT_FILE")), - - cli.String("tls-key-file", "Path to TLS key file (dev mode)", - cli.EnvVar("UNKEY_TLS_KEY_FILE")), - - cli.String("region", "The cloud region with platform, e.g. us-east-1.aws", - cli.Required(), - cli.EnvVar("UNKEY_REGION"), - ), - - cli.String("frontline-id", "Unique identifier for this instance. Auto-generated if not provided.", - cli.Default(uid.New("frontline", 4)), cli.EnvVar("UNKEY_GATE_ID")), - - cli.String("default-cert-domain", "Domain to use for fallback TLS certificate when a domain has no cert configured", - cli.EnvVar("UNKEY_DEFAULT_CERT_DOMAIN")), - - cli.String("apex-domain", "Apex domain for region routing. Cross-region requests forwarded to frontline.{region}.{apex-domain}. Example: unkey.cloud", - cli.Default("unkey.cloud"), cli.EnvVar("UNKEY_APEX_DOMAIN")), - - // Database Configuration - Partitioned (for hostname lookups) - cli.String("database-primary", "MySQL connection string for partitioned primary database (frontline operations). Required. Example: user:pass@host:3306/unkey?parseTime=true", - cli.Required(), cli.EnvVar("UNKEY_DATABASE_PRIMARY")), - - cli.String("database-replica", "MySQL connection string for partitioned read-replica (frontline operations). Format same as database-primary.", - cli.EnvVar("UNKEY_DATABASE_REPLICA")), - - // 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). Only used when --otel is provided. 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")), - - // Vault Configuration - cli.String("vault-url", "URL of the remote vault service (e.g., http://vault:8080)", - cli.EnvVar("UNKEY_VAULT_URL")), - cli.String("vault-token", "Authentication token for the vault service", - cli.EnvVar("UNKEY_VAULT_TOKEN")), - - cli.Int("max-hops", "Maximum number of hops allowed for a request", - cli.Default(10), cli.EnvVar("UNKEY_MAX_HOPS")), - - cli.String("ctrl-addr", "Address of the control plane", - cli.Default("localhost:8080"), cli.EnvVar("UNKEY_CTRL_ADDR")), - - // 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 := frontline.Config{ - // Basic configuration - FrontlineID: cmd.String("frontline-id"), - Image: cmd.String("image"), - Region: cmd.String("region"), - - // HTTP configuration - HttpPort: cmd.Int("http-port"), - HttpsPort: cmd.Int("https-port"), - - // TLS configuration - EnableTLS: cmd.Bool("tls-enabled"), - TLSCertFile: cmd.String("tls-cert-file"), - TLSKeyFile: cmd.String("tls-key-file"), - ApexDomain: cmd.String("apex-domain"), - MaxHops: cmd.Int("max-hops"), - - // Control Plane Configuration - CtrlAddr: cmd.String("ctrl-addr"), - - // Partitioned Database configuration (for hostname lookups) - DatabasePrimary: cmd.String("database-primary"), - DatabaseReadonlyReplica: cmd.String("database-replica"), - - // OpenTelemetry configuration - OtelEnabled: cmd.Bool("otel"), - OtelTraceSamplingRate: cmd.Float("otel-trace-sampling-rate"), - PrometheusPort: cmd.Int("prometheus-port"), - - // Vault configuration - VaultURL: cmd.String("vault-url"), - VaultToken: cmd.String("vault-token"), - - // Logging sampler configuration - LogSampleRate: cmd.Float("log-sample-rate"), - LogSlowThreshold: cmd.Duration("log-slow-threshold"), - } - - err := config.Validate() + cfg, err := config.Load[frontline.Config](cmd.String("config")) if err != nil { - return err + return cli.Exit("Failed to load config: "+err.Error(), 1) } - return frontline.Run(ctx, config) + cfg.FrontlineID = uid.New("frontline", 4) + + return frontline.Run(ctx, cfg) } diff --git a/cmd/krane/BUILD.bazel b/cmd/krane/BUILD.bazel index 9a4d2d0fd0..70eefc9770 100644 --- a/cmd/krane/BUILD.bazel +++ b/cmd/krane/BUILD.bazel @@ -7,7 +7,8 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pkg/cli", - "//pkg/uid", + "//pkg/clock", + "//pkg/config", "//svc/krane", ], ) diff --git a/cmd/krane/main.go b/cmd/krane/main.go index 2ea11683d3..b469c8a4e6 100644 --- a/cmd/krane/main.go +++ b/cmd/krane/main.go @@ -2,10 +2,10 @@ package krane import ( "context" - "time" "github.com/unkeyed/unkey/pkg/cli" - "github.com/unkeyed/unkey/pkg/uid" + "github.com/unkeyed/unkey/pkg/clock" + "github.com/unkeyed/unkey/pkg/config" "github.com/unkeyed/unkey/svc/krane" ) @@ -21,113 +21,21 @@ var Cmd = &cli.Command{ It manages the lifecycle of deployments in a kubernetes cluster: EXAMPLES: -unkey run krane # Run with default configuration`, + unkey run krane --config /etc/unkey/krane.toml`, Flags: []cli.Flag{ - // Server Configuration - cli.String("control-plane-url", - "URL of the control plane to connect to", - cli.Default("https://control.unkey.cloud"), - cli.EnvVar("UNKEY_CONTROL_PLANE_URL"), - ), - cli.String("control-plane-bearer", - "Bearer token for authenticating with the control plane", - cli.Default(""), - cli.EnvVar("UNKEY_CONTROL_PLANE_BEARER"), - ), - - // Instance Identification - cli.String("instance-id", - "Unique identifier for this instance. Auto-generated if not provided.", - cli.Default(uid.New(uid.InstancePrefix, 4)), - cli.EnvVar("UNKEY_INSTANCE_ID"), - ), - cli.String("region", - "The cloud region with platform, e.g. us-east-1.aws", - cli.Required(), - cli.EnvVar("UNKEY_REGION"), - ), - - cli.String("registry-url", - "URL of the container registry for pulling images. Example: registry.depot.dev", - cli.EnvVar("UNKEY_REGISTRY_URL"), - ), - - cli.String("registry-username", - "Username for authenticating with the container registry.", - cli.EnvVar("UNKEY_REGISTRY_USERNAME"), - ), - - cli.String("registry-password", - "Password/token for authenticating with the container registry.", - cli.EnvVar("UNKEY_REGISTRY_PASSWORD"), - ), - - cli.Int("prometheus-port", - "Port for Prometheus metrics, set to 0 to disable.", - cli.Default(0), - cli.EnvVar("UNKEY_PROMETHEUS_PORT")), - - cli.Int("rpc-port", - "Port for RPC server", - cli.Default(8070), - cli.EnvVar("UNKEY_RPC_PORT")), - - // Vault Configuration - cli.String("vault-url", "URL of the vault service", - cli.EnvVar("UNKEY_VAULT_URL")), - cli.String("vault-token", "Authentication token for the vault service", - cli.EnvVar("UNKEY_VAULT_TOKEN")), - - cli.String("cluster-id", "ID of the cluster", - cli.Default("local"), - cli.EnvVar("UNKEY_CLUSTER_ID")), - - // Observability - cli.Bool("otel-enabled", "Enable OpenTelemetry tracing and logging", - cli.Default(false), - cli.EnvVar("UNKEY_OTEL_ENABLED")), - cli.Float("otel-trace-sampling-rate", "Sampling rate for traces (0.0 to 1.0)", - cli.Default(0.01), - cli.EnvVar("UNKEY_OTEL_TRACE_SAMPLING_RATE")), - - // 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 := krane.Config{ - Clock: nil, - Region: cmd.RequireString("region"), - InstanceID: cmd.RequireString("instance-id"), - RegistryURL: cmd.RequireString("registry-url"), - RegistryUsername: cmd.RequireString("registry-username"), - RegistryPassword: cmd.RequireString("registry-password"), - RPCPort: cmd.RequireInt("rpc-port"), - VaultURL: cmd.String("vault-url"), - VaultToken: cmd.String("vault-token"), - PrometheusPort: cmd.RequireInt("prometheus-port"), - ControlPlaneURL: cmd.RequireString("control-plane-url"), - ControlPlaneBearer: cmd.RequireString("control-plane-bearer"), - OtelEnabled: cmd.Bool("otel-enabled"), - OtelTraceSamplingRate: cmd.Float("otel-trace-sampling-rate"), - - // Logging sampler configuration - LogSampleRate: cmd.Float("log-sample-rate"), - LogSlowThreshold: cmd.Duration("log-slow-threshold"), - } - - // Validate configuration - err := config.Validate() + cfg, err := config.Load[krane.Config](cmd.String("config")) if err != nil { - return cli.Exit("Invalid configuration: "+err.Error(), 1) + return cli.Exit("Failed to load config: "+err.Error(), 1) } - // Run krane - return krane.Run(ctx, config) + cfg.Clock = clock.New() + + return krane.Run(ctx, cfg) } 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/config/krane.toml b/dev/config/krane.toml new file mode 100644 index 0000000000..915dbd841f --- /dev/null +++ b/dev/config/krane.toml @@ -0,0 +1,17 @@ +region = "local.dev" + +[control_plane] +url = "http://ctrl-api:7091" +bearer = "your-local-dev-key" + +[vault] +url = "http://vault:8060" +token = "vault-test-token-123" + +[registry] +url = "${UNKEY_REGISTRY_URL}" +username = "${UNKEY_REGISTRY_USERNAME}" +password = "${UNKEY_REGISTRY_PASSWORD}" + +[otel] +enabled = false 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/frontline.yaml b/dev/k8s/manifests/frontline.yaml index c8f27b8ccb..796ab47626 100644 --- a/dev/k8s/manifests/frontline.yaml +++ b/dev/k8s/manifests/frontline.yaml @@ -1,4 +1,32 @@ --- +apiVersion: v1 +kind: ConfigMap +metadata: + name: frontline-config + namespace: unkey +data: + unkey.toml: | + region = "local.dev" + http_port = 7070 + https_port = 7443 + apex_domain = "unkey.local" + ctrl_addr = "http://ctrl-api:7091" + + [tls] + enabled = true + cert_file = "/certs/unkey.local.crt" + key_file = "/certs/unkey.local.key" + + [database] + primary = "unkey:password@tcp(mysql:3306)/unkey?parseTime=true&interpolateParams=true" + + [otel] + enabled = false + + [vault] + url = "http://vault:8060" + token = "vault-test-token-123" +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -19,39 +47,17 @@ spec: containers: - name: frontline image: unkey/go:latest - args: ["run", "frontline"] + args: ["run", "frontline", "--config", "/etc/unkey/unkey.toml"] imagePullPolicy: Never ports: - containerPort: 7070 name: http - containerPort: 7443 name: https - env: - - name: UNKEY_HTTP_PORT - value: "7070" - - name: UNKEY_HTTPS_PORT - value: "7443" - - name: UNKEY_TLS_ENABLED - value: "true" - - name: UNKEY_TLS_CERT_FILE - value: "/certs/unkey.local.crt" - - name: UNKEY_TLS_KEY_FILE - value: "/certs/unkey.local.key" - - name: UNKEY_REGION - value: "local.dev" - - name: UNKEY_APEX_DOMAIN - value: "unkey.local" - - name: UNKEY_DATABASE_PRIMARY - value: "unkey:password@tcp(mysql:3306)/unkey?parseTime=true&interpolateParams=true" - - name: UNKEY_CTRL_ADDR - value: "http://ctrl-api:7091" - - name: UNKEY_VAULT_URL - value: "http://vault:8060" - - name: UNKEY_VAULT_TOKEN - value: "vault-test-token-123" - - name: UNKEY_OTEL - value: "false" volumeMounts: + - name: config + mountPath: /etc/unkey + readOnly: true - name: tls-certs mountPath: /certs readOnly: true @@ -68,6 +74,9 @@ spec: initialDelaySeconds: 10 periodSeconds: 10 volumes: + - name: config + configMap: + name: frontline-config - name: tls-certs configMap: name: frontline-certs diff --git a/dev/k8s/manifests/krane.yaml b/dev/k8s/manifests/krane.yaml index c22114e78c..3e08325ed0 100644 --- a/dev/k8s/manifests/krane.yaml +++ b/dev/k8s/manifests/krane.yaml @@ -1,4 +1,24 @@ apiVersion: v1 +kind: ConfigMap +metadata: + name: krane-config + namespace: unkey + labels: + app: krane + component: krane +data: + unkey.toml: | + region = "local.dev" + + [control_plane] + url = "http://ctrl-api:7091" + bearer = "your-local-dev-key" + + [vault] + url = "http://vault:8060" + token = "vault-test-token-123" +--- +apiVersion: v1 kind: Service metadata: name: krane @@ -37,7 +57,7 @@ spec: containers: - name: krane image: unkey/go:latest - args: ["run", "krane"] + args: ["run", "krane", "--config", "/etc/unkey/unkey.toml"] imagePullPolicy: Never # Use local images ports: - containerPort: 8070 @@ -53,16 +73,11 @@ spec: port: 8070 initialDelaySeconds: 10 periodSeconds: 10 - env: - # Server configuration - - name: UNKEY_REGION - value: "local.dev" - - name: "UNKEY_CONTROL_PLANE_URL" - value: "http://ctrl-api:7091" - - name: "UNKEY_CONTROL_PLANE_BEARER" - value: "your-local-dev-key" - # Vault configuration for SecretsService - - name: UNKEY_VAULT_URL - value: "http://vault:8060" - - name: UNKEY_VAULT_TOKEN - value: "vault-test-token-123" + volumeMounts: + - name: config + mountPath: /etc/unkey + readOnly: true + volumes: + - name: config + configMap: + name: krane-config 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/frontline/BUILD.bazel b/svc/frontline/BUILD.bazel index f0544ab1ed..ec7a44ff6e 100644 --- a/svc/frontline/BUILD.bazel +++ b/svc/frontline/BUILD.bazel @@ -12,6 +12,7 @@ go_library( "//gen/proto/ctrl/v1/ctrlv1connect", "//gen/proto/vault/v1/vaultv1connect", "//pkg/clock", + "//pkg/config", "//pkg/db", "//pkg/logger", "//pkg/otel", diff --git a/svc/frontline/config.go b/svc/frontline/config.go index 3fd289dcf6..03a6f06aa6 100644 --- a/svc/frontline/config.go +++ b/svc/frontline/config.go @@ -1,87 +1,91 @@ package frontline -import "time" +import ( + "fmt" + + "github.com/unkeyed/unkey/pkg/config" +) + +// TLSConfig controls TLS termination for the frontline server. When Enabled +// is true and both CertFile and KeyFile are set, the server uses static +// file-based TLS (dev mode). When only Enabled is true and a vault/cert +// manager is available, dynamic certificates are used (production mode). +type TLSConfig struct { + // Enabled activates TLS termination. Defaults to true. + Enabled bool `toml:"enabled" config:"default=true"` + + // CertFile is the filesystem path to a PEM-encoded TLS certificate. + // Used together with KeyFile for static file-based TLS (dev mode). + CertFile string `toml:"cert_file"` + + // KeyFile is the filesystem path to a PEM-encoded TLS private key. + // Used together with CertFile for static file-based TLS (dev mode). + KeyFile string `toml:"key_file"` +} +// Config holds the complete configuration for the frontline server. It is +// designed to be loaded from a TOML file using [config.Load]: +// +// cfg, err := config.Load[frontline.Config]("/etc/unkey/frontline.toml") +// +// FrontlineID and Image are runtime-only fields set programmatically after +// loading and tagged toml:"-". type Config struct { - // FrontlineID is the unique identifier for this instance of the Frontline server - FrontlineID string + // FrontlineID is the unique identifier for this instance of the frontline + // server. Set at runtime; not read from the config file. + FrontlineID string `toml:"-"` - // Image specifies the container image identifier including repository and tag - Image string + // Image is the container image identifier including repository and tag. + // Set at runtime; not read from the config file. + Image string `toml:"-"` - // HttpPort defines the HTTP port for the Gate server to listen on (default: 7070) - HttpPort int + // HttpPort is the TCP port the HTTP challenge server binds to. + HttpPort int `toml:"http_port" config:"default=7070,min=1,max=65535"` - // HttpsPort defines the HTTPS port for the Gate server to listen on (default: 7443) - HttpsPort int + // HttpsPort is the TCP port the HTTPS frontline server binds to. + HttpsPort int `toml:"https_port" config:"default=7443,min=1,max=65535"` // Region identifies the geographic region where this node is deployed. - // Used for observability, latency optimization, and compliance requirements. - // Must match the region identifier used by the underlying cloud platform - // and control plane configuration. - Region string - - // EnableTLS specifies whether TLS should be enabled for the Frontline server - EnableTLS bool - - // TLSCertFile is the path to a static TLS certificate file (for dev mode) - // When set along with TLSKeyFile, frontline uses file-based TLS instead of dynamic certs - TLSCertFile string - - // TLSKeyFile is the path to a static TLS key file (for dev mode) - // When set along with TLSCertFile, frontline uses file-based TLS instead of dynamic certs - TLSKeyFile string - - // ApexDomain is the apex domain for region routing (e.g., unkey.cloud) - // Cross-region requests are forwarded to frontline.{region}.{ApexDomain} - // Example: frontline.us-east-1.aws.unkey.cloud - ApexDomain string - - // MaxHops is the maximum number of frontline hops allowed before rejecting the request - // This prevents infinite routing loops. Default: 3 - MaxHops int - - // -- Control Plane Configuration --- - - // CtrlAddr is the address of the control plane (e.g., control.unkey.com) - CtrlAddr string - - // --- Database configuration --- - - // DatabasePrimary is the primary database connection string for read and write operations - DatabasePrimary string - - // DatabaseReadonlyReplica is an optional read-replica database connection string for read operations - DatabaseReadonlyReplica string - - // --- OpenTelemetry configuration --- + // Used for observability, latency optimization, and cross-region routing. + Region string `toml:"region" config:"required"` - // OtelEnabled specifies whether OpenTelemetry tracing is enabled - OtelEnabled bool + // ApexDomain is the apex domain for region routing. Cross-region requests + // are forwarded to frontline.{region}.{ApexDomain}. + ApexDomain string `toml:"apex_domain" config:"default=unkey.cloud"` - // OtelTraceSamplingRate specifies the sampling rate for OpenTelemetry traces (0.0 - 1.0) - OtelTraceSamplingRate float64 + // MaxHops is the maximum number of frontline hops allowed before rejecting + // the request. Prevents infinite routing loops. + MaxHops int `toml:"max_hops" config:"default=10"` - // PrometheusPort specifies the port for Prometheus metrics - PrometheusPort int + // CtrlAddr is the address of the control plane service. + CtrlAddr string `toml:"ctrl_addr" config:"default=localhost:8080"` - // --- Vault Configuration --- + // PrometheusPort starts a Prometheus /metrics HTTP endpoint on the + // specified port. Set to 0 to disable. + PrometheusPort int `toml:"prometheus_port"` - // VaultURL is the URL of the remote vault service (e.g., http://vault:8080) - VaultURL string + // TLS controls TLS termination. See [TLSConfig]. + TLS TLSConfig `toml:"tls"` - // VaultToken is the authentication token for the vault service - VaultToken string + // Database configures MySQL connections. See [config.DatabaseConfig]. + Database config.DatabaseConfig `toml:"database"` - // --- 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 + // Vault configures the encryption/decryption service. See [config.VaultConfig]. + Vault config.VaultConfig `toml:"vault"` - // 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 { +// 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 { + if (c.TLS.CertFile == "") != (c.TLS.KeyFile == "") { + return fmt.Errorf("both tls.cert_file and tls.key_file must be provided together") + } return nil } diff --git a/svc/frontline/run.go b/svc/frontline/run.go index 37606de131..ee7b3d0890 100644 --- a/svc/frontline/run.go +++ b/svc/frontline/run.go @@ -43,8 +43,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, }) // Create cached clock with millisecond resolution for efficient time tracking @@ -52,13 +52,13 @@ func Run(ctx context.Context, cfg Config) error { // 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: "frontline", Version: version.Version, InstanceID: cfg.FrontlineID, CloudRegion: cfg.Region, - TraceSampleRate: cfg.OtelTraceSamplingRate, + TraceSampleRate: cfg.Otel.TraceSamplingRate, }) if err != nil { return fmt.Errorf("unable to init grafana: %w", err) @@ -105,22 +105,22 @@ func Run(ctx context.Context, cfg Config) error { } var vaultClient vaultv1connect.VaultServiceClient - if cfg.VaultURL != "" { + if cfg.Vault.URL != "" { vaultClient = vaultv1connect.NewVaultServiceClient( http.DefaultClient, - cfg.VaultURL, + cfg.Vault.URL, connect.WithInterceptors(interceptor.NewHeaderInjector(map[string]string{ - "Authorization": "Bearer " + cfg.VaultToken, + "Authorization": "Bearer " + cfg.Vault.Token, })), ) - logger.Info("Vault client initialized", "url", cfg.VaultURL) + logger.Info("Vault client initialized", "url", cfg.Vault.URL) } else { logger.Warn("Vault not configured - TLS certificate decryption will be unavailable") } db, 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 partitioned db: %w", err) @@ -172,17 +172,17 @@ func Run(ctx context.Context, cfg Config) error { // Create TLS config - either from static files (dev mode) or dynamic certificates (production) var tlsConfig *pkgtls.Config - if cfg.EnableTLS { - if cfg.TLSCertFile != "" && cfg.TLSKeyFile != "" { + if cfg.TLS.Enabled { + if cfg.TLS.CertFile != "" && cfg.TLS.KeyFile != "" { // Dev mode: static file-based certificate - fileTLSConfig, tlsErr := pkgtls.NewFromFiles(cfg.TLSCertFile, cfg.TLSKeyFile) + fileTLSConfig, tlsErr := pkgtls.NewFromFiles(cfg.TLS.CertFile, cfg.TLS.KeyFile) if tlsErr != nil { return fmt.Errorf("failed to load TLS certificate from files: %w", tlsErr) } tlsConfig = fileTLSConfig logger.Info("TLS configured with static certificate files", - "certFile", cfg.TLSCertFile, - "keyFile", cfg.TLSKeyFile) + "certFile", cfg.TLS.CertFile, + "keyFile", cfg.TLS.KeyFile) } else if certManager != nil { // Production mode: dynamic certificates from database/vault //nolint:exhaustruct diff --git a/svc/krane/BUILD.bazel b/svc/krane/BUILD.bazel index dc77a50c05..ac97d0d573 100644 --- a/svc/krane/BUILD.bazel +++ b/svc/krane/BUILD.bazel @@ -13,6 +13,7 @@ go_library( "//gen/proto/krane/v1/kranev1connect", "//gen/proto/vault/v1/vaultv1connect", "//pkg/clock", + "//pkg/config", "//pkg/logger", "//pkg/otel", "//pkg/prometheus", diff --git a/svc/krane/config.go b/svc/krane/config.go index df5e3d2e59..bbfd39b1dd 100644 --- a/svc/krane/config.go +++ b/svc/krane/config.go @@ -1,94 +1,82 @@ package krane import ( - "time" - "github.com/unkeyed/unkey/pkg/clock" + "github.com/unkeyed/unkey/pkg/config" ) -// Config holds configuration for the krane agent server. +// RegistryConfig holds credentials for the container image registry used when +// pulling deployment images. All fields are optional; when URL is empty, the +// default registry configured on the cluster is used. +type RegistryConfig struct { + // URL is the container registry endpoint (e.g. "registry.depot.dev"). + URL string `toml:"url"` + + // Username is the registry authentication username (e.g. "x-token"). + Username string `toml:"username"` + + // Password is the registry authentication password or token. + Password string `toml:"password"` +} + +// ControlPlaneConfig configures the connection to the control plane that streams +// deployment and sentinel state to krane agents. +type ControlPlaneConfig struct { + // URL is the control plane endpoint. + URL string `toml:"url" config:"default=https://control.unkey.cloud"` + + // Bearer is the authentication token for the control plane API. + Bearer string `toml:"bearer" config:"required,nonempty"` +} + +// Config holds the complete configuration for the krane agent. It is designed +// to be loaded from a TOML file using [config.Load]: +// +// cfg, err := config.Load[krane.Config]("/etc/unkey/krane.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]. // -// This configuration defines how the krane agent connects to Kubernetes, -// authenticates with container registries, handles secrets, and exposes metrics. +// The Clock field is runtime-only and cannot be set through a config file. type Config struct { // InstanceID is the unique identifier for this krane agent instance. - // Used for distributed tracing, logging correlation, and cluster coordination. - // Must be unique across all running krane instances in the same cluster. - InstanceID string + InstanceID string `toml:"instance_id"` // Region identifies the geographic region where this node is deployed. - // Used for observability, latency optimization, and compliance requirements. - // Must match the region identifier used by the underlying cloud platform - // and control plane configuration. - Region string - - // RegistryURL is the URL of the container registry for pulling images. - // Should include the protocol and registry domain, e.g., "registry.depot.dev" - // or "https://registry.example.com". Used by all deployments unless overridden. - RegistryURL string - - // RegistryUsername is the username for authenticating with the container registry. - // Common values include "x-token" for token-based authentication or the - // actual registry username. Must be paired with RegistryPassword. - RegistryUsername string - - // RegistryPassword is the password or token for authenticating with the container registry. - // Should be stored securely (e.g., environment variable or secret management system). - // For token-based auth, this is the actual token value. - RegistryPassword string - - // Clock provides time operations for testing and time zone handling. - // Use clock.RealClock{} for production deployments and mock clocks for - // deterministic testing. Enables time-based operations to be controlled in tests. - Clock clock.Clock - - // PrometheusPort specifies the port for exposing Prometheus metrics. - // Set to 0 to disable metrics exposure. When enabled, metrics are served - // on all interfaces (0.0.0.0) on the specified port. - PrometheusPort int - - // VaultURL is the URL of the remote vault service (e.g., http://vault:8080). - // Required for decrypting environment variable secrets. - VaultURL string - - // VaultToken is the authentication token for the vault service. - // Used to authenticate requests to the vault API. - VaultToken string - - // RPCPort specifies the port for the gRPC server that exposes krane APIs. - // The SchedulerService and optionally SecretsService are served on this port. - // Must be a valid port number (1-65535). - RPCPort int - - ControlPlaneURL string - ControlPlaneBearer string - - // OtelEnabled enables OpenTelemetry instrumentation for tracing and metrics. - // When true, InitGrafana will be called to set up OTEL exporters. - OtelEnabled bool - - // OtelTraceSamplingRate controls the sampling rate for traces (0.0 to 1.0). - // Only used when OtelEnabled is true. - OtelTraceSamplingRate float64 - - // --- 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 + Region string `toml:"region" config:"required,nonempty"` + + // RPCPort is the TCP port for the gRPC server. + RPCPort int `toml:"rpc_port" config:"default=8070,min=1,max=65535"` + + // PrometheusPort starts a Prometheus /metrics endpoint on the specified + // port. Set to 0 to disable. + PrometheusPort int `toml:"prometheus_port"` + + // Registry configures container image registry access. See [RegistryConfig]. + Registry RegistryConfig `toml:"registry"` + + // Vault configures the secrets decryption service. See [config.VaultConfig]. + Vault config.VaultConfig `toml:"vault"` + + // ControlPlane configures the upstream control plane. See [ControlPlaneConfig]. + ControlPlane ControlPlaneConfig `toml:"control_plane"` + + // Otel configures OpenTelemetry export. See [config.OtelConfig]. + Otel config.OtelConfig `toml:"otel"` + + // Logging configures log sampling. See [config.LoggingConfig]. + Logging config.LoggingConfig `toml:"logging"` + + // Clock provides time operations and is injected for testability. Production + // callers set this to [clock.New]; tests can substitute a fake clock. + Clock clock.Clock `toml:"-"` } -// Validate checks the configuration for required fields and logical consistency. -// -// Returns an error if required fields are missing or configuration values are invalid. -// This method should be called before starting the krane agent to ensure -// proper configuration and provide early feedback on configuration errors. -// -// Currently, this method always returns nil as validation is not implemented. -// Future implementations will validate required fields such as RPCPort, -// RegistryURL, and consistency between VaultMasterKeys and VaultS3 configuration. -func (c Config) Validate() error { +// 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 { return nil } diff --git a/svc/krane/doc.go b/svc/krane/doc.go index a5d11ed912..ec188b2048 100644 --- a/svc/krane/doc.go +++ b/svc/krane/doc.go @@ -54,23 +54,10 @@ // // Basic krane agent setup: // -// cfg := krane.Config{ -// InstanceID: "krane-node-001", -// Region: "us-west-2", -// RegistryURL: "registry.depot.dev", -// RegistryUsername: "x-token", -// RegistryPassword: "depot-token", -// RPCPort: 8080, -// PrometheusPort: 9090, -// VaultMasterKeys: []string{"master-key-1"}, -// VaultS3: krane.S3Config{ -// URL: "https://s3.amazonaws.com", -// Bucket: "krane-vault", -// AccessKeyID: "access-key", -// AccessKeySecret: "secret-key", -// }, -// } -// err := krane.Run(context.Background(), cfg) +// cfg, err := config.Load[krane.Config]("/etc/unkey/krane.toml") +// if err != nil { ... } +// cfg.Clock = clock.New() +// err = krane.Run(context.Background(), cfg) // // The agent will: // 1. Initialize Kubernetes client using in-cluster configuration diff --git a/svc/krane/run.go b/svc/krane/run.go index 2cda5c19c0..f5a96ec136 100644 --- a/svc/krane/run.go +++ b/svc/krane/run.go @@ -57,18 +57,18 @@ 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, }) var shutdownGrafana func(context.Context) error - if cfg.OtelEnabled { + if cfg.Otel.Enabled { shutdownGrafana, err = otel.InitGrafana(ctx, otel.Config{ Application: "krane", Version: pkgversion.Version, InstanceID: cfg.InstanceID, CloudRegion: cfg.Region, - TraceSampleRate: cfg.OtelTraceSamplingRate, + TraceSampleRate: cfg.Otel.TraceSamplingRate, }) if err != nil { return fmt.Errorf("unable to init grafana: %w", err) @@ -92,8 +92,8 @@ func Run(ctx context.Context, cfg Config) error { r.DeferCtx(shutdownGrafana) cluster := controlplane.NewClient(controlplane.ClientConfig{ - URL: cfg.ControlPlaneURL, - BearerToken: cfg.ControlPlaneBearer, + URL: cfg.ControlPlane.URL, + BearerToken: cfg.ControlPlane.Bearer, Region: cfg.Region, }) @@ -149,15 +149,15 @@ func Run(ctx context.Context, cfg Config) error { // Create vault client for secrets decryption var vaultClient vaultv1connect.VaultServiceClient - if cfg.VaultURL != "" { + if cfg.Vault.URL != "" { vaultClient = vaultv1connect.NewVaultServiceClient( http.DefaultClient, - cfg.VaultURL, + cfg.Vault.URL, connect.WithInterceptors(interceptor.NewHeaderInjector(map[string]string{ - "Authorization": "Bearer " + cfg.VaultToken, + "Authorization": "Bearer " + cfg.Vault.Token, })), ) - logger.Info("Vault client initialized", "url", cfg.VaultURL) + logger.Info("Vault client initialized", "url", cfg.Vault.URL) } // Create the connect handler 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)