Add Secrets Manager resolution to probod-bootstrap
Introduce a Resolver that owns env lookup and typed parsing for probod-bootstrap. Env values prefixed with aws://<secret-id> are fetched from AWS Secrets Manager (plaintext SecretString); each secret ID is cached per run. Builder now takes a Resolver only. Prefix every probod-bootstrap input with PROBOD_ so bootstrap config does not collide with unrelated process environment (for example AWS_* used by other tooling). Secrets Manager authentication uses the standard AWS SDK default chain (AWS_REGION, IAM role, profile); PROBOD_AWS_* vars configure S3 in the generated config only. Update Helm deployment env names, GNUmakefile dev-config, Lima provision, e2e testutil, compose.prod.yaml, and docs. Deployments must rename bootstrap env vars to PROBOD_* (e.g. AUTH_COOKIE_SECRET → PROBOD_AUTH_COOKIE_SECRET). BREAKING CHANGE: all env vars are now prefixed by `PROBOD_`. Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
@@ -17,7 +17,6 @@ package bootstrap
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.probo.inc/probo/pkg/probodconfig"
|
||||
@@ -26,18 +25,18 @@ import (
|
||||
type EnvGetter func(key string) string
|
||||
|
||||
type Builder struct {
|
||||
getEnv EnvGetter
|
||||
resolver *Resolver
|
||||
samlCertificate string
|
||||
samlPrivateKey string
|
||||
oauth2SigningKey string
|
||||
}
|
||||
|
||||
func NewBuilder(getEnv EnvGetter) *Builder {
|
||||
if getEnv == nil {
|
||||
getEnv = os.Getenv
|
||||
func NewBuilder(resolver *Resolver) *Builder {
|
||||
if resolver == nil {
|
||||
resolver = NewResolver(nil)
|
||||
}
|
||||
|
||||
return &Builder{getEnv: getEnv}
|
||||
return &Builder{resolver: resolver}
|
||||
}
|
||||
|
||||
func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
||||
@@ -57,272 +56,272 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
||||
cfg := &probodconfig.FullConfig{
|
||||
Unit: probodconfig.UnitConfig{
|
||||
Metrics: probodconfig.MetricsConfig{
|
||||
Addr: b.getEnvOrDefault("METRICS_ADDR", "localhost:8081"),
|
||||
Addr: b.resolver.getEnvOrDefault("PROBOD_METRICS_ADDR", "localhost:8081"),
|
||||
},
|
||||
Tracing: probodconfig.TracingConfig{
|
||||
Addr: b.getEnvOrDefault("TRACING_ADDR", "localhost:4318"),
|
||||
MaxBatchSize: b.getEnvIntOrDefault("TRACING_MAX_BATCH_SIZE", 512),
|
||||
BatchTimeout: b.getEnvIntOrDefault("TRACING_BATCH_TIMEOUT", 5),
|
||||
ExportTimeout: b.getEnvIntOrDefault("TRACING_EXPORT_TIMEOUT", 30),
|
||||
MaxQueueSize: b.getEnvIntOrDefault("TRACING_MAX_QUEUE_SIZE", 2048),
|
||||
Addr: b.resolver.getEnvOrDefault("PROBOD_TRACING_ADDR", "localhost:4318"),
|
||||
MaxBatchSize: b.resolver.getEnvIntOrDefault("PROBOD_TRACING_MAX_BATCH_SIZE", 512),
|
||||
BatchTimeout: b.resolver.getEnvIntOrDefault("PROBOD_TRACING_BATCH_TIMEOUT", 5),
|
||||
ExportTimeout: b.resolver.getEnvIntOrDefault("PROBOD_TRACING_EXPORT_TIMEOUT", 30),
|
||||
MaxQueueSize: b.resolver.getEnvIntOrDefault("PROBOD_TRACING_MAX_QUEUE_SIZE", 2048),
|
||||
},
|
||||
},
|
||||
Probod: probodconfig.Config{
|
||||
BaseURL: b.getEnvOrDefault("PROBOD_BASE_URL", "http://localhost:8080"),
|
||||
EncryptionKey: b.getEnv("PROBOD_ENCRYPTION_KEY"),
|
||||
ChromeDPAddr: b.getEnvOrDefault("CHROME_DP_ADDR", "localhost:9222"),
|
||||
BaseURL: b.resolver.getEnvOrDefault("PROBOD_BASE_URL", "http://localhost:8080"),
|
||||
EncryptionKey: b.resolver.getEnv("PROBOD_ENCRYPTION_KEY"),
|
||||
ChromeDPAddr: b.resolver.getEnvOrDefault("PROBOD_CHROME_DP_ADDR", "localhost:9222"),
|
||||
Api: probodconfig.APIConfig{
|
||||
Addr: b.getEnvOrDefault("API_ADDR", ":8080"),
|
||||
Addr: b.resolver.getEnvOrDefault("PROBOD_API_ADDR", ":8080"),
|
||||
ProxyProtocol: probodconfig.ProxyProtocolConfig{
|
||||
TrustedProxies: b.parseOriginsList(b.getEnv("API_PROXY_PROTOCOL_TRUSTED_PROXIES")),
|
||||
TrustedProxies: b.parseOriginsList(b.resolver.getEnv("PROBOD_API_PROXY_PROTOCOL_TRUSTED_PROXIES")),
|
||||
},
|
||||
Cors: probodconfig.CorsConfig{
|
||||
AllowedOrigins: b.parseOriginsList(b.getEnvOrDefault("API_CORS_ALLOWED_ORIGINS", "http://localhost:8080")),
|
||||
AllowedOrigins: b.parseOriginsList(b.resolver.getEnvOrDefault("PROBOD_API_CORS_ALLOWED_ORIGINS", "http://localhost:8080")),
|
||||
},
|
||||
ExtraHeaderFields: make(map[string]string),
|
||||
},
|
||||
Pg: probodconfig.PgConfig{
|
||||
Addr: b.getEnvOrDefault("PG_ADDR", "localhost:5432"),
|
||||
Username: b.getEnvOrDefault("PG_USERNAME", "probod"),
|
||||
Password: b.getEnvOrDefault("PG_PASSWORD", "probod"),
|
||||
Database: b.getEnvOrDefault("PG_DATABASE", "probod"),
|
||||
PoolSize: int32(b.getEnvIntOrDefault("PG_POOL_SIZE", 100)),
|
||||
MinPoolSize: int32(b.getEnvIntOrDefault("PG_MIN_POOL_SIZE", 10)),
|
||||
MaxConnIdleTimeSeconds: b.getEnvIntOrDefault("PG_MAX_CONN_IDLE_TIME_SECONDS", 1800),
|
||||
MaxConnLifetimeSeconds: b.getEnvIntOrDefault("PG_MAX_CONN_LIFETIME_SECONDS", 3600),
|
||||
MaxConnLifetimeJitterSeconds: b.getEnvIntOrDefault("PG_MAX_CONN_LIFETIME_JITTER_SECONDS", 300),
|
||||
HealthCheckPeriodSeconds: b.getEnvIntOrDefault("PG_HEALTH_CHECK_PERIOD_SECONDS", 60),
|
||||
Addr: b.resolver.getEnvOrDefault("PROBOD_PG_ADDR", "localhost:5432"),
|
||||
Username: b.resolver.getEnvOrDefault("PROBOD_PG_USERNAME", "probod"),
|
||||
Password: b.resolver.getEnvOrDefault("PROBOD_PG_PASSWORD", "probod"),
|
||||
Database: b.resolver.getEnvOrDefault("PROBOD_PG_DATABASE", "probod"),
|
||||
PoolSize: int32(b.resolver.getEnvIntOrDefault("PROBOD_PG_POOL_SIZE", 100)),
|
||||
MinPoolSize: int32(b.resolver.getEnvIntOrDefault("PROBOD_PG_MIN_POOL_SIZE", 10)),
|
||||
MaxConnIdleTimeSeconds: b.resolver.getEnvIntOrDefault("PROBOD_PG_MAX_CONN_IDLE_TIME_SECONDS", 1800),
|
||||
MaxConnLifetimeSeconds: b.resolver.getEnvIntOrDefault("PROBOD_PG_MAX_CONN_LIFETIME_SECONDS", 3600),
|
||||
MaxConnLifetimeJitterSeconds: b.resolver.getEnvIntOrDefault("PROBOD_PG_MAX_CONN_LIFETIME_JITTER_SECONDS", 300),
|
||||
HealthCheckPeriodSeconds: b.resolver.getEnvIntOrDefault("PROBOD_PG_HEALTH_CHECK_PERIOD_SECONDS", 60),
|
||||
CACertBundle: pgCACertBundle,
|
||||
Debug: b.getEnvBoolOrDefault("PG_DEBUG", false),
|
||||
Debug: b.resolver.getEnvBoolOrDefault("PROBOD_PG_DEBUG", false),
|
||||
},
|
||||
Auth: probodconfig.AuthConfig{
|
||||
DisableSignup: b.getEnvBoolOrDefault("AUTH_DISABLE_SIGNUP", false),
|
||||
InvitationConfirmationTokenValidity: b.getEnvIntOrDefault("AUTH_INVITATION_TOKEN_VALIDITY", 3600),
|
||||
PasswordResetTokenValidity: b.getEnvIntOrDefault("AUTH_PASSWORD_RESET_TOKEN_VALIDITY", 3600),
|
||||
MagicLinkTokenValidity: b.getEnvIntOrDefault("AUTH_MAGIC_LINK_TOKEN_VALIDITY", 900),
|
||||
DisableSignup: b.resolver.getEnvBoolOrDefault("PROBOD_AUTH_DISABLE_SIGNUP", false),
|
||||
InvitationConfirmationTokenValidity: b.resolver.getEnvIntOrDefault("PROBOD_AUTH_INVITATION_TOKEN_VALIDITY", 3600),
|
||||
PasswordResetTokenValidity: b.resolver.getEnvIntOrDefault("PROBOD_AUTH_PASSWORD_RESET_TOKEN_VALIDITY", 3600),
|
||||
MagicLinkTokenValidity: b.resolver.getEnvIntOrDefault("PROBOD_AUTH_MAGIC_LINK_TOKEN_VALIDITY", 900),
|
||||
Cookie: probodconfig.CookieConfig{
|
||||
Name: b.getEnvOrDefault("AUTH_COOKIE_NAME", "SSID"),
|
||||
Domain: b.getEnvOrDefault("AUTH_COOKIE_DOMAIN", "localhost"),
|
||||
Secret: b.getEnv("AUTH_COOKIE_SECRET"),
|
||||
Duration: b.getEnvIntOrDefault("AUTH_COOKIE_DURATION", 24),
|
||||
Secure: b.getEnvBoolOrDefault("AUTH_COOKIE_SECURE", true),
|
||||
Name: b.resolver.getEnvOrDefault("PROBOD_AUTH_COOKIE_NAME", "SSID"),
|
||||
Domain: b.resolver.getEnvOrDefault("PROBOD_AUTH_COOKIE_DOMAIN", "localhost"),
|
||||
Secret: b.resolver.getEnv("PROBOD_AUTH_COOKIE_SECRET"),
|
||||
Duration: b.resolver.getEnvIntOrDefault("PROBOD_AUTH_COOKIE_DURATION", 24),
|
||||
Secure: b.resolver.getEnvBoolOrDefault("PROBOD_AUTH_COOKIE_SECURE", true),
|
||||
},
|
||||
Password: probodconfig.PasswordConfig{
|
||||
Pepper: b.getEnv("AUTH_PASSWORD_PEPPER"),
|
||||
Iterations: b.getEnvIntOrDefault("AUTH_PASSWORD_ITERATIONS", 1000000),
|
||||
Pepper: b.resolver.getEnv("PROBOD_AUTH_PASSWORD_PEPPER"),
|
||||
Iterations: b.resolver.getEnvIntOrDefault("PROBOD_AUTH_PASSWORD_ITERATIONS", 1000000),
|
||||
},
|
||||
SAML: probodconfig.SAMLConfig{
|
||||
SessionDuration: b.getEnvIntOrDefault("SAML_SESSION_DURATION", 604800),
|
||||
CleanupIntervalSeconds: b.getEnvIntOrDefault("SAML_CLEANUP_INTERVAL_SECONDS", 0),
|
||||
SessionDuration: b.resolver.getEnvIntOrDefault("PROBOD_SAML_SESSION_DURATION", 604800),
|
||||
CleanupIntervalSeconds: b.resolver.getEnvIntOrDefault("PROBOD_SAML_CLEANUP_INTERVAL_SECONDS", 0),
|
||||
Certificate: samlCert,
|
||||
PrivateKey: samlKey,
|
||||
DomainVerificationIntervalSeconds: b.getEnvIntOrDefault("SAML_DOMAIN_VERIFICATION_INTERVAL_SECONDS", 60),
|
||||
DomainVerificationResolverAddr: b.getEnvOrDefault("SAML_DOMAIN_VERIFICATION_RESOLVER_ADDR", "8.8.8.8:53"),
|
||||
DomainVerificationIntervalSeconds: b.resolver.getEnvIntOrDefault("PROBOD_SAML_DOMAIN_VERIFICATION_INTERVAL_SECONDS", 60),
|
||||
DomainVerificationResolverAddr: b.resolver.getEnvOrDefault("PROBOD_SAML_DOMAIN_VERIFICATION_RESOLVER_ADDR", "8.8.8.8:53"),
|
||||
},
|
||||
Google: probodconfig.OIDCProviderConfig{
|
||||
ClientID: b.getEnv("AUTH_GOOGLE_CLIENT_ID"),
|
||||
ClientSecret: b.getEnv("AUTH_GOOGLE_CLIENT_SECRET"),
|
||||
Enabled: b.getEnv("AUTH_GOOGLE_CLIENT_ID") != "" && b.getEnv("AUTH_GOOGLE_CLIENT_SECRET") != "",
|
||||
ClientID: b.resolver.getEnv("PROBOD_AUTH_GOOGLE_CLIENT_ID"),
|
||||
ClientSecret: b.resolver.getEnv("PROBOD_AUTH_GOOGLE_CLIENT_SECRET"),
|
||||
Enabled: b.resolver.getEnv("PROBOD_AUTH_GOOGLE_CLIENT_ID") != "" && b.resolver.getEnv("PROBOD_AUTH_GOOGLE_CLIENT_SECRET") != "",
|
||||
},
|
||||
Microsoft: probodconfig.OIDCProviderConfig{
|
||||
ClientID: b.getEnv("AUTH_MICROSOFT_CLIENT_ID"),
|
||||
ClientSecret: b.getEnv("AUTH_MICROSOFT_CLIENT_SECRET"),
|
||||
Enabled: b.getEnv("AUTH_MICROSOFT_CLIENT_ID") != "" && b.getEnv("AUTH_MICROSOFT_CLIENT_SECRET") != "",
|
||||
ClientID: b.resolver.getEnv("PROBOD_AUTH_MICROSOFT_CLIENT_ID"),
|
||||
ClientSecret: b.resolver.getEnv("PROBOD_AUTH_MICROSOFT_CLIENT_SECRET"),
|
||||
Enabled: b.resolver.getEnv("PROBOD_AUTH_MICROSOFT_CLIENT_ID") != "" && b.resolver.getEnv("PROBOD_AUTH_MICROSOFT_CLIENT_SECRET") != "",
|
||||
},
|
||||
OAuth2Server: probodconfig.OAuth2ServerConfig{
|
||||
SigningKeys: []probodconfig.OAuth2SigningKeyConfig{{
|
||||
PrivateKey: oauth2SigningKey,
|
||||
KID: b.getEnvOrDefault("OAUTH2_SERVER_SIGNING_KEY_KID", "default"),
|
||||
KID: b.resolver.getEnvOrDefault("PROBOD_OAUTH2_SERVER_SIGNING_KEY_KID", "default"),
|
||||
Active: true,
|
||||
}},
|
||||
AccessTokenDuration: b.getEnvIntOrDefault("OAUTH2_SERVER_ACCESS_TOKEN_DURATION", 3600),
|
||||
RefreshTokenDuration: b.getEnvIntOrDefault("OAUTH2_SERVER_REFRESH_TOKEN_DURATION", 2592000),
|
||||
AuthorizationCodeDuration: b.getEnvIntOrDefault("OAUTH2_SERVER_AUTHORIZATION_CODE_DURATION", 600),
|
||||
DeviceCodeDuration: b.getEnvIntOrDefault("OAUTH2_SERVER_DEVICE_CODE_DURATION", 600),
|
||||
AccessTokenDuration: b.resolver.getEnvIntOrDefault("PROBOD_OAUTH2_SERVER_ACCESS_TOKEN_DURATION", 3600),
|
||||
RefreshTokenDuration: b.resolver.getEnvIntOrDefault("PROBOD_OAUTH2_SERVER_REFRESH_TOKEN_DURATION", 2592000),
|
||||
AuthorizationCodeDuration: b.resolver.getEnvIntOrDefault("PROBOD_OAUTH2_SERVER_AUTHORIZATION_CODE_DURATION", 600),
|
||||
DeviceCodeDuration: b.resolver.getEnvIntOrDefault("PROBOD_OAUTH2_SERVER_DEVICE_CODE_DURATION", 600),
|
||||
CIMDAllowedClientIDs: b.parseOriginsList(
|
||||
b.getEnv("OAUTH2_SERVER_CIMD_ALLOWED_CLIENT_IDS"),
|
||||
b.resolver.getEnv("PROBOD_OAUTH2_SERVER_CIMD_ALLOWED_CLIENT_IDS"),
|
||||
),
|
||||
},
|
||||
},
|
||||
TrustCenter: probodconfig.TrustCenterConfig{
|
||||
HTTPAddr: b.getEnvOrDefault("TRUST_CENTER_HTTP_ADDR", ":80"),
|
||||
HTTPSAddr: b.getEnvOrDefault("TRUST_CENTER_HTTPS_ADDR", ":443"),
|
||||
HTTPAddr: b.resolver.getEnvOrDefault("PROBOD_TRUST_CENTER_HTTP_ADDR", ":80"),
|
||||
HTTPSAddr: b.resolver.getEnvOrDefault("PROBOD_TRUST_CENTER_HTTPS_ADDR", ":443"),
|
||||
ProxyProtocol: probodconfig.ProxyProtocolConfig{
|
||||
TrustedProxies: b.parseOriginsList(b.getEnv("TRUST_CENTER_PROXY_PROTOCOL_TRUSTED_PROXIES")),
|
||||
TrustedProxies: b.parseOriginsList(b.resolver.getEnv("PROBOD_TRUST_CENTER_PROXY_PROTOCOL_TRUSTED_PROXIES")),
|
||||
},
|
||||
},
|
||||
AWS: probodconfig.AWSConfig{
|
||||
Region: b.getEnvOrDefault("AWS_REGION", "us-east-1"),
|
||||
Bucket: b.getEnvOrDefault("AWS_BUCKET", "probod"),
|
||||
AccessKeyID: b.getEnv("AWS_ACCESS_KEY_ID"),
|
||||
SecretAccessKey: b.getEnv("AWS_SECRET_ACCESS_KEY"),
|
||||
Endpoint: b.getEnv("AWS_ENDPOINT"),
|
||||
UsePathStyle: b.getEnvBoolOrDefault("AWS_USE_PATH_STYLE", false),
|
||||
Region: b.resolver.getEnvOrDefault("PROBOD_AWS_REGION", "us-east-1"),
|
||||
Bucket: b.resolver.getEnvOrDefault("PROBOD_AWS_BUCKET", "probod"),
|
||||
AccessKeyID: b.resolver.getEnv("PROBOD_AWS_ACCESS_KEY_ID"),
|
||||
SecretAccessKey: b.resolver.getEnv("PROBOD_AWS_SECRET_ACCESS_KEY"),
|
||||
Endpoint: b.resolver.getEnv("PROBOD_AWS_ENDPOINT"),
|
||||
UsePathStyle: b.resolver.getEnvBoolOrDefault("PROBOD_AWS_USE_PATH_STYLE", false),
|
||||
},
|
||||
Notifications: probodconfig.NotificationsConfig{
|
||||
Mailer: probodconfig.MailerConfig{
|
||||
SenderName: b.getEnvOrDefault("MAILER_SENDER_NAME", "Probo"),
|
||||
SenderEmail: b.getEnvOrDefault("MAILER_SENDER_EMAIL", "no-reply@notification.getprobo.com"),
|
||||
MailerInterval: b.getEnvIntOrDefault("MAILER_INTERVAL", 60),
|
||||
SenderName: b.resolver.getEnvOrDefault("PROBOD_MAILER_SENDER_NAME", "Probo"),
|
||||
SenderEmail: b.resolver.getEnvOrDefault("PROBOD_MAILER_SENDER_EMAIL", "no-reply@notification.getprobo.com"),
|
||||
MailerInterval: b.resolver.getEnvIntOrDefault("PROBOD_MAILER_INTERVAL", 60),
|
||||
SMTP: probodconfig.SMTPConfig{
|
||||
Addr: b.getEnvOrDefault("SMTP_ADDR", "localhost:1025"),
|
||||
User: b.getEnv("SMTP_USER"),
|
||||
Password: b.getEnv("SMTP_PASSWORD"),
|
||||
TLSRequired: b.getEnvBoolOrDefault("SMTP_TLS_REQUIRED", false),
|
||||
HelloName: b.getEnv("SMTP_HELLO_NAME"),
|
||||
Addr: b.resolver.getEnvOrDefault("PROBOD_SMTP_ADDR", "localhost:1025"),
|
||||
User: b.resolver.getEnv("PROBOD_SMTP_USER"),
|
||||
Password: b.resolver.getEnv("PROBOD_SMTP_PASSWORD"),
|
||||
TLSRequired: b.resolver.getEnvBoolOrDefault("PROBOD_SMTP_TLS_REQUIRED", false),
|
||||
HelloName: b.resolver.getEnv("PROBOD_SMTP_HELLO_NAME"),
|
||||
},
|
||||
},
|
||||
Slack: probodconfig.SlackConfig{
|
||||
SenderInterval: b.getEnvIntOrDefault("SLACK_SENDER_INTERVAL", 60),
|
||||
SigningSecret: b.getEnv("CONNECTOR_SLACK_SIGNING_SECRET"),
|
||||
SenderInterval: b.resolver.getEnvIntOrDefault("PROBOD_SLACK_SENDER_INTERVAL", 60),
|
||||
SigningSecret: b.resolver.getEnv("PROBOD_CONNECTOR_SLACK_SIGNING_SECRET"),
|
||||
},
|
||||
Webhook: probodconfig.WebhookConfig{
|
||||
SenderInterval: b.getEnvIntOrDefault("WEBHOOK_SENDER_INTERVAL", 5),
|
||||
CacheTTL: b.getEnvIntOrDefault("WEBHOOK_CACHE_TTL", 86400),
|
||||
SenderInterval: b.resolver.getEnvIntOrDefault("PROBOD_WEBHOOK_SENDER_INTERVAL", 5),
|
||||
CacheTTL: b.resolver.getEnvIntOrDefault("PROBOD_WEBHOOK_CACHE_TTL", 86400),
|
||||
},
|
||||
Document: probodconfig.DocumentNotificationConfig{
|
||||
Interval: b.getEnvIntOrDefault("DOCUMENT_NOTIFICATION_INTERVAL", 300),
|
||||
DebounceDelay: b.getEnvIntOrDefault("DOCUMENT_NOTIFICATION_DEBOUNCE_DELAY", 900),
|
||||
ReminderInterval: b.getEnvIntOrDefault("DOCUMENT_NOTIFICATION_REMINDER_INTERVAL", 86400),
|
||||
Interval: b.resolver.getEnvIntOrDefault("PROBOD_DOCUMENT_NOTIFICATION_INTERVAL", 300),
|
||||
DebounceDelay: b.resolver.getEnvIntOrDefault("PROBOD_DOCUMENT_NOTIFICATION_DEBOUNCE_DELAY", 900),
|
||||
ReminderInterval: b.resolver.getEnvIntOrDefault("PROBOD_DOCUMENT_NOTIFICATION_REMINDER_INTERVAL", 86400),
|
||||
},
|
||||
},
|
||||
Agents: probodconfig.AgentsConfig{
|
||||
Providers: map[string]probodconfig.LLMProviderConfig{
|
||||
"openai": {
|
||||
Type: "openai",
|
||||
APIKey: b.getEnv("OPENAI_API_KEY"),
|
||||
APIKey: b.resolver.getEnv("PROBOD_OPENAI_API_KEY"),
|
||||
},
|
||||
"anthropic": {
|
||||
Type: "anthropic",
|
||||
APIKey: b.getEnv("ANTHROPIC_API_KEY"),
|
||||
APIKey: b.resolver.getEnv("PROBOD_ANTHROPIC_API_KEY"),
|
||||
},
|
||||
},
|
||||
Default: probodconfig.LLMAgentConfig{
|
||||
Provider: b.getEnvOrDefault("AGENT_DEFAULT_PROVIDER", "openai"),
|
||||
ModelName: b.getEnvOrDefault("AGENT_DEFAULT_MODEL_NAME", "gpt-4o"),
|
||||
Temperature: new(b.getEnvFloatOrDefault("AGENT_DEFAULT_TEMPERATURE", 0.1)),
|
||||
MaxTokens: new(b.getEnvIntOrDefault("AGENT_DEFAULT_MAX_TOKENS", 4096)),
|
||||
Provider: b.resolver.getEnvOrDefault("PROBOD_AGENT_DEFAULT_PROVIDER", "openai"),
|
||||
ModelName: b.resolver.getEnvOrDefault("PROBOD_AGENT_DEFAULT_MODEL_NAME", "gpt-4o"),
|
||||
Temperature: new(b.resolver.getEnvFloatOrDefault("PROBOD_AGENT_DEFAULT_TEMPERATURE", 0.1)),
|
||||
MaxTokens: new(b.resolver.getEnvIntOrDefault("PROBOD_AGENT_DEFAULT_MAX_TOKENS", 4096)),
|
||||
},
|
||||
Probo: probodconfig.LLMAgentConfig{
|
||||
Provider: b.getEnvOrDefault("AGENT_PROBO_PROVIDER", ""),
|
||||
ModelName: b.getEnvOrDefault("AGENT_PROBO_MODEL_NAME", ""),
|
||||
Temperature: b.getEnvFloatPtr("AGENT_PROBO_TEMPERATURE"),
|
||||
MaxTokens: b.getEnvIntPtr("AGENT_PROBO_MAX_TOKENS"),
|
||||
Provider: b.resolver.getEnvOrDefault("PROBOD_AGENT_PROBO_PROVIDER", ""),
|
||||
ModelName: b.resolver.getEnvOrDefault("PROBOD_AGENT_PROBO_MODEL_NAME", ""),
|
||||
Temperature: b.resolver.getEnvFloatPtr("PROBOD_AGENT_PROBO_TEMPERATURE"),
|
||||
MaxTokens: b.resolver.getEnvIntPtr("PROBOD_AGENT_PROBO_MAX_TOKENS"),
|
||||
},
|
||||
EvidenceDescriber: probodconfig.LLMAgentConfig{
|
||||
Provider: b.getEnvOrDefault("AGENT_EVIDENCE_DESCRIBER_PROVIDER", ""),
|
||||
ModelName: b.getEnvOrDefault("AGENT_EVIDENCE_DESCRIBER_MODEL_NAME", ""),
|
||||
Temperature: b.getEnvFloatPtr("AGENT_EVIDENCE_DESCRIBER_TEMPERATURE"),
|
||||
MaxTokens: b.getEnvIntPtr("AGENT_EVIDENCE_DESCRIBER_MAX_TOKENS"),
|
||||
Provider: b.resolver.getEnvOrDefault("PROBOD_AGENT_EVIDENCE_DESCRIBER_PROVIDER", ""),
|
||||
ModelName: b.resolver.getEnvOrDefault("PROBOD_AGENT_EVIDENCE_DESCRIBER_MODEL_NAME", ""),
|
||||
Temperature: b.resolver.getEnvFloatPtr("PROBOD_AGENT_EVIDENCE_DESCRIBER_TEMPERATURE"),
|
||||
MaxTokens: b.resolver.getEnvIntPtr("PROBOD_AGENT_EVIDENCE_DESCRIBER_MAX_TOKENS"),
|
||||
},
|
||||
ThirdPartyVetter: probodconfig.LLMAgentConfig{
|
||||
Provider: b.getEnvOrDefault("AGENT_THIRD_PARTY_VETTER_PROVIDER", ""),
|
||||
ModelName: b.getEnvOrDefault("AGENT_THIRD_PARTY_VETTER_MODEL_NAME", ""),
|
||||
Temperature: b.getEnvFloatPtr("AGENT_THIRD_PARTY_VETTER_TEMPERATURE"),
|
||||
MaxTokens: b.getEnvIntPtr("AGENT_THIRD_PARTY_VETTER_MAX_TOKENS"),
|
||||
Provider: b.resolver.getEnvOrDefault("PROBOD_AGENT_THIRD_PARTY_VETTER_PROVIDER", ""),
|
||||
ModelName: b.resolver.getEnvOrDefault("PROBOD_AGENT_THIRD_PARTY_VETTER_MODEL_NAME", ""),
|
||||
Temperature: b.resolver.getEnvFloatPtr("PROBOD_AGENT_THIRD_PARTY_VETTER_TEMPERATURE"),
|
||||
MaxTokens: b.resolver.getEnvIntPtr("PROBOD_AGENT_THIRD_PARTY_VETTER_MAX_TOKENS"),
|
||||
},
|
||||
ThirdPartyDisambiguation: probodconfig.LLMAgentConfig{
|
||||
Provider: b.getEnvOrDefault("AGENT_THIRD_PARTY_DISAMBIGUATION_PROVIDER", ""),
|
||||
ModelName: b.getEnvOrDefault("AGENT_THIRD_PARTY_DISAMBIGUATION_MODEL_NAME", ""),
|
||||
Provider: b.resolver.getEnvOrDefault("PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_PROVIDER", ""),
|
||||
ModelName: b.resolver.getEnvOrDefault("PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_MODEL_NAME", ""),
|
||||
// The disambiguation agent emits a single id plus a
|
||||
// short rationale, but the budget must leave headroom
|
||||
// for reasoning models whose reasoning tokens count
|
||||
// against max_tokens; too small a budget truncates the
|
||||
// JSON.
|
||||
Temperature: b.getEnvFloatPtr("AGENT_THIRD_PARTY_DISAMBIGUATION_TEMPERATURE"),
|
||||
MaxTokens: new(b.getEnvIntOrDefault("AGENT_THIRD_PARTY_DISAMBIGUATION_MAX_TOKENS", 4096)),
|
||||
Temperature: b.resolver.getEnvFloatPtr("PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_TEMPERATURE"),
|
||||
MaxTokens: new(b.resolver.getEnvIntOrDefault("PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_MAX_TOKENS", 4096)),
|
||||
},
|
||||
TrackerMapping: probodconfig.LLMAgentConfig{
|
||||
Provider: b.getEnvOrDefault("AGENT_TRACKER_MAPPING_PROVIDER", ""),
|
||||
ModelName: b.getEnvOrDefault("AGENT_TRACKER_MAPPING_MODEL_NAME", ""),
|
||||
Provider: b.resolver.getEnvOrDefault("PROBOD_AGENT_TRACKER_MAPPING_PROVIDER", ""),
|
||||
ModelName: b.resolver.getEnvOrDefault("PROBOD_AGENT_TRACKER_MAPPING_MODEL_NAME", ""),
|
||||
// The tracker agents emit tiny structured JSON, but
|
||||
// the budget must leave headroom for reasoning
|
||||
// models whose reasoning tokens count against
|
||||
// max_tokens; too small a budget truncates the JSON.
|
||||
Temperature: b.getEnvFloatPtr("AGENT_TRACKER_MAPPING_TEMPERATURE"),
|
||||
MaxTokens: new(b.getEnvIntOrDefault("AGENT_TRACKER_MAPPING_MAX_TOKENS", 4096)),
|
||||
Temperature: b.resolver.getEnvFloatPtr("PROBOD_AGENT_TRACKER_MAPPING_TEMPERATURE"),
|
||||
MaxTokens: new(b.resolver.getEnvIntOrDefault("PROBOD_AGENT_TRACKER_MAPPING_MAX_TOKENS", 4096)),
|
||||
},
|
||||
TrackerEnrichment: probodconfig.LLMAgentConfig{
|
||||
Provider: b.getEnvOrDefault("AGENT_TRACKER_ENRICHMENT_PROVIDER", ""),
|
||||
ModelName: b.getEnvOrDefault("AGENT_TRACKER_ENRICHMENT_MODEL_NAME", ""),
|
||||
Provider: b.resolver.getEnvOrDefault("PROBOD_AGENT_TRACKER_ENRICHMENT_PROVIDER", ""),
|
||||
ModelName: b.resolver.getEnvOrDefault("PROBOD_AGENT_TRACKER_ENRICHMENT_MODEL_NAME", ""),
|
||||
// See the tracker-mapping note: keep ample headroom so
|
||||
// reasoning models do not truncate the structured JSON.
|
||||
Temperature: b.getEnvFloatPtr("AGENT_TRACKER_ENRICHMENT_TEMPERATURE"),
|
||||
MaxTokens: new(b.getEnvIntOrDefault("AGENT_TRACKER_ENRICHMENT_MAX_TOKENS", 4096)),
|
||||
Temperature: b.resolver.getEnvFloatPtr("PROBOD_AGENT_TRACKER_ENRICHMENT_TEMPERATURE"),
|
||||
MaxTokens: new(b.resolver.getEnvIntOrDefault("PROBOD_AGENT_TRACKER_ENRICHMENT_MAX_TOKENS", 4096)),
|
||||
},
|
||||
CommonThirdPartyEnrichment: probodconfig.LLMAgentConfig{
|
||||
Provider: b.getEnvOrDefault("AGENT_COMMON_THIRD_PARTY_ENRICHMENT_PROVIDER", ""),
|
||||
ModelName: b.getEnvOrDefault("AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MODEL_NAME", ""),
|
||||
Provider: b.resolver.getEnvOrDefault("PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_PROVIDER", ""),
|
||||
ModelName: b.resolver.getEnvOrDefault("PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MODEL_NAME", ""),
|
||||
// Agent B browses pages and emits a moderate structured
|
||||
// output; the budget must leave headroom for reasoning
|
||||
// models whose reasoning tokens count against max_tokens.
|
||||
Temperature: b.getEnvFloatPtr("AGENT_COMMON_THIRD_PARTY_ENRICHMENT_TEMPERATURE"),
|
||||
MaxTokens: new(b.getEnvIntOrDefault("AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MAX_TOKENS", 8192)),
|
||||
Temperature: b.resolver.getEnvFloatPtr("PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_TEMPERATURE"),
|
||||
MaxTokens: new(b.resolver.getEnvIntOrDefault("PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MAX_TOKENS", 8192)),
|
||||
},
|
||||
Tools: probodconfig.AgentToolsConfig{
|
||||
FirecrawlAPIKey: b.getEnv("FIRECRAWL_API_KEY"),
|
||||
FirecrawlAPIKey: b.resolver.getEnv("PROBOD_FIRECRAWL_API_KEY"),
|
||||
},
|
||||
},
|
||||
CustomDomains: probodconfig.CustomDomainsConfig{
|
||||
RenewalInterval: b.getEnvIntOrDefault("CUSTOM_DOMAINS_RENEWAL_INTERVAL", 3600),
|
||||
ProvisionInterval: b.getEnvIntOrDefault("CUSTOM_DOMAINS_PROVISION_INTERVAL", 30),
|
||||
CnameTarget: b.getEnvOrDefault("CUSTOM_DOMAINS_CNAME_TARGET", "custom.getprobo.com"),
|
||||
ResolverAddr: b.getEnvOrDefault("CUSTOM_DOMAINS_RESOLVER_ADDR", "8.8.8.8:53"),
|
||||
CAAIssuerDomain: b.getEnvOrDefault("CUSTOM_DOMAINS_CAA_ISSUER_DOMAIN", "letsencrypt.org"),
|
||||
RenewalInterval: b.resolver.getEnvIntOrDefault("PROBOD_CUSTOM_DOMAINS_RENEWAL_INTERVAL", 3600),
|
||||
ProvisionInterval: b.resolver.getEnvIntOrDefault("PROBOD_CUSTOM_DOMAINS_PROVISION_INTERVAL", 30),
|
||||
CnameTarget: b.resolver.getEnvOrDefault("PROBOD_CUSTOM_DOMAINS_CNAME_TARGET", "custom.getprobo.com"),
|
||||
ResolverAddr: b.resolver.getEnvOrDefault("PROBOD_CUSTOM_DOMAINS_RESOLVER_ADDR", "8.8.8.8:53"),
|
||||
CAAIssuerDomain: b.resolver.getEnvOrDefault("PROBOD_CUSTOM_DOMAINS_CAA_ISSUER_DOMAIN", "letsencrypt.org"),
|
||||
ACME: probodconfig.ACMEConfig{
|
||||
Directory: b.getEnvOrDefault("ACME_DIRECTORY", "https://acme-v02.api.letsencrypt.org/directory"),
|
||||
Email: b.getEnvOrDefault("ACME_EMAIL", "admin@probo.com"),
|
||||
KeyType: b.getEnvOrDefault("ACME_KEY_TYPE", "EC256"),
|
||||
RootCA: b.getEnv("ACME_ROOT_CA"),
|
||||
AccountKey: b.getEnv("ACME_ACCOUNT_KEY"),
|
||||
Directory: b.resolver.getEnvOrDefault("PROBOD_ACME_DIRECTORY", "https://acme-v02.api.letsencrypt.org/directory"),
|
||||
Email: b.resolver.getEnvOrDefault("PROBOD_ACME_EMAIL", "admin@probo.com"),
|
||||
KeyType: b.resolver.getEnvOrDefault("PROBOD_ACME_KEY_TYPE", "EC256"),
|
||||
RootCA: b.resolver.getEnv("PROBOD_ACME_ROOT_CA"),
|
||||
AccountKey: b.resolver.getEnv("PROBOD_ACME_ACCOUNT_KEY"),
|
||||
},
|
||||
},
|
||||
SCIMBridge: probodconfig.SCIMBridgeConfig{
|
||||
SyncInterval: b.getEnvIntOrDefault("SCIM_BRIDGE_SYNC_INTERVAL", 900),
|
||||
PollInterval: b.getEnvIntOrDefault("SCIM_BRIDGE_POLL_INTERVAL", 30),
|
||||
SyncInterval: b.resolver.getEnvIntOrDefault("PROBOD_SCIM_BRIDGE_SYNC_INTERVAL", 900),
|
||||
PollInterval: b.resolver.getEnvIntOrDefault("PROBOD_SCIM_BRIDGE_POLL_INTERVAL", 30),
|
||||
},
|
||||
ESign: probodconfig.ESignConfig{
|
||||
TSAURL: b.getEnvOrDefault("ESIGN_TSA_URL", "http://timestamp.digicert.com"),
|
||||
TSAURL: b.resolver.getEnvOrDefault("PROBOD_ESIGN_TSA_URL", "http://timestamp.digicert.com"),
|
||||
},
|
||||
EvidenceDescriber: probodconfig.EvidenceDescriberConfig{
|
||||
Interval: b.getEnvIntOrDefault("EVIDENCE_DESCRIBER_INTERVAL", 10),
|
||||
StaleAfter: b.getEnvIntOrDefault("EVIDENCE_DESCRIBER_STALE_AFTER", 300),
|
||||
MaxConcurrency: b.getEnvIntOrDefault("EVIDENCE_DESCRIBER_MAX_CONCURRENCY", 10),
|
||||
Interval: b.resolver.getEnvIntOrDefault("PROBOD_EVIDENCE_DESCRIBER_INTERVAL", 10),
|
||||
StaleAfter: b.resolver.getEnvIntOrDefault("PROBOD_EVIDENCE_DESCRIBER_STALE_AFTER", 300),
|
||||
MaxConcurrency: b.resolver.getEnvIntOrDefault("PROBOD_EVIDENCE_DESCRIBER_MAX_CONCURRENCY", 10),
|
||||
},
|
||||
ThirdPartyVetting: probodconfig.ThirdPartyVettingWorkerConfig{
|
||||
Interval: b.getEnvIntOrDefault("THIRD_PARTY_VETTING_INTERVAL", 10),
|
||||
StaleAfter: b.getEnvIntOrDefault("THIRD_PARTY_VETTING_STALE_AFTER", 1500),
|
||||
MaxConcurrency: b.getEnvIntOrDefault("THIRD_PARTY_VETTING_MAX_CONCURRENCY", 1),
|
||||
Interval: b.resolver.getEnvIntOrDefault("PROBOD_THIRD_PARTY_VETTING_INTERVAL", 10),
|
||||
StaleAfter: b.resolver.getEnvIntOrDefault("PROBOD_THIRD_PARTY_VETTING_STALE_AFTER", 1500),
|
||||
MaxConcurrency: b.resolver.getEnvIntOrDefault("PROBOD_THIRD_PARTY_VETTING_MAX_CONCURRENCY", 1),
|
||||
},
|
||||
TrackerMappingWorker: probodconfig.TrackerMappingWorkerConfig{
|
||||
Interval: b.getEnvIntOrDefault("TRACKER_MAPPING_INTERVAL", 10),
|
||||
MaxConcurrency: b.getEnvIntOrDefault("TRACKER_MAPPING_MAX_CONCURRENCY", 3),
|
||||
StaleAfter: b.getEnvIntOrDefault("TRACKER_MAPPING_STALE_AFTER", 600),
|
||||
AgentTimeout: b.getEnvIntOrDefault("TRACKER_MAPPING_AGENT_TIMEOUT", 45),
|
||||
AgentMaxTurns: b.getEnvIntOrDefault("TRACKER_MAPPING_AGENT_MAX_TURNS", 10),
|
||||
DisambiguationAgentTimeout: b.getEnvIntOrDefault("TRACKER_MAPPING_DISAMBIGUATION_AGENT_TIMEOUT", 45),
|
||||
Interval: b.resolver.getEnvIntOrDefault("PROBOD_TRACKER_MAPPING_INTERVAL", 10),
|
||||
MaxConcurrency: b.resolver.getEnvIntOrDefault("PROBOD_TRACKER_MAPPING_MAX_CONCURRENCY", 3),
|
||||
StaleAfter: b.resolver.getEnvIntOrDefault("PROBOD_TRACKER_MAPPING_STALE_AFTER", 600),
|
||||
AgentTimeout: b.resolver.getEnvIntOrDefault("PROBOD_TRACKER_MAPPING_AGENT_TIMEOUT", 45),
|
||||
AgentMaxTurns: b.resolver.getEnvIntOrDefault("PROBOD_TRACKER_MAPPING_AGENT_MAX_TURNS", 10),
|
||||
DisambiguationAgentTimeout: b.resolver.getEnvIntOrDefault("PROBOD_TRACKER_MAPPING_DISAMBIGUATION_AGENT_TIMEOUT", 45),
|
||||
},
|
||||
CommonPatternEnrichmentWorker: probodconfig.CommonPatternEnrichmentWorkerConfig{
|
||||
Interval: b.getEnvIntOrDefault("COMMON_PATTERN_ENRICHMENT_INTERVAL", 10),
|
||||
MaxConcurrency: b.getEnvIntOrDefault("COMMON_PATTERN_ENRICHMENT_MAX_CONCURRENCY", 2),
|
||||
StaleAfter: b.getEnvIntOrDefault("COMMON_PATTERN_ENRICHMENT_STALE_AFTER", 600),
|
||||
AgentTimeout: b.getEnvIntOrDefault("COMMON_PATTERN_ENRICHMENT_AGENT_TIMEOUT", 45),
|
||||
AgentMaxTurns: b.getEnvIntOrDefault("COMMON_PATTERN_ENRICHMENT_AGENT_MAX_TURNS", 10),
|
||||
Interval: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_PATTERN_ENRICHMENT_INTERVAL", 10),
|
||||
MaxConcurrency: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_PATTERN_ENRICHMENT_MAX_CONCURRENCY", 2),
|
||||
StaleAfter: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_PATTERN_ENRICHMENT_STALE_AFTER", 600),
|
||||
AgentTimeout: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_PATTERN_ENRICHMENT_AGENT_TIMEOUT", 45),
|
||||
AgentMaxTurns: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_PATTERN_ENRICHMENT_AGENT_MAX_TURNS", 10),
|
||||
},
|
||||
CommonThirdPartyEnrichmentWorker: probodconfig.CommonThirdPartyEnrichmentWorkerConfig{
|
||||
Interval: b.getEnvIntOrDefault("COMMON_THIRD_PARTY_ENRICHMENT_INTERVAL", 10),
|
||||
MaxConcurrency: b.getEnvIntOrDefault("COMMON_THIRD_PARTY_ENRICHMENT_MAX_CONCURRENCY", 1),
|
||||
StaleAfter: b.getEnvIntOrDefault("COMMON_THIRD_PARTY_ENRICHMENT_STALE_AFTER", 900),
|
||||
AgentTimeout: b.getEnvIntOrDefault("COMMON_THIRD_PARTY_ENRICHMENT_AGENT_TIMEOUT", 90),
|
||||
AgentMaxTurns: b.getEnvIntOrDefault("COMMON_THIRD_PARTY_ENRICHMENT_AGENT_MAX_TURNS", 12),
|
||||
ConfidenceThreshold: b.getEnvFloatOrDefault("COMMON_THIRD_PARTY_ENRICHMENT_CONFIDENCE_THRESHOLD", 0.7),
|
||||
MaxAttempts: b.getEnvIntOrDefault("COMMON_THIRD_PARTY_ENRICHMENT_MAX_ATTEMPTS", 3),
|
||||
Interval: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_INTERVAL", 10),
|
||||
MaxConcurrency: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_MAX_CONCURRENCY", 1),
|
||||
StaleAfter: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_STALE_AFTER", 900),
|
||||
AgentTimeout: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_AGENT_TIMEOUT", 90),
|
||||
AgentMaxTurns: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_AGENT_MAX_TURNS", 12),
|
||||
ConfidenceThreshold: b.resolver.getEnvFloatOrDefault("PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_CONFIDENCE_THRESHOLD", 0.7),
|
||||
MaxAttempts: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_MAX_ATTEMPTS", 3),
|
||||
},
|
||||
Branding: b.getEnvBoolOrDefault("BRANDING", true),
|
||||
Branding: b.resolver.getEnvBoolOrDefault("PROBOD_BRANDING", true),
|
||||
},
|
||||
}
|
||||
|
||||
if slackClientID := b.getEnv("CONNECTOR_SLACK_CLIENT_ID"); slackClientID != "" {
|
||||
if slackClientID := b.resolver.getEnv("PROBOD_CONNECTOR_SLACK_CLIENT_ID"); slackClientID != "" {
|
||||
cfg.Probod.Connectors = append(
|
||||
cfg.Probod.Connectors,
|
||||
probodconfig.ConnectorConfig{
|
||||
@@ -330,16 +329,16 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
||||
Protocol: "oauth2",
|
||||
RawConfig: probodconfig.ConnectorConfigOAuth2{
|
||||
ClientID: slackClientID,
|
||||
ClientSecret: b.getEnv("CONNECTOR_SLACK_CLIENT_SECRET"),
|
||||
ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_SLACK_CLIENT_SECRET"),
|
||||
},
|
||||
RawSettings: map[string]any{
|
||||
"signing-secret": b.getEnv("CONNECTOR_SLACK_SIGNING_SECRET"),
|
||||
"signing-secret": b.resolver.getEnv("PROBOD_CONNECTOR_SLACK_SIGNING_SECRET"),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if hubspotClientID := b.getEnv("CONNECTOR_HUBSPOT_CLIENT_ID"); hubspotClientID != "" {
|
||||
if hubspotClientID := b.resolver.getEnv("PROBOD_CONNECTOR_HUBSPOT_CLIENT_ID"); hubspotClientID != "" {
|
||||
cfg.Probod.Connectors = append(
|
||||
cfg.Probod.Connectors,
|
||||
probodconfig.ConnectorConfig{
|
||||
@@ -347,13 +346,13 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
||||
Protocol: "oauth2",
|
||||
RawConfig: probodconfig.ConnectorConfigOAuth2{
|
||||
ClientID: hubspotClientID,
|
||||
ClientSecret: b.getEnv("CONNECTOR_HUBSPOT_CLIENT_SECRET"),
|
||||
ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_HUBSPOT_CLIENT_SECRET"),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if docusignClientID := b.getEnv("CONNECTOR_DOCUSIGN_CLIENT_ID"); docusignClientID != "" {
|
||||
if docusignClientID := b.resolver.getEnv("PROBOD_CONNECTOR_DOCUSIGN_CLIENT_ID"); docusignClientID != "" {
|
||||
cfg.Probod.Connectors = append(
|
||||
cfg.Probod.Connectors,
|
||||
probodconfig.ConnectorConfig{
|
||||
@@ -361,13 +360,13 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
||||
Protocol: "oauth2",
|
||||
RawConfig: probodconfig.ConnectorConfigOAuth2{
|
||||
ClientID: docusignClientID,
|
||||
ClientSecret: b.getEnv("CONNECTOR_DOCUSIGN_CLIENT_SECRET"),
|
||||
ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_DOCUSIGN_CLIENT_SECRET"),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if notionClientID := b.getEnv("CONNECTOR_NOTION_CLIENT_ID"); notionClientID != "" {
|
||||
if notionClientID := b.resolver.getEnv("PROBOD_CONNECTOR_NOTION_CLIENT_ID"); notionClientID != "" {
|
||||
cfg.Probod.Connectors = append(
|
||||
cfg.Probod.Connectors,
|
||||
probodconfig.ConnectorConfig{
|
||||
@@ -375,13 +374,13 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
||||
Protocol: "oauth2",
|
||||
RawConfig: probodconfig.ConnectorConfigOAuth2{
|
||||
ClientID: notionClientID,
|
||||
ClientSecret: b.getEnv("CONNECTOR_NOTION_CLIENT_SECRET"),
|
||||
ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_NOTION_CLIENT_SECRET"),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if githubClientID := b.getEnv("CONNECTOR_GITHUB_CLIENT_ID"); githubClientID != "" {
|
||||
if githubClientID := b.resolver.getEnv("PROBOD_CONNECTOR_GITHUB_CLIENT_ID"); githubClientID != "" {
|
||||
cfg.Probod.Connectors = append(
|
||||
cfg.Probod.Connectors,
|
||||
probodconfig.ConnectorConfig{
|
||||
@@ -389,13 +388,13 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
||||
Protocol: "oauth2",
|
||||
RawConfig: probodconfig.ConnectorConfigOAuth2{
|
||||
ClientID: githubClientID,
|
||||
ClientSecret: b.getEnv("CONNECTOR_GITHUB_CLIENT_SECRET"),
|
||||
ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_GITHUB_CLIENT_SECRET"),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if sentryClientID := b.getEnv("CONNECTOR_SENTRY_CLIENT_ID"); sentryClientID != "" {
|
||||
if sentryClientID := b.resolver.getEnv("PROBOD_CONNECTOR_SENTRY_CLIENT_ID"); sentryClientID != "" {
|
||||
cfg.Probod.Connectors = append(
|
||||
cfg.Probod.Connectors,
|
||||
probodconfig.ConnectorConfig{
|
||||
@@ -403,13 +402,13 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
||||
Protocol: "oauth2",
|
||||
RawConfig: probodconfig.ConnectorConfigOAuth2{
|
||||
ClientID: sentryClientID,
|
||||
ClientSecret: b.getEnv("CONNECTOR_SENTRY_CLIENT_SECRET"),
|
||||
ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_SENTRY_CLIENT_SECRET"),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if intercomClientID := b.getEnv("CONNECTOR_INTERCOM_CLIENT_ID"); intercomClientID != "" {
|
||||
if intercomClientID := b.resolver.getEnv("PROBOD_CONNECTOR_INTERCOM_CLIENT_ID"); intercomClientID != "" {
|
||||
cfg.Probod.Connectors = append(
|
||||
cfg.Probod.Connectors,
|
||||
probodconfig.ConnectorConfig{
|
||||
@@ -417,13 +416,13 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
||||
Protocol: "oauth2",
|
||||
RawConfig: probodconfig.ConnectorConfigOAuth2{
|
||||
ClientID: intercomClientID,
|
||||
ClientSecret: b.getEnv("CONNECTOR_INTERCOM_CLIENT_SECRET"),
|
||||
ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_INTERCOM_CLIENT_SECRET"),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if brexClientID := b.getEnv("CONNECTOR_BREX_CLIENT_ID"); brexClientID != "" {
|
||||
if brexClientID := b.resolver.getEnv("PROBOD_CONNECTOR_BREX_CLIENT_ID"); brexClientID != "" {
|
||||
cfg.Probod.Connectors = append(
|
||||
cfg.Probod.Connectors,
|
||||
probodconfig.ConnectorConfig{
|
||||
@@ -431,13 +430,13 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
||||
Protocol: "oauth2",
|
||||
RawConfig: probodconfig.ConnectorConfigOAuth2{
|
||||
ClientID: brexClientID,
|
||||
ClientSecret: b.getEnv("CONNECTOR_BREX_CLIENT_SECRET"),
|
||||
ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_BREX_CLIENT_SECRET"),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if googleWorkspaceClientID := b.getEnv("CONNECTOR_GOOGLE_WORKSPACE_CLIENT_ID"); googleWorkspaceClientID != "" {
|
||||
if googleWorkspaceClientID := b.resolver.getEnv("PROBOD_CONNECTOR_GOOGLE_WORKSPACE_CLIENT_ID"); googleWorkspaceClientID != "" {
|
||||
cfg.Probod.Connectors = append(
|
||||
cfg.Probod.Connectors,
|
||||
probodconfig.ConnectorConfig{
|
||||
@@ -445,13 +444,13 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
||||
Protocol: "oauth2",
|
||||
RawConfig: probodconfig.ConnectorConfigOAuth2{
|
||||
ClientID: googleWorkspaceClientID,
|
||||
ClientSecret: b.getEnv("CONNECTOR_GOOGLE_WORKSPACE_CLIENT_SECRET"),
|
||||
ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_GOOGLE_WORKSPACE_CLIENT_SECRET"),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if microsoft365ClientID := b.getEnv("CONNECTOR_MICROSOFT_365_CLIENT_ID"); microsoft365ClientID != "" {
|
||||
if microsoft365ClientID := b.resolver.getEnv("PROBOD_CONNECTOR_MICROSOFT_365_CLIENT_ID"); microsoft365ClientID != "" {
|
||||
cfg.Probod.Connectors = append(
|
||||
cfg.Probod.Connectors,
|
||||
probodconfig.ConnectorConfig{
|
||||
@@ -459,7 +458,7 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
||||
Protocol: "oauth2",
|
||||
RawConfig: probodconfig.ConnectorConfigOAuth2{
|
||||
ClientID: microsoft365ClientID,
|
||||
ClientSecret: b.getEnv("CONNECTOR_MICROSOFT_365_CLIENT_SECRET"),
|
||||
ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_MICROSOFT_365_CLIENT_SECRET"),
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -476,8 +475,9 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
||||
"MONDAY",
|
||||
"DATADOG",
|
||||
"ZENDESK",
|
||||
"LINEAR",
|
||||
} {
|
||||
clientID := b.getEnv("CONNECTOR_" + provider + "_CLIENT_ID")
|
||||
clientID := b.resolver.getEnv("PROBOD_CONNECTOR_" + provider + "_CLIENT_ID")
|
||||
if clientID == "" {
|
||||
continue
|
||||
}
|
||||
@@ -489,7 +489,7 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
||||
Protocol: "oauth2",
|
||||
RawConfig: probodconfig.ConnectorConfigOAuth2{
|
||||
ClientID: clientID,
|
||||
ClientSecret: b.getEnv("CONNECTOR_" + provider + "_CLIENT_SECRET"),
|
||||
ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_" + provider + "_CLIENT_SECRET"),
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -497,7 +497,7 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
||||
|
||||
// Vercel needs the operator-supplied integration slug to resolve the
|
||||
// templated AuthURL ("https://vercel.com/integrations/{integration_slug}/new").
|
||||
if vercelClientID := b.getEnv("CONNECTOR_VERCEL_CLIENT_ID"); vercelClientID != "" {
|
||||
if vercelClientID := b.resolver.getEnv("PROBOD_CONNECTOR_VERCEL_CLIENT_ID"); vercelClientID != "" {
|
||||
cfg.Probod.Connectors = append(
|
||||
cfg.Probod.Connectors,
|
||||
probodconfig.ConnectorConfig{
|
||||
@@ -505,13 +505,17 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) {
|
||||
Protocol: "oauth2",
|
||||
RawConfig: probodconfig.ConnectorConfigOAuth2{
|
||||
ClientID: vercelClientID,
|
||||
ClientSecret: b.getEnv("CONNECTOR_VERCEL_CLIENT_SECRET"),
|
||||
IntegrationSlug: b.getEnv("CONNECTOR_VERCEL_INTEGRATION_SLUG"),
|
||||
ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_VERCEL_CLIENT_SECRET"),
|
||||
IntegrationSlug: b.resolver.getEnv("PROBOD_CONNECTOR_VERCEL_INTEGRATION_SLUG"),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if b.resolver.Err() != nil {
|
||||
return nil, b.resolver.Err()
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -520,28 +524,28 @@ func (b *Builder) validateRequired() error {
|
||||
|
||||
required := []string{
|
||||
"PROBOD_ENCRYPTION_KEY",
|
||||
"AUTH_COOKIE_SECRET",
|
||||
"AUTH_PASSWORD_PEPPER",
|
||||
"PROBOD_AUTH_COOKIE_SECRET",
|
||||
"PROBOD_AUTH_PASSWORD_PEPPER",
|
||||
}
|
||||
|
||||
for _, key := range required {
|
||||
if b.getEnv(key) == "" {
|
||||
if b.resolver.getEnv(key) == "" {
|
||||
missing = append(missing, key)
|
||||
}
|
||||
}
|
||||
|
||||
if b.oauth2SigningKey == "" && b.getEnv("OAUTH2_SERVER_SIGNING_KEY") == "" {
|
||||
missing = append(missing, "OAUTH2_SERVER_SIGNING_KEY")
|
||||
if b.oauth2SigningKey == "" && b.resolver.getEnv("PROBOD_OAUTH2_SERVER_SIGNING_KEY") == "" {
|
||||
missing = append(missing, "PROBOD_OAUTH2_SERVER_SIGNING_KEY")
|
||||
}
|
||||
|
||||
if slackClientID := b.getEnv("CONNECTOR_SLACK_CLIENT_ID"); slackClientID != "" {
|
||||
if slackClientID := b.resolver.getEnv("PROBOD_CONNECTOR_SLACK_CLIENT_ID"); slackClientID != "" {
|
||||
slackRequired := []string{
|
||||
"CONNECTOR_SLACK_CLIENT_SECRET",
|
||||
"CONNECTOR_SLACK_SIGNING_SECRET",
|
||||
"PROBOD_CONNECTOR_SLACK_CLIENT_SECRET",
|
||||
"PROBOD_CONNECTOR_SLACK_SIGNING_SECRET",
|
||||
}
|
||||
for _, key := range slackRequired {
|
||||
if b.getEnv(key) == "" {
|
||||
missing = append(missing, key+" (required when CONNECTOR_SLACK_CLIENT_ID is set)")
|
||||
if b.resolver.getEnv(key) == "" {
|
||||
missing = append(missing, key+" (required when PROBOD_CONNECTOR_SLACK_CLIENT_ID is set)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -569,15 +573,16 @@ func (b *Builder) validateRequired() error {
|
||||
{"CONNECTOR_MONDAY", []string{"CLIENT_SECRET"}},
|
||||
{"CONNECTOR_DATADOG", []string{"CLIENT_SECRET"}},
|
||||
{"CONNECTOR_ZENDESK", []string{"CLIENT_SECRET"}},
|
||||
{"CONNECTOR_LINEAR", []string{"CLIENT_SECRET"}},
|
||||
{"CONNECTOR_VERCEL", []string{"CLIENT_SECRET", "INTEGRATION_SLUG"}},
|
||||
}
|
||||
|
||||
for _, p := range oauthProviders {
|
||||
clientIDKey := p.envPrefix + "_CLIENT_ID"
|
||||
if b.getEnv(clientIDKey) != "" {
|
||||
clientIDKey := "PROBOD_" + p.envPrefix + "_CLIENT_ID"
|
||||
if b.resolver.getEnv(clientIDKey) != "" {
|
||||
for _, suffix := range p.required {
|
||||
key := p.envPrefix + "_" + suffix
|
||||
if b.getEnv(key) == "" {
|
||||
key := "PROBOD_" + p.envPrefix + "_" + suffix
|
||||
if b.resolver.getEnv(key) == "" {
|
||||
missing = append(missing, key+" (required when "+clientIDKey+" is set)")
|
||||
}
|
||||
}
|
||||
@@ -585,6 +590,10 @@ func (b *Builder) validateRequired() error {
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
if err := b.resolver.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("missing required environment variables:\n - %s", strings.Join(missing, "\n - "))
|
||||
}
|
||||
|
||||
@@ -596,11 +605,11 @@ func (b *Builder) getSAMLCredentials() (cert, key string, err error) {
|
||||
key = b.samlPrivateKey
|
||||
|
||||
if cert == "" {
|
||||
cert = b.getEnv("SAML_CERTIFICATE")
|
||||
cert = b.resolver.getEnv("PROBOD_SAML_CERTIFICATE")
|
||||
}
|
||||
|
||||
if key == "" {
|
||||
key = b.getEnv("SAML_PRIVATE_KEY")
|
||||
key = b.resolver.getEnv("PROBOD_SAML_PRIVATE_KEY")
|
||||
}
|
||||
|
||||
if cert == "" || key == "" {
|
||||
@@ -618,77 +627,18 @@ func (b *Builder) getOAuth2SigningKey() string {
|
||||
return b.oauth2SigningKey
|
||||
}
|
||||
|
||||
return b.getEnv("OAUTH2_SERVER_SIGNING_KEY")
|
||||
return b.resolver.getEnv("PROBOD_OAUTH2_SERVER_SIGNING_KEY")
|
||||
}
|
||||
|
||||
func (b *Builder) getPgCACertBundle() string {
|
||||
if path := b.getEnv("PG_CA_BUNDLE_PATH"); path != "" {
|
||||
if path := b.resolver.getEnv("PROBOD_PG_CA_BUNDLE_PATH"); path != "" {
|
||||
data, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
return string(data)
|
||||
}
|
||||
}
|
||||
|
||||
return b.getEnv("PG_CA_BUNDLE")
|
||||
}
|
||||
|
||||
func (b *Builder) getEnvOrDefault(key, defaultValue string) string {
|
||||
if value := b.getEnv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (b *Builder) getEnvIntOrDefault(key string, defaultValue int) int {
|
||||
if value := b.getEnv(key); value != "" {
|
||||
if intValue, err := strconv.ParseInt(value, 10, 32); err == nil {
|
||||
return int(intValue)
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (b *Builder) getEnvFloatOrDefault(key string, defaultValue float64) float64 {
|
||||
if value := b.getEnv(key); value != "" {
|
||||
if floatValue, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return floatValue
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (b *Builder) getEnvFloatPtr(key string) *float64 {
|
||||
if value := b.getEnv(key); value != "" {
|
||||
if floatValue, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return &floatValue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) getEnvIntPtr(key string) *int {
|
||||
if value := b.getEnv(key); value != "" {
|
||||
if intValue, err := strconv.ParseInt(value, 10, 32); err == nil {
|
||||
v := int(intValue)
|
||||
return &v
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) getEnvBoolOrDefault(key string, defaultValue bool) bool {
|
||||
if value := b.getEnv(key); value != "" {
|
||||
if boolValue, err := strconv.ParseBool(value); err == nil {
|
||||
return boolValue
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
return b.resolver.getEnv("PROBOD_PG_CA_BUNDLE")
|
||||
}
|
||||
|
||||
func (b *Builder) parseOriginsList(s string) []string {
|
||||
|
||||
@@ -33,10 +33,10 @@ func mockEnv(env map[string]string) EnvGetter {
|
||||
|
||||
func requiredEnv() map[string]string {
|
||||
return map[string]string{
|
||||
"PROBOD_ENCRYPTION_KEY": "test-encryption-key-32-bytes-long",
|
||||
"AUTH_COOKIE_SECRET": "test-cookie-secret-32-bytes-long!",
|
||||
"AUTH_PASSWORD_PEPPER": "test-password-pepper-32-bytes-lo",
|
||||
"OAUTH2_SERVER_SIGNING_KEY": "test-oauth2-signing-key",
|
||||
"PROBOD_ENCRYPTION_KEY": "test-encryption-key-32-bytes-long",
|
||||
"PROBOD_AUTH_COOKIE_SECRET": "test-cookie-secret-32-bytes-long!",
|
||||
"PROBOD_AUTH_PASSWORD_PEPPER": "test-password-pepper-32-bytes-lo",
|
||||
"PROBOD_OAUTH2_SERVER_SIGNING_KEY": "test-oauth2-signing-key",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,68 +49,68 @@ func TestBuilder_Build_MissingRequiredEnvVars(t *testing.T) {
|
||||
{
|
||||
name: "all missing",
|
||||
env: map[string]string{},
|
||||
wantMissing: []string{"PROBOD_ENCRYPTION_KEY", "AUTH_COOKIE_SECRET", "AUTH_PASSWORD_PEPPER", "OAUTH2_SERVER_SIGNING_KEY"},
|
||||
wantMissing: []string{"PROBOD_ENCRYPTION_KEY", "PROBOD_AUTH_COOKIE_SECRET", "PROBOD_AUTH_PASSWORD_PEPPER", "PROBOD_OAUTH2_SERVER_SIGNING_KEY"},
|
||||
},
|
||||
{
|
||||
name: "missing oauth2 signing key",
|
||||
env: map[string]string{
|
||||
"PROBOD_ENCRYPTION_KEY": "key",
|
||||
"AUTH_COOKIE_SECRET": "secret",
|
||||
"AUTH_PASSWORD_PEPPER": "pepper",
|
||||
"PROBOD_ENCRYPTION_KEY": "key",
|
||||
"PROBOD_AUTH_COOKIE_SECRET": "secret",
|
||||
"PROBOD_AUTH_PASSWORD_PEPPER": "pepper",
|
||||
},
|
||||
wantMissing: []string{"OAUTH2_SERVER_SIGNING_KEY"},
|
||||
wantMissing: []string{"PROBOD_OAUTH2_SERVER_SIGNING_KEY"},
|
||||
},
|
||||
{
|
||||
name: "missing encryption key",
|
||||
env: map[string]string{
|
||||
"AUTH_COOKIE_SECRET": "secret",
|
||||
"AUTH_PASSWORD_PEPPER": "pepper",
|
||||
"PROBOD_AUTH_COOKIE_SECRET": "secret",
|
||||
"PROBOD_AUTH_PASSWORD_PEPPER": "pepper",
|
||||
},
|
||||
wantMissing: []string{"PROBOD_ENCRYPTION_KEY"},
|
||||
},
|
||||
{
|
||||
name: "missing cookie secret",
|
||||
env: map[string]string{
|
||||
"PROBOD_ENCRYPTION_KEY": "key",
|
||||
"AUTH_PASSWORD_PEPPER": "pepper",
|
||||
"PROBOD_ENCRYPTION_KEY": "key",
|
||||
"PROBOD_AUTH_PASSWORD_PEPPER": "pepper",
|
||||
},
|
||||
wantMissing: []string{"AUTH_COOKIE_SECRET"},
|
||||
wantMissing: []string{"PROBOD_AUTH_COOKIE_SECRET"},
|
||||
},
|
||||
{
|
||||
name: "slack connector missing required fields",
|
||||
env: map[string]string{
|
||||
"PROBOD_ENCRYPTION_KEY": "key",
|
||||
"AUTH_COOKIE_SECRET": "secret",
|
||||
"AUTH_PASSWORD_PEPPER": "pepper",
|
||||
"CONNECTOR_SLACK_CLIENT_ID": "client-id",
|
||||
"PROBOD_ENCRYPTION_KEY": "key",
|
||||
"PROBOD_AUTH_COOKIE_SECRET": "secret",
|
||||
"PROBOD_AUTH_PASSWORD_PEPPER": "pepper",
|
||||
"PROBOD_CONNECTOR_SLACK_CLIENT_ID": "client-id",
|
||||
},
|
||||
wantMissing: []string{"CONNECTOR_SLACK_CLIENT_SECRET", "CONNECTOR_SLACK_SIGNING_SECRET"},
|
||||
wantMissing: []string{"PROBOD_CONNECTOR_SLACK_CLIENT_SECRET", "PROBOD_CONNECTOR_SLACK_SIGNING_SECRET"},
|
||||
},
|
||||
{
|
||||
name: "google workspace connector missing required fields",
|
||||
env: map[string]string{
|
||||
"PROBOD_ENCRYPTION_KEY": "key",
|
||||
"AUTH_COOKIE_SECRET": "secret",
|
||||
"AUTH_PASSWORD_PEPPER": "pepper",
|
||||
"CONNECTOR_GOOGLE_WORKSPACE_CLIENT_ID": "client-id",
|
||||
"PROBOD_ENCRYPTION_KEY": "key",
|
||||
"PROBOD_AUTH_COOKIE_SECRET": "secret",
|
||||
"PROBOD_AUTH_PASSWORD_PEPPER": "pepper",
|
||||
"PROBOD_CONNECTOR_GOOGLE_WORKSPACE_CLIENT_ID": "client-id",
|
||||
},
|
||||
wantMissing: []string{"CONNECTOR_GOOGLE_WORKSPACE_CLIENT_SECRET"},
|
||||
wantMissing: []string{"PROBOD_CONNECTOR_GOOGLE_WORKSPACE_CLIENT_SECRET"},
|
||||
},
|
||||
{
|
||||
name: "microsoft 365 connector missing required fields",
|
||||
env: map[string]string{
|
||||
"PROBOD_ENCRYPTION_KEY": "key",
|
||||
"AUTH_COOKIE_SECRET": "secret",
|
||||
"AUTH_PASSWORD_PEPPER": "pepper",
|
||||
"CONNECTOR_MICROSOFT_365_CLIENT_ID": "client-id",
|
||||
"PROBOD_ENCRYPTION_KEY": "key",
|
||||
"PROBOD_AUTH_COOKIE_SECRET": "secret",
|
||||
"PROBOD_AUTH_PASSWORD_PEPPER": "pepper",
|
||||
"PROBOD_CONNECTOR_MICROSOFT_365_CLIENT_ID": "client-id",
|
||||
},
|
||||
wantMissing: []string{"CONNECTOR_MICROSOFT_365_CLIENT_SECRET"},
|
||||
wantMissing: []string{"PROBOD_CONNECTOR_MICROSOFT_365_CLIENT_SECRET"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b := NewBuilder(mockEnv(tt.env))
|
||||
b := NewBuilder(NewResolver(mockEnv(tt.env)))
|
||||
_, err := b.Build()
|
||||
|
||||
require.Error(t, err)
|
||||
@@ -123,7 +123,7 @@ func TestBuilder_Build_MissingRequiredEnvVars(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuilder_Build_Defaults(t *testing.T) {
|
||||
b := NewBuilder(mockEnv(requiredEnv()))
|
||||
b := NewBuilder(NewResolver(mockEnv(requiredEnv())))
|
||||
b.samlCertificate = "test-cert"
|
||||
b.samlPrivateKey = "test-key"
|
||||
|
||||
@@ -285,129 +285,129 @@ func TestBuilder_Build_Defaults(t *testing.T) {
|
||||
func TestBuilder_Build_CustomValues(t *testing.T) {
|
||||
env := requiredEnv()
|
||||
// Unit
|
||||
env["METRICS_ADDR"] = "0.0.0.0:9090"
|
||||
env["TRACING_ADDR"] = "jaeger:4317"
|
||||
env["TRACING_MAX_BATCH_SIZE"] = "1024"
|
||||
env["PROBOD_METRICS_ADDR"] = "0.0.0.0:9090"
|
||||
env["PROBOD_TRACING_ADDR"] = "jaeger:4317"
|
||||
env["PROBOD_TRACING_MAX_BATCH_SIZE"] = "1024"
|
||||
// Probod
|
||||
env["PROBOD_BASE_URL"] = "https://app.example.com"
|
||||
env["CHROME_DP_ADDR"] = "chrome:9222"
|
||||
env["PROBOD_CHROME_DP_ADDR"] = "chrome:9222"
|
||||
// API
|
||||
env["API_ADDR"] = "0.0.0.0:8080"
|
||||
env["API_CORS_ALLOWED_ORIGINS"] = "https://app.example.com,https://admin.example.com"
|
||||
env["API_PROXY_PROTOCOL_TRUSTED_PROXIES"] = "10.0.0.1,10.0.0.2"
|
||||
env["PROBOD_API_ADDR"] = "0.0.0.0:8080"
|
||||
env["PROBOD_API_CORS_ALLOWED_ORIGINS"] = "https://app.example.com,https://admin.example.com"
|
||||
env["PROBOD_API_PROXY_PROTOCOL_TRUSTED_PROXIES"] = "10.0.0.1,10.0.0.2"
|
||||
// PG
|
||||
env["PG_ADDR"] = "postgres.example.com:5432"
|
||||
env["PG_USERNAME"] = "probo"
|
||||
env["PG_PASSWORD"] = "secret123"
|
||||
env["PG_DATABASE"] = "probo_prod"
|
||||
env["PG_POOL_SIZE"] = "200"
|
||||
env["PG_MIN_POOL_SIZE"] = "25"
|
||||
env["PG_MAX_CONN_IDLE_TIME_SECONDS"] = "900"
|
||||
env["PG_MAX_CONN_LIFETIME_SECONDS"] = "7200"
|
||||
env["PG_MAX_CONN_LIFETIME_JITTER_SECONDS"] = "600"
|
||||
env["PG_HEALTH_CHECK_PERIOD_SECONDS"] = "30"
|
||||
env["PG_DEBUG"] = "true"
|
||||
env["PROBOD_PG_ADDR"] = "postgres.example.com:5432"
|
||||
env["PROBOD_PG_USERNAME"] = "probo"
|
||||
env["PROBOD_PG_PASSWORD"] = "secret123"
|
||||
env["PROBOD_PG_DATABASE"] = "probo_prod"
|
||||
env["PROBOD_PG_POOL_SIZE"] = "200"
|
||||
env["PROBOD_PG_MIN_POOL_SIZE"] = "25"
|
||||
env["PROBOD_PG_MAX_CONN_IDLE_TIME_SECONDS"] = "900"
|
||||
env["PROBOD_PG_MAX_CONN_LIFETIME_SECONDS"] = "7200"
|
||||
env["PROBOD_PG_MAX_CONN_LIFETIME_JITTER_SECONDS"] = "600"
|
||||
env["PROBOD_PG_HEALTH_CHECK_PERIOD_SECONDS"] = "30"
|
||||
env["PROBOD_PG_DEBUG"] = "true"
|
||||
// Auth
|
||||
env["AUTH_DISABLE_SIGNUP"] = "true"
|
||||
env["AUTH_INVITATION_TOKEN_VALIDITY"] = "7200"
|
||||
env["AUTH_PASSWORD_RESET_TOKEN_VALIDITY"] = "1800"
|
||||
env["AUTH_MAGIC_LINK_TOKEN_VALIDITY"] = "600"
|
||||
env["AUTH_COOKIE_DOMAIN"] = ".example.com"
|
||||
env["AUTH_COOKIE_DURATION"] = "48"
|
||||
env["PROBOD_AUTH_DISABLE_SIGNUP"] = "true"
|
||||
env["PROBOD_AUTH_INVITATION_TOKEN_VALIDITY"] = "7200"
|
||||
env["PROBOD_AUTH_PASSWORD_RESET_TOKEN_VALIDITY"] = "1800"
|
||||
env["PROBOD_AUTH_MAGIC_LINK_TOKEN_VALIDITY"] = "600"
|
||||
env["PROBOD_AUTH_COOKIE_DOMAIN"] = ".example.com"
|
||||
env["PROBOD_AUTH_COOKIE_DURATION"] = "48"
|
||||
// SAML
|
||||
env["SAML_DOMAIN_VERIFICATION_INTERVAL_SECONDS"] = "120"
|
||||
env["SAML_DOMAIN_VERIFICATION_RESOLVER_ADDR"] = "1.1.1.1:53"
|
||||
env["PROBOD_SAML_DOMAIN_VERIFICATION_INTERVAL_SECONDS"] = "120"
|
||||
env["PROBOD_SAML_DOMAIN_VERIFICATION_RESOLVER_ADDR"] = "1.1.1.1:53"
|
||||
// Trust center
|
||||
env["TRUST_CENTER_HTTP_ADDR"] = ":8080"
|
||||
env["TRUST_CENTER_HTTPS_ADDR"] = ":8443"
|
||||
env["TRUST_CENTER_PROXY_PROTOCOL_TRUSTED_PROXIES"] = "10.0.1.1,10.0.1.2"
|
||||
env["PROBOD_TRUST_CENTER_HTTP_ADDR"] = ":8080"
|
||||
env["PROBOD_TRUST_CENTER_HTTPS_ADDR"] = ":8443"
|
||||
env["PROBOD_TRUST_CENTER_PROXY_PROTOCOL_TRUSTED_PROXIES"] = "10.0.1.1,10.0.1.2"
|
||||
// AWS
|
||||
env["AWS_REGION"] = "eu-west-1"
|
||||
env["AWS_BUCKET"] = "probo-files"
|
||||
env["AWS_ACCESS_KEY_ID"] = "AKIAIOSFODNN7EXAMPLE"
|
||||
env["AWS_SECRET_ACCESS_KEY"] = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
env["AWS_ENDPOINT"] = "https://s3.example.com"
|
||||
env["AWS_USE_PATH_STYLE"] = "true"
|
||||
env["PROBOD_AWS_REGION"] = "eu-west-1"
|
||||
env["PROBOD_AWS_BUCKET"] = "probo-files"
|
||||
env["PROBOD_AWS_ACCESS_KEY_ID"] = "AKIAIOSFODNN7EXAMPLE"
|
||||
env["PROBOD_AWS_SECRET_ACCESS_KEY"] = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
env["PROBOD_AWS_ENDPOINT"] = "https://s3.example.com"
|
||||
env["PROBOD_AWS_USE_PATH_STYLE"] = "true"
|
||||
// Notifications
|
||||
env["WEBHOOK_SENDER_INTERVAL"] = "10"
|
||||
env["WEBHOOK_CACHE_TTL"] = "3600"
|
||||
env["CONNECTOR_SLACK_SIGNING_SECRET"] = "slack-signing-secret"
|
||||
env["DOCUMENT_NOTIFICATION_INTERVAL"] = "120"
|
||||
env["DOCUMENT_NOTIFICATION_DEBOUNCE_DELAY"] = "60"
|
||||
env["DOCUMENT_NOTIFICATION_REMINDER_INTERVAL"] = "43200"
|
||||
env["PROBOD_WEBHOOK_SENDER_INTERVAL"] = "10"
|
||||
env["PROBOD_WEBHOOK_CACHE_TTL"] = "3600"
|
||||
env["PROBOD_CONNECTOR_SLACK_SIGNING_SECRET"] = "slack-signing-secret"
|
||||
env["PROBOD_DOCUMENT_NOTIFICATION_INTERVAL"] = "120"
|
||||
env["PROBOD_DOCUMENT_NOTIFICATION_DEBOUNCE_DELAY"] = "60"
|
||||
env["PROBOD_DOCUMENT_NOTIFICATION_REMINDER_INTERVAL"] = "43200"
|
||||
// Firecrawl
|
||||
env["FIRECRAWL_API_KEY"] = "fc-test-key"
|
||||
env["PROBOD_FIRECRAWL_API_KEY"] = "fc-test-key"
|
||||
// Agents — providers
|
||||
env["OPENAI_API_KEY"] = "sk-test-key"
|
||||
env["ANTHROPIC_API_KEY"] = "sk-ant-test-key"
|
||||
env["PROBOD_OPENAI_API_KEY"] = "sk-test-key"
|
||||
env["PROBOD_ANTHROPIC_API_KEY"] = "sk-ant-test-key"
|
||||
// Agents — default
|
||||
env["AGENT_DEFAULT_PROVIDER"] = "openai"
|
||||
env["AGENT_DEFAULT_MODEL_NAME"] = "gpt-4-turbo"
|
||||
env["AGENT_DEFAULT_TEMPERATURE"] = "0.5"
|
||||
env["AGENT_DEFAULT_MAX_TOKENS"] = "8192"
|
||||
env["PROBOD_AGENT_DEFAULT_PROVIDER"] = "openai"
|
||||
env["PROBOD_AGENT_DEFAULT_MODEL_NAME"] = "gpt-4-turbo"
|
||||
env["PROBOD_AGENT_DEFAULT_TEMPERATURE"] = "0.5"
|
||||
env["PROBOD_AGENT_DEFAULT_MAX_TOKENS"] = "8192"
|
||||
// Agents — evidence-describer override
|
||||
env["AGENT_EVIDENCE_DESCRIBER_PROVIDER"] = "anthropic"
|
||||
env["AGENT_EVIDENCE_DESCRIBER_MODEL_NAME"] = "claude-sonnet-4-20250514"
|
||||
env["AGENT_EVIDENCE_DESCRIBER_TEMPERATURE"] = "0.2"
|
||||
env["AGENT_EVIDENCE_DESCRIBER_MAX_TOKENS"] = "4096"
|
||||
env["PROBOD_AGENT_EVIDENCE_DESCRIBER_PROVIDER"] = "anthropic"
|
||||
env["PROBOD_AGENT_EVIDENCE_DESCRIBER_MODEL_NAME"] = "claude-sonnet-4-20250514"
|
||||
env["PROBOD_AGENT_EVIDENCE_DESCRIBER_TEMPERATURE"] = "0.2"
|
||||
env["PROBOD_AGENT_EVIDENCE_DESCRIBER_MAX_TOKENS"] = "4096"
|
||||
// Agents — third-party-vetter override
|
||||
env["AGENT_THIRD_PARTY_VETTER_PROVIDER"] = "openai"
|
||||
env["AGENT_THIRD_PARTY_VETTER_MODEL_NAME"] = "gpt-4o"
|
||||
env["AGENT_THIRD_PARTY_VETTER_TEMPERATURE"] = "0.3"
|
||||
env["AGENT_THIRD_PARTY_VETTER_MAX_TOKENS"] = "8192"
|
||||
env["PROBOD_AGENT_THIRD_PARTY_VETTER_PROVIDER"] = "openai"
|
||||
env["PROBOD_AGENT_THIRD_PARTY_VETTER_MODEL_NAME"] = "gpt-4o"
|
||||
env["PROBOD_AGENT_THIRD_PARTY_VETTER_TEMPERATURE"] = "0.3"
|
||||
env["PROBOD_AGENT_THIRD_PARTY_VETTER_MAX_TOKENS"] = "8192"
|
||||
// Agents — third-party-disambiguation override
|
||||
env["AGENT_THIRD_PARTY_DISAMBIGUATION_PROVIDER"] = "anthropic"
|
||||
env["AGENT_THIRD_PARTY_DISAMBIGUATION_MODEL_NAME"] = "claude-sonnet-4-20250514"
|
||||
env["AGENT_THIRD_PARTY_DISAMBIGUATION_TEMPERATURE"] = "0.4"
|
||||
env["AGENT_THIRD_PARTY_DISAMBIGUATION_MAX_TOKENS"] = "2048"
|
||||
env["PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_PROVIDER"] = "anthropic"
|
||||
env["PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_MODEL_NAME"] = "claude-sonnet-4-20250514"
|
||||
env["PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_TEMPERATURE"] = "0.4"
|
||||
env["PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_MAX_TOKENS"] = "2048"
|
||||
// Agents — tracker-mapping override
|
||||
env["AGENT_TRACKER_MAPPING_PROVIDER"] = "openai"
|
||||
env["AGENT_TRACKER_MAPPING_MODEL_NAME"] = "gpt-4o-mini"
|
||||
env["AGENT_TRACKER_MAPPING_TEMPERATURE"] = "0.1"
|
||||
env["AGENT_TRACKER_MAPPING_MAX_TOKENS"] = "1024"
|
||||
env["PROBOD_AGENT_TRACKER_MAPPING_PROVIDER"] = "openai"
|
||||
env["PROBOD_AGENT_TRACKER_MAPPING_MODEL_NAME"] = "gpt-4o-mini"
|
||||
env["PROBOD_AGENT_TRACKER_MAPPING_TEMPERATURE"] = "0.1"
|
||||
env["PROBOD_AGENT_TRACKER_MAPPING_MAX_TOKENS"] = "1024"
|
||||
// Agents — tracker-enrichment override
|
||||
env["AGENT_TRACKER_ENRICHMENT_PROVIDER"] = "openai"
|
||||
env["AGENT_TRACKER_ENRICHMENT_MODEL_NAME"] = "gpt-4o"
|
||||
env["AGENT_TRACKER_ENRICHMENT_TEMPERATURE"] = "0.2"
|
||||
env["AGENT_TRACKER_ENRICHMENT_MAX_TOKENS"] = "2048"
|
||||
env["PROBOD_AGENT_TRACKER_ENRICHMENT_PROVIDER"] = "openai"
|
||||
env["PROBOD_AGENT_TRACKER_ENRICHMENT_MODEL_NAME"] = "gpt-4o"
|
||||
env["PROBOD_AGENT_TRACKER_ENRICHMENT_TEMPERATURE"] = "0.2"
|
||||
env["PROBOD_AGENT_TRACKER_ENRICHMENT_MAX_TOKENS"] = "2048"
|
||||
// Tracker worker tuning override
|
||||
env["TRACKER_MAPPING_INTERVAL"] = "20"
|
||||
env["TRACKER_MAPPING_MAX_CONCURRENCY"] = "5"
|
||||
env["TRACKER_MAPPING_STALE_AFTER"] = "1200"
|
||||
env["TRACKER_MAPPING_AGENT_TIMEOUT"] = "30"
|
||||
env["TRACKER_MAPPING_AGENT_MAX_TURNS"] = "6"
|
||||
env["TRACKER_MAPPING_DISAMBIGUATION_AGENT_TIMEOUT"] = "35"
|
||||
env["COMMON_PATTERN_ENRICHMENT_INTERVAL"] = "15"
|
||||
env["COMMON_PATTERN_ENRICHMENT_MAX_CONCURRENCY"] = "4"
|
||||
env["COMMON_PATTERN_ENRICHMENT_STALE_AFTER"] = "900"
|
||||
env["COMMON_PATTERN_ENRICHMENT_AGENT_TIMEOUT"] = "50"
|
||||
env["COMMON_PATTERN_ENRICHMENT_AGENT_MAX_TURNS"] = "5"
|
||||
env["PROBOD_TRACKER_MAPPING_INTERVAL"] = "20"
|
||||
env["PROBOD_TRACKER_MAPPING_MAX_CONCURRENCY"] = "5"
|
||||
env["PROBOD_TRACKER_MAPPING_STALE_AFTER"] = "1200"
|
||||
env["PROBOD_TRACKER_MAPPING_AGENT_TIMEOUT"] = "30"
|
||||
env["PROBOD_TRACKER_MAPPING_AGENT_MAX_TURNS"] = "6"
|
||||
env["PROBOD_TRACKER_MAPPING_DISAMBIGUATION_AGENT_TIMEOUT"] = "35"
|
||||
env["PROBOD_COMMON_PATTERN_ENRICHMENT_INTERVAL"] = "15"
|
||||
env["PROBOD_COMMON_PATTERN_ENRICHMENT_MAX_CONCURRENCY"] = "4"
|
||||
env["PROBOD_COMMON_PATTERN_ENRICHMENT_STALE_AFTER"] = "900"
|
||||
env["PROBOD_COMMON_PATTERN_ENRICHMENT_AGENT_TIMEOUT"] = "50"
|
||||
env["PROBOD_COMMON_PATTERN_ENRICHMENT_AGENT_MAX_TURNS"] = "5"
|
||||
// Common third party enrichment agent + worker tuning override
|
||||
env["AGENT_COMMON_THIRD_PARTY_ENRICHMENT_PROVIDER"] = "openai"
|
||||
env["AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MODEL_NAME"] = "gpt-4o"
|
||||
env["AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MAX_TOKENS"] = "16384"
|
||||
env["COMMON_THIRD_PARTY_ENRICHMENT_INTERVAL"] = "25"
|
||||
env["COMMON_THIRD_PARTY_ENRICHMENT_MAX_CONCURRENCY"] = "2"
|
||||
env["COMMON_THIRD_PARTY_ENRICHMENT_STALE_AFTER"] = "1200"
|
||||
env["COMMON_THIRD_PARTY_ENRICHMENT_AGENT_TIMEOUT"] = "120"
|
||||
env["COMMON_THIRD_PARTY_ENRICHMENT_AGENT_MAX_TURNS"] = "8"
|
||||
env["COMMON_THIRD_PARTY_ENRICHMENT_CONFIDENCE_THRESHOLD"] = "0.85"
|
||||
env["COMMON_THIRD_PARTY_ENRICHMENT_MAX_ATTEMPTS"] = "5"
|
||||
env["THIRD_PARTY_VETTING_INTERVAL"] = "15"
|
||||
env["THIRD_PARTY_VETTING_STALE_AFTER"] = "1800"
|
||||
env["THIRD_PARTY_VETTING_MAX_CONCURRENCY"] = "2"
|
||||
env["PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_PROVIDER"] = "openai"
|
||||
env["PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MODEL_NAME"] = "gpt-4o"
|
||||
env["PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MAX_TOKENS"] = "16384"
|
||||
env["PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_INTERVAL"] = "25"
|
||||
env["PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_MAX_CONCURRENCY"] = "2"
|
||||
env["PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_STALE_AFTER"] = "1200"
|
||||
env["PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_AGENT_TIMEOUT"] = "120"
|
||||
env["PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_AGENT_MAX_TURNS"] = "8"
|
||||
env["PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_CONFIDENCE_THRESHOLD"] = "0.85"
|
||||
env["PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_MAX_ATTEMPTS"] = "5"
|
||||
env["PROBOD_THIRD_PARTY_VETTING_INTERVAL"] = "15"
|
||||
env["PROBOD_THIRD_PARTY_VETTING_STALE_AFTER"] = "1800"
|
||||
env["PROBOD_THIRD_PARTY_VETTING_MAX_CONCURRENCY"] = "2"
|
||||
// Custom domains
|
||||
env["CUSTOM_DOMAINS_RESOLVER_ADDR"] = "1.1.1.1:53"
|
||||
env["ACME_ACCOUNT_KEY"] = "-----BEGIN EC PRIVATE KEY-----\ntest\n-----END EC PRIVATE KEY-----"
|
||||
env["PROBOD_CUSTOM_DOMAINS_RESOLVER_ADDR"] = "1.1.1.1:53"
|
||||
env["PROBOD_ACME_ACCOUNT_KEY"] = "-----BEGIN EC PRIVATE KEY-----\ntest\n-----END EC PRIVATE KEY-----"
|
||||
// SCIM bridge
|
||||
env["SCIM_BRIDGE_SYNC_INTERVAL"] = "1800"
|
||||
env["SCIM_BRIDGE_POLL_INTERVAL"] = "60"
|
||||
env["PROBOD_SCIM_BRIDGE_SYNC_INTERVAL"] = "1800"
|
||||
env["PROBOD_SCIM_BRIDGE_POLL_INTERVAL"] = "60"
|
||||
// ESign
|
||||
env["ESIGN_TSA_URL"] = "http://custom.tsa.example.com"
|
||||
env["PROBOD_ESIGN_TSA_URL"] = "http://custom.tsa.example.com"
|
||||
// Branding
|
||||
env["BRANDING"] = "false"
|
||||
env["PROBOD_BRANDING"] = "false"
|
||||
|
||||
b := NewBuilder(mockEnv(env))
|
||||
b := NewBuilder(NewResolver(mockEnv(env)))
|
||||
b.samlCertificate = "test-cert"
|
||||
b.samlPrivateKey = "test-key"
|
||||
|
||||
@@ -545,10 +545,10 @@ func TestBuilder_Build_CustomValues(t *testing.T) {
|
||||
|
||||
func TestBuilder_Build_GoogleWorkspaceConnector(t *testing.T) {
|
||||
env := requiredEnv()
|
||||
env["CONNECTOR_GOOGLE_WORKSPACE_CLIENT_ID"] = "gw-client-id"
|
||||
env["CONNECTOR_GOOGLE_WORKSPACE_CLIENT_SECRET"] = "gw-client-secret"
|
||||
env["PROBOD_CONNECTOR_GOOGLE_WORKSPACE_CLIENT_ID"] = "gw-client-id"
|
||||
env["PROBOD_CONNECTOR_GOOGLE_WORKSPACE_CLIENT_SECRET"] = "gw-client-secret"
|
||||
|
||||
b := NewBuilder(mockEnv(env))
|
||||
b := NewBuilder(NewResolver(mockEnv(env)))
|
||||
b.samlCertificate = "test-cert"
|
||||
b.samlPrivateKey = "test-key"
|
||||
|
||||
@@ -566,10 +566,10 @@ func TestBuilder_Build_GoogleWorkspaceConnector(t *testing.T) {
|
||||
|
||||
func TestBuilder_Build_Microsoft365Connector(t *testing.T) {
|
||||
env := requiredEnv()
|
||||
env["CONNECTOR_MICROSOFT_365_CLIENT_ID"] = "ms365-client-id"
|
||||
env["CONNECTOR_MICROSOFT_365_CLIENT_SECRET"] = "ms365-client-secret"
|
||||
env["PROBOD_CONNECTOR_MICROSOFT_365_CLIENT_ID"] = "ms365-client-id"
|
||||
env["PROBOD_CONNECTOR_MICROSOFT_365_CLIENT_SECRET"] = "ms365-client-secret"
|
||||
|
||||
b := NewBuilder(mockEnv(env))
|
||||
b := NewBuilder(NewResolver(mockEnv(env)))
|
||||
b.samlCertificate = "test-cert"
|
||||
b.samlPrivateKey = "test-key"
|
||||
|
||||
@@ -592,16 +592,16 @@ func TestBuilder_Build_AccessReviewConnectors(t *testing.T) {
|
||||
providers := []string{
|
||||
"GITLAB", "BITBUCKET", "HEROKU", "PAGERDUTY",
|
||||
"ASANA", "NETLIFY", "CLICKUP", "MONDAY", "DATADOG",
|
||||
"ZENDESK",
|
||||
"ZENDESK", "LINEAR",
|
||||
}
|
||||
|
||||
env := requiredEnv()
|
||||
for _, provider := range providers {
|
||||
env["CONNECTOR_"+provider+"_CLIENT_ID"] = strings.ToLower(provider) + "-id"
|
||||
env["CONNECTOR_"+provider+"_CLIENT_SECRET"] = strings.ToLower(provider) + "-secret"
|
||||
env["PROBOD_CONNECTOR_"+provider+"_CLIENT_ID"] = strings.ToLower(provider) + "-id"
|
||||
env["PROBOD_CONNECTOR_"+provider+"_CLIENT_SECRET"] = strings.ToLower(provider) + "-secret"
|
||||
}
|
||||
|
||||
b := NewBuilder(mockEnv(env))
|
||||
b := NewBuilder(NewResolver(mockEnv(env)))
|
||||
b.samlCertificate = "test-cert"
|
||||
b.samlPrivateKey = "test-key"
|
||||
|
||||
@@ -628,11 +628,11 @@ func TestBuilder_Build_AccessReviewConnectors(t *testing.T) {
|
||||
|
||||
func TestBuilder_Build_VercelConnector(t *testing.T) {
|
||||
env := requiredEnv()
|
||||
env["CONNECTOR_VERCEL_CLIENT_ID"] = "vercel-id"
|
||||
env["CONNECTOR_VERCEL_CLIENT_SECRET"] = "vercel-secret"
|
||||
env["CONNECTOR_VERCEL_INTEGRATION_SLUG"] = "probo-app"
|
||||
env["PROBOD_CONNECTOR_VERCEL_CLIENT_ID"] = "vercel-id"
|
||||
env["PROBOD_CONNECTOR_VERCEL_CLIENT_SECRET"] = "vercel-secret"
|
||||
env["PROBOD_CONNECTOR_VERCEL_INTEGRATION_SLUG"] = "probo-app"
|
||||
|
||||
b := NewBuilder(mockEnv(env))
|
||||
b := NewBuilder(NewResolver(mockEnv(env)))
|
||||
b.samlCertificate = "test-cert"
|
||||
b.samlPrivateKey = "test-key"
|
||||
|
||||
@@ -651,11 +651,11 @@ func TestBuilder_Build_VercelConnector(t *testing.T) {
|
||||
|
||||
func TestBuilder_Build_SlackConnector(t *testing.T) {
|
||||
env := requiredEnv()
|
||||
env["CONNECTOR_SLACK_CLIENT_ID"] = "slack-client-id"
|
||||
env["CONNECTOR_SLACK_CLIENT_SECRET"] = "slack-client-secret"
|
||||
env["CONNECTOR_SLACK_SIGNING_SECRET"] = "slack-signing-secret"
|
||||
env["PROBOD_CONNECTOR_SLACK_CLIENT_ID"] = "slack-client-id"
|
||||
env["PROBOD_CONNECTOR_SLACK_CLIENT_SECRET"] = "slack-client-secret"
|
||||
env["PROBOD_CONNECTOR_SLACK_SIGNING_SECRET"] = "slack-signing-secret"
|
||||
|
||||
b := NewBuilder(mockEnv(env))
|
||||
b := NewBuilder(NewResolver(mockEnv(env)))
|
||||
b.samlCertificate = "test-cert"
|
||||
b.samlPrivateKey = "test-key"
|
||||
|
||||
@@ -675,7 +675,7 @@ func TestBuilder_Build_SlackConnector(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuilder_Build_SAMLAutoGeneration(t *testing.T) {
|
||||
b := NewBuilder(mockEnv(requiredEnv()))
|
||||
b := NewBuilder(NewResolver(mockEnv(requiredEnv())))
|
||||
|
||||
cfg, err := b.Build()
|
||||
require.NoError(t, err)
|
||||
@@ -688,10 +688,10 @@ func TestBuilder_Build_SAMLAutoGeneration(t *testing.T) {
|
||||
|
||||
func TestBuilder_Build_SAMLFromEnv(t *testing.T) {
|
||||
env := requiredEnv()
|
||||
env["SAML_CERTIFICATE"] = "env-cert"
|
||||
env["SAML_PRIVATE_KEY"] = "env-key"
|
||||
env["PROBOD_SAML_CERTIFICATE"] = "env-cert"
|
||||
env["PROBOD_SAML_PRIVATE_KEY"] = "env-key"
|
||||
|
||||
b := NewBuilder(mockEnv(env))
|
||||
b := NewBuilder(NewResolver(mockEnv(env)))
|
||||
|
||||
cfg, err := b.Build()
|
||||
require.NoError(t, err)
|
||||
@@ -701,7 +701,7 @@ func TestBuilder_Build_SAMLFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuilder_Build_SAMLPreset(t *testing.T) {
|
||||
b := NewBuilder(mockEnv(requiredEnv()))
|
||||
b := NewBuilder(NewResolver(mockEnv(requiredEnv())))
|
||||
b.samlCertificate = "preset-cert"
|
||||
b.samlPrivateKey = "preset-key"
|
||||
|
||||
@@ -713,7 +713,7 @@ func TestBuilder_Build_SAMLPreset(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuilder_Build_OAuth2Defaults(t *testing.T) {
|
||||
b := NewBuilder(mockEnv(requiredEnv()))
|
||||
b := NewBuilder(NewResolver(mockEnv(requiredEnv())))
|
||||
|
||||
cfg, err := b.Build()
|
||||
require.NoError(t, err)
|
||||
@@ -733,15 +733,15 @@ func TestBuilder_Build_OAuth2Defaults(t *testing.T) {
|
||||
|
||||
func TestBuilder_Build_OAuth2FromEnv(t *testing.T) {
|
||||
env := requiredEnv()
|
||||
env["OAUTH2_SERVER_SIGNING_KEY"] = "env-signing-key"
|
||||
env["OAUTH2_SERVER_SIGNING_KEY_KID"] = "env-kid"
|
||||
env["OAUTH2_SERVER_ACCESS_TOKEN_DURATION"] = "10"
|
||||
env["OAUTH2_SERVER_REFRESH_TOKEN_DURATION"] = "20"
|
||||
env["OAUTH2_SERVER_AUTHORIZATION_CODE_DURATION"] = "30"
|
||||
env["OAUTH2_SERVER_DEVICE_CODE_DURATION"] = "40"
|
||||
env["OAUTH2_SERVER_CIMD_ALLOWED_CLIENT_IDS"] = "https://chatgpt.com/oauth/client.json,https://claude.ai/oauth/client.json"
|
||||
env["PROBOD_OAUTH2_SERVER_SIGNING_KEY"] = "env-signing-key"
|
||||
env["PROBOD_OAUTH2_SERVER_SIGNING_KEY_KID"] = "env-kid"
|
||||
env["PROBOD_OAUTH2_SERVER_ACCESS_TOKEN_DURATION"] = "10"
|
||||
env["PROBOD_OAUTH2_SERVER_REFRESH_TOKEN_DURATION"] = "20"
|
||||
env["PROBOD_OAUTH2_SERVER_AUTHORIZATION_CODE_DURATION"] = "30"
|
||||
env["PROBOD_OAUTH2_SERVER_DEVICE_CODE_DURATION"] = "40"
|
||||
env["PROBOD_OAUTH2_SERVER_CIMD_ALLOWED_CLIENT_IDS"] = "https://chatgpt.com/oauth/client.json,https://claude.ai/oauth/client.json"
|
||||
|
||||
b := NewBuilder(mockEnv(env))
|
||||
b := NewBuilder(NewResolver(mockEnv(env)))
|
||||
|
||||
cfg, err := b.Build()
|
||||
require.NoError(t, err)
|
||||
@@ -770,7 +770,7 @@ func TestBuilder_Build_OAuth2Preset(t *testing.T) {
|
||||
env := requiredEnv()
|
||||
delete(env, "OAUTH2_SERVER_SIGNING_KEY")
|
||||
|
||||
b := NewBuilder(mockEnv(env))
|
||||
b := NewBuilder(NewResolver(mockEnv(env)))
|
||||
b.oauth2SigningKey = "preset-signing-key"
|
||||
|
||||
cfg, err := b.Build()
|
||||
@@ -782,9 +782,9 @@ func TestBuilder_Build_OAuth2Preset(t *testing.T) {
|
||||
|
||||
func TestBuilder_Build_PgCABundleFromEnv(t *testing.T) {
|
||||
env := requiredEnv()
|
||||
env["PG_CA_BUNDLE"] = "test-ca-bundle-content"
|
||||
env["PROBOD_PG_CA_BUNDLE"] = "test-ca-bundle-content"
|
||||
|
||||
b := NewBuilder(mockEnv(env))
|
||||
b := NewBuilder(NewResolver(mockEnv(env)))
|
||||
b.samlCertificate = "test-cert"
|
||||
b.samlPrivateKey = "test-key"
|
||||
|
||||
@@ -801,9 +801,9 @@ func TestBuilder_Build_PgCABundleFromFile(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
env := requiredEnv()
|
||||
env["PG_CA_BUNDLE_PATH"] = caFile
|
||||
env["PROBOD_PG_CA_BUNDLE_PATH"] = caFile
|
||||
|
||||
b := NewBuilder(mockEnv(env))
|
||||
b := NewBuilder(NewResolver(mockEnv(env)))
|
||||
b.samlCertificate = "test-cert"
|
||||
b.samlPrivateKey = "test-key"
|
||||
|
||||
|
||||
225
pkg/bootstrap/resolver.go
Normal file
225
pkg/bootstrap/resolver.go
Normal file
@@ -0,0 +1,225 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
|
||||
)
|
||||
|
||||
const awsSecretRefPrefix = "aws://"
|
||||
|
||||
type (
|
||||
SecretsManagerClient interface {
|
||||
GetSecretValue(
|
||||
ctx context.Context,
|
||||
params *secretsmanager.GetSecretValueInput,
|
||||
optFns ...func(*secretsmanager.Options),
|
||||
) (*secretsmanager.GetSecretValueOutput, error)
|
||||
}
|
||||
|
||||
Resolver struct {
|
||||
lookup EnvGetter
|
||||
secretsManagerClient SecretsManagerClient
|
||||
smCache map[string]string
|
||||
err error
|
||||
}
|
||||
)
|
||||
|
||||
func NewResolver(lookup EnvGetter) *Resolver {
|
||||
if lookup == nil {
|
||||
lookup = os.Getenv
|
||||
}
|
||||
|
||||
return &Resolver{lookup: lookup}
|
||||
}
|
||||
|
||||
func (r *Resolver) Err() error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
func (r *Resolver) getEnv(key string) string {
|
||||
if r.err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
value, err := r.resolve(key)
|
||||
if err != nil {
|
||||
r.err = err
|
||||
return ""
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func (r *Resolver) getEnvOrDefault(key, defaultValue string) string {
|
||||
if value := r.getEnv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (r *Resolver) getEnvIntOrDefault(key string, defaultValue int) int {
|
||||
if value := r.getEnv(key); value != "" {
|
||||
if intValue, err := strconv.ParseInt(value, 10, 32); err == nil {
|
||||
return int(intValue)
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (r *Resolver) getEnvFloatOrDefault(key string, defaultValue float64) float64 {
|
||||
if value := r.getEnv(key); value != "" {
|
||||
if floatValue, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return floatValue
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (r *Resolver) getEnvFloatPtr(key string) *float64 {
|
||||
if value := r.getEnv(key); value != "" {
|
||||
if floatValue, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return &floatValue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Resolver) getEnvIntPtr(key string) *int {
|
||||
if value := r.getEnv(key); value != "" {
|
||||
if intValue, err := strconv.ParseInt(value, 10, 32); err == nil {
|
||||
v := int(intValue)
|
||||
return &v
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Resolver) getEnvBoolOrDefault(key string, defaultValue bool) bool {
|
||||
if value := r.getEnv(key); value != "" {
|
||||
if boolValue, err := strconv.ParseBool(value); err == nil {
|
||||
return boolValue
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (r *Resolver) resolve(key string) (string, error) {
|
||||
raw := r.lookup(key)
|
||||
|
||||
secretID, ok := parseAWSSecretRef(raw)
|
||||
if !ok {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
value, err := r.loadPlaintextSecret(secretID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot resolve %s: %w", key, err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) loadPlaintextSecret(secretID string) (string, error) {
|
||||
if r.smCache != nil {
|
||||
if value, ok := r.smCache[secretID]; ok {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
|
||||
value, err := fetchPlaintextSecret(
|
||||
context.Background(),
|
||||
secretsManagerOptions{
|
||||
SecretID: secretID,
|
||||
Client: r.secretsManagerClient,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if r.smCache == nil {
|
||||
r.smCache = make(map[string]string)
|
||||
}
|
||||
|
||||
r.smCache[secretID] = value
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func parseAWSSecretRef(value string) (string, bool) {
|
||||
if !strings.HasPrefix(value, awsSecretRefPrefix) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
secretID := strings.TrimPrefix(value, awsSecretRefPrefix)
|
||||
if secretID == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return secretID, true
|
||||
}
|
||||
|
||||
type secretsManagerOptions struct {
|
||||
SecretID string
|
||||
Client SecretsManagerClient
|
||||
}
|
||||
|
||||
func fetchPlaintextSecret(ctx context.Context, opts secretsManagerOptions) (string, error) {
|
||||
client, err := secretsManagerClient(ctx, opts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
out, err := client.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{
|
||||
SecretId: aws.String(opts.SecretID),
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot load secret from AWS Secrets Manager: %w", err)
|
||||
}
|
||||
|
||||
if out.SecretString == nil || *out.SecretString == "" {
|
||||
return "", fmt.Errorf("secret %q has an empty SecretString", opts.SecretID)
|
||||
}
|
||||
|
||||
return *out.SecretString, nil
|
||||
}
|
||||
|
||||
func secretsManagerClient(ctx context.Context, opts secretsManagerOptions) (SecretsManagerClient, error) {
|
||||
if opts.Client != nil {
|
||||
return opts.Client, nil
|
||||
}
|
||||
|
||||
cfg, err := config.LoadDefaultConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot load AWS config: %w", err)
|
||||
}
|
||||
|
||||
return secretsmanager.NewFromConfig(cfg), nil
|
||||
}
|
||||
179
pkg/bootstrap/resolver_test.go
Normal file
179
pkg/bootstrap/resolver_test.go
Normal file
@@ -0,0 +1,179 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockSecretsManagerClient struct {
|
||||
secrets map[string]string
|
||||
called []string
|
||||
}
|
||||
|
||||
func (m *mockSecretsManagerClient) GetSecretValue(
|
||||
_ context.Context,
|
||||
params *secretsmanager.GetSecretValueInput,
|
||||
_ ...func(*secretsmanager.Options),
|
||||
) (*secretsmanager.GetSecretValueOutput, error) {
|
||||
secretID := aws.ToString(params.SecretId)
|
||||
m.called = append(m.called, secretID)
|
||||
|
||||
value, ok := m.secrets[secretID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("secret %q not found", secretID)
|
||||
}
|
||||
|
||||
return &secretsmanager.GetSecretValueOutput{
|
||||
SecretString: aws.String(value),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func testResolver(env map[string]string, client SecretsManagerClient) *Resolver {
|
||||
r := NewResolver(mockEnv(env))
|
||||
r.secretsManagerClient = client
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func TestResolver_ResolveLiteralEnv(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewResolver(mockEnv(map[string]string{"PROBOD_FOO": "bar"}))
|
||||
assert.Equal(t, "bar", r.getEnv("PROBOD_FOO"))
|
||||
require.NoError(t, r.Err())
|
||||
}
|
||||
|
||||
func TestBuilder_Build_ResolvesAWSSecretRefs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := &mockSecretsManagerClient{
|
||||
secrets: map[string]string{
|
||||
"probo/sandbox/probod/encryption_key": "test-encryption-key-32-bytes-long",
|
||||
"probo/sandbox/probod/cookie_secret": "test-cookie-secret-32-bytes-long!",
|
||||
"probo/sandbox/probod/password_pepper": "test-password-pepper-32-bytes-lo",
|
||||
"probo/sandbox/probod/oauth2_signing_key": "test-oauth2-signing-key",
|
||||
},
|
||||
}
|
||||
|
||||
env := map[string]string{
|
||||
"PROBOD_ENCRYPTION_KEY": "aws://probo/sandbox/probod/encryption_key",
|
||||
"PROBOD_AUTH_COOKIE_SECRET": "aws://probo/sandbox/probod/cookie_secret",
|
||||
"PROBOD_AUTH_PASSWORD_PEPPER": "aws://probo/sandbox/probod/password_pepper",
|
||||
"PROBOD_OAUTH2_SERVER_SIGNING_KEY": "aws://probo/sandbox/probod/oauth2_signing_key",
|
||||
"PROBOD_BASE_URL": "https://app.example.com",
|
||||
}
|
||||
b := NewBuilder(testResolver(env, client))
|
||||
|
||||
cfg, err := b.Build()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, client.called, 4)
|
||||
|
||||
assert.Equal(t, "test-encryption-key-32-bytes-long", cfg.Probod.EncryptionKey)
|
||||
assert.Equal(t, "https://app.example.com", cfg.Probod.BaseURL)
|
||||
}
|
||||
|
||||
func TestResolver_ResolveAWSSecretRefsCachesBySecretID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := &mockSecretsManagerClient{
|
||||
secrets: map[string]string{
|
||||
"probo/sandbox/probod/encryption_key": "secret-value",
|
||||
},
|
||||
}
|
||||
r := testResolver(map[string]string{
|
||||
"PROBOD_ENCRYPTION_KEY": "aws://probo/sandbox/probod/encryption_key",
|
||||
"PROBOD_AUTH_COOKIE_SECRET": "aws://probo/sandbox/probod/encryption_key",
|
||||
"PROBOD_AUTH_PASSWORD_PEPPER": "plaintext-pepper",
|
||||
"PROBOD_OAUTH2_SERVER_SIGNING_KEY": "plaintext-oauth2",
|
||||
}, client)
|
||||
|
||||
assert.Equal(t, "secret-value", r.getEnv("PROBOD_ENCRYPTION_KEY"))
|
||||
assert.Equal(t, "secret-value", r.getEnv("PROBOD_AUTH_COOKIE_SECRET"))
|
||||
require.NoError(t, r.Err())
|
||||
require.Len(t, client.called, 1)
|
||||
}
|
||||
|
||||
func TestBuilder_Build_MixedPlaintextAndAWSSecrets(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := &mockSecretsManagerClient{
|
||||
secrets: map[string]string{
|
||||
"probo/sandbox/probod/encryption_key": "test-encryption-key-32-bytes-long",
|
||||
},
|
||||
}
|
||||
b := NewBuilder(testResolver(map[string]string{
|
||||
"PROBOD_ENCRYPTION_KEY": "aws://probo/sandbox/probod/encryption_key",
|
||||
"PROBOD_AUTH_COOKIE_SECRET": "test-cookie-secret-32-bytes-long!",
|
||||
"PROBOD_AUTH_PASSWORD_PEPPER": "test-password-pepper-32-bytes-lo",
|
||||
"PROBOD_OAUTH2_SERVER_SIGNING_KEY": "test-oauth2-signing-key",
|
||||
}, client))
|
||||
|
||||
cfg, err := b.Build()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test-encryption-key-32-bytes-long", cfg.Probod.EncryptionKey)
|
||||
assert.Equal(t, "test-cookie-secret-32-bytes-long!", cfg.Probod.Auth.Cookie.Secret)
|
||||
}
|
||||
|
||||
func TestResolver_ResolveAWSSecretRefMissingSecret(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := &mockSecretsManagerClient{secrets: map[string]string{}}
|
||||
r := testResolver(map[string]string{
|
||||
"PROBOD_ENCRYPTION_KEY": "aws://probo/sandbox/probod/encryption_key",
|
||||
}, client)
|
||||
|
||||
assert.Empty(t, r.getEnv("PROBOD_ENCRYPTION_KEY"))
|
||||
require.Error(t, r.Err())
|
||||
assert.Contains(t, r.Err().Error(), "cannot resolve PROBOD_ENCRYPTION_KEY")
|
||||
}
|
||||
|
||||
func TestResolver_ResolveAWSSecretRefEmptySecretString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := &mockSecretsManagerClient{
|
||||
secrets: map[string]string{
|
||||
"probo/sandbox/probod/encryption_key": "",
|
||||
},
|
||||
}
|
||||
r := testResolver(map[string]string{
|
||||
"PROBOD_ENCRYPTION_KEY": "aws://probo/sandbox/probod/encryption_key",
|
||||
}, client)
|
||||
|
||||
assert.Empty(t, r.getEnv("PROBOD_ENCRYPTION_KEY"))
|
||||
require.Error(t, r.Err())
|
||||
assert.Contains(t, r.Err().Error(), "empty SecretString")
|
||||
}
|
||||
|
||||
func TestParseAWSSecretRef(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
secretID, ok := parseAWSSecretRef("aws://probo/sandbox/probod/encryption_key")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "probo/sandbox/probod/encryption_key", secretID)
|
||||
|
||||
_, ok = parseAWSSecretRef("literal-value")
|
||||
assert.False(t, ok)
|
||||
|
||||
_, ok = parseAWSSecretRef("aws://")
|
||||
assert.False(t, ok)
|
||||
}
|
||||
Reference in New Issue
Block a user