quorate/services/stalwart/cmd/controller/options.go

145 lines
5.8 KiB
Go
Raw Permalink Normal View History

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
}