145 lines
5.8 KiB
Go
145 lines
5.8 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"flag"
|
||
|
|
"fmt"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type options struct {
|
||
|
|
dsnFile string
|
||
|
|
plan string
|
||
|
|
renderedPlan string
|
||
|
|
secretRoot string
|
||
|
|
cli string
|
||
|
|
stalwartURL string
|
||
|
|
certificateSAN string
|
||
|
|
updateApps bool
|
||
|
|
oidcIssuerFile string
|
||
|
|
oidcAudienceFile string
|
||
|
|
oidcClientIDFile string
|
||
|
|
oidcClientSecretFile string
|
||
|
|
appPasswords string
|
||
|
|
vaultAddress string
|
||
|
|
vaultRole string
|
||
|
|
vaultTokenFile string
|
||
|
|
vaultCACertFile string
|
||
|
|
bootstrapUser string
|
||
|
|
bootstrapPasswordFile string
|
||
|
|
statusAddress string
|
||
|
|
interval time.Duration
|
||
|
|
initialBackoff time.Duration
|
||
|
|
maxBackoff time.Duration
|
||
|
|
reconcileTimeout time.Duration
|
||
|
|
persistentFailureAfter int
|
||
|
|
standbyReady bool
|
||
|
|
once bool
|
||
|
|
}
|
||
|
|
|
||
|
|
func parseOptions(arguments []string) (options, error) {
|
||
|
|
var value options
|
||
|
|
flags := flag.NewFlagSet("stalwart-controller", flag.ContinueOnError)
|
||
|
|
flags.StringVar(&value.dsnFile, "dsn-file", "", "PostgreSQL DSN file used for controller ownership")
|
||
|
|
flags.StringVar(&value.plan, "plan", "", "Stalwart desired-state plan")
|
||
|
|
flags.StringVar(&value.renderedPlan, "rendered-plan", "/tmp/stalwart-plan.ndjson", "Rendered plan destination")
|
||
|
|
flags.StringVar(&value.secretRoot, "secret-root", "/run/control", "Root for plan secret-file references")
|
||
|
|
flags.StringVar(&value.cli, "cli", "/usr/local/bin/stalwart-cli", "Stalwart CLI executable")
|
||
|
|
flags.StringVar(&value.stalwartURL, "stalwart-url", "", "Stalwart management URL")
|
||
|
|
flags.StringVar(&value.certificateSAN, "certificate-san", "", "SAN used to select the default certificate")
|
||
|
|
flags.BoolVar(&value.updateApps, "update-apps", false, "Refresh embedded web applications")
|
||
|
|
flags.StringVar(&value.oidcIssuerFile, "oidc-issuer-file", "", "OIDC issuer URL file")
|
||
|
|
flags.StringVar(&value.oidcAudienceFile, "oidc-audience-file", "", "OIDC audience file")
|
||
|
|
flags.StringVar(&value.oidcClientIDFile, "oidc-client-id-file", "", "OIDC client ID file")
|
||
|
|
flags.StringVar(&value.oidcClientSecretFile, "oidc-client-secret-file", "", "OIDC client secret file")
|
||
|
|
flags.StringVar(&value.appPasswords, "app-passwords", "", "Managed app-password identity file")
|
||
|
|
flags.StringVar(&value.vaultAddress, "vault-address", "", "Vault API address")
|
||
|
|
flags.StringVar(&value.vaultRole, "vault-role", "", "Vault Kubernetes authentication role")
|
||
|
|
flags.StringVar(&value.vaultTokenFile, "vault-token-file", "", "Vault service-account token file")
|
||
|
|
flags.StringVar(&value.vaultCACertFile, "vault-ca-cert-file", "", "Vault CA certificate file")
|
||
|
|
flags.StringVar(&value.bootstrapUser, "bootstrap-user", "", "Temporary recovery username")
|
||
|
|
flags.StringVar(&value.bootstrapPasswordFile, "bootstrap-password-file", "", "Temporary recovery password file")
|
||
|
|
flags.StringVar(&value.statusAddress, "status-address", "127.0.0.1:9090", "Status server listen address")
|
||
|
|
flags.DurationVar(&value.interval, "interval", 5*time.Minute, "Successful reconciliation interval")
|
||
|
|
flags.DurationVar(&value.initialBackoff, "initial-backoff", 5*time.Second, "Initial failure retry delay")
|
||
|
|
flags.DurationVar(&value.maxBackoff, "max-backoff", 5*time.Minute, "Maximum failure retry delay")
|
||
|
|
flags.DurationVar(&value.reconcileTimeout, "reconcile-timeout", 2*time.Minute, "Per-reconciliation timeout")
|
||
|
|
flags.IntVar(&value.persistentFailureAfter, "persistent-failure-after", 5, "Failures before relinquishing ownership")
|
||
|
|
flags.BoolVar(&value.standbyReady, "standby-ready", false, "Report a healthy lock contender as ready")
|
||
|
|
flags.BoolVar(&value.once, "once", false, "Reconcile once without ownership or status serving")
|
||
|
|
if err := flags.Parse(arguments); err != nil {
|
||
|
|
return options{}, err
|
||
|
|
}
|
||
|
|
return value, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func allOrNone(name string, fields ...string) (bool, error) {
|
||
|
|
configured := 0
|
||
|
|
for _, field := range fields {
|
||
|
|
if field != "" {
|
||
|
|
configured++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if configured != 0 && configured != len(fields) {
|
||
|
|
return false, fmt.Errorf("%s fields must be set together", name)
|
||
|
|
}
|
||
|
|
return configured != 0, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func validateOptions(value options) error {
|
||
|
|
if value.plan == "" {
|
||
|
|
return errors.New("plan is required")
|
||
|
|
}
|
||
|
|
if value.stalwartURL == "" {
|
||
|
|
return errors.New("stalwart-url is required")
|
||
|
|
}
|
||
|
|
if !value.once && value.dsnFile == "" {
|
||
|
|
return errors.New("dsn-file is required in controller mode")
|
||
|
|
}
|
||
|
|
oidcConfigured, err := allOrNone(
|
||
|
|
"OIDC",
|
||
|
|
value.oidcIssuerFile,
|
||
|
|
value.oidcAudienceFile,
|
||
|
|
value.oidcClientIDFile,
|
||
|
|
value.oidcClientSecretFile,
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
vaultConfigured, err := allOrNone(
|
||
|
|
"vault",
|
||
|
|
value.vaultAddress,
|
||
|
|
value.vaultRole,
|
||
|
|
value.vaultTokenFile,
|
||
|
|
value.vaultCACertFile,
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if value.appPasswords != "" && !vaultConfigured {
|
||
|
|
return errors.New("app-password identities require vault")
|
||
|
|
}
|
||
|
|
if value.appPasswords != "" && !oidcConfigured {
|
||
|
|
return errors.New("app-password identities require OIDC issuer and audience")
|
||
|
|
}
|
||
|
|
bootstrapConfigured, err := allOrNone("bootstrap", value.bootstrapUser, value.bootstrapPasswordFile)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if oidcConfigured && bootstrapConfigured {
|
||
|
|
return errors.New("OIDC and bootstrap credentials are mutually exclusive")
|
||
|
|
}
|
||
|
|
if !value.once && !oidcConfigured && !bootstrapConfigured {
|
||
|
|
return errors.New("controller mode requires OIDC or bootstrap credentials")
|
||
|
|
}
|
||
|
|
if value.interval <= 0 || value.initialBackoff <= 0 || value.maxBackoff <= 0 {
|
||
|
|
return errors.New("interval and backoff durations must be positive")
|
||
|
|
}
|
||
|
|
if value.initialBackoff > value.maxBackoff {
|
||
|
|
return errors.New("initial backoff exceeds maximum backoff")
|
||
|
|
}
|
||
|
|
if value.reconcileTimeout <= 0 || value.persistentFailureAfter <= 0 {
|
||
|
|
return errors.New("reconcile timeout and failure threshold must be positive")
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|