f1bcb8cd3a
repospawner turns JSON new-repo requests into terraform-git pull requests via kubernetes Jobs, follows those PRs to merge and optionally activates the repository in Woodpecker.
148 lines
5.8 KiB
Go
148 lines
5.8 KiB
Go
// Package config loads repospawner runtime configuration from the environment.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// Config is the fully-resolved repospawner configuration. The same struct is
|
|
// loaded by the server and by the job subcommands, which run from the same
|
|
// image with the same environment.
|
|
type Config struct {
|
|
// Listen is the HTTP listen address, e.g. ":8080".
|
|
Listen string
|
|
// Namespace is where request Jobs are created and looked up.
|
|
Namespace string
|
|
// Image is repospawner's own image reference; Jobs run it with a
|
|
// different argv, so the deployment must pass its own tag through.
|
|
Image string
|
|
// JobServiceAccount is the service account the request Jobs run as; it
|
|
// carries the projected vault-audience token.
|
|
JobServiceAccount string
|
|
// GiteaURL is the forge base URL, e.g. https://git.unkin.net.
|
|
GiteaURL string
|
|
// TFGitRepo is the "owner/name" of the terraform-git repository whose
|
|
// config tree owns repository definitions.
|
|
TFGitRepo string
|
|
// VaultAddr, VaultK8sMount and VaultK8sRole drive the native kubernetes
|
|
// auth login used to mint short-lived Gitea tokens.
|
|
VaultAddr string
|
|
VaultK8sMount string
|
|
VaultK8sRole string
|
|
// VaultSATokenPath is the projected service account token with the
|
|
// "vault" audience.
|
|
VaultSATokenPath string
|
|
// GiteaCredsPath is the vault path of the dynamic Gitea credential.
|
|
GiteaCredsPath string
|
|
// WoodpeckerServer is the CI server base URL.
|
|
WoodpeckerServer string
|
|
// WoodpeckerTokenFile holds the Woodpecker API token. Absent means
|
|
// Woodpecker enablement is unavailable, not fatal.
|
|
WoodpeckerTokenFile string
|
|
// WoodpeckerSecret is the Secret the enablement Job mounts to obtain
|
|
// WoodpeckerTokenFile; the server reads the same file to decide whether
|
|
// enablement is offered at all.
|
|
WoodpeckerSecret string
|
|
// GroupsHeader is the oauth2-proxy header carrying Authentik group names.
|
|
GroupsHeader string
|
|
// AllowedGroups gates every page load and API call. Never empty.
|
|
AllowedGroups []string
|
|
}
|
|
|
|
// Load resolves configuration from the environment, failing closed on an empty
|
|
// allow-list (an empty list would authorize nobody or, worse, be read as
|
|
// "anyone" by a future refactor).
|
|
func Load() (*Config, error) {
|
|
c := &Config{
|
|
Listen: envOr("REPOSPAWNER_LISTEN", ":8080"),
|
|
Namespace: envOr("REPOSPAWNER_NAMESPACE", "repospawner"),
|
|
Image: envOr("REPOSPAWNER_IMAGE", ""),
|
|
JobServiceAccount: envOr("REPOSPAWNER_JOB_SERVICE_ACCOUNT", "repospawner"),
|
|
GiteaURL: envOr("GITEA_URL", "https://git.unkin.net"),
|
|
TFGitRepo: envOr("REPOSPAWNER_TFGIT_REPO", "unkin/terraform-git"),
|
|
VaultAddr: envOr("VAULT_ADDR", "https://vault.service.consul:8200"),
|
|
VaultK8sMount: envOr("REPOSPAWNER_VAULT_K8S_MOUNT", "k8s/au/syd1"),
|
|
VaultK8sRole: envOr("REPOSPAWNER_VAULT_K8S_ROLE", "repospawner"),
|
|
VaultSATokenPath: envOr("REPOSPAWNER_VAULT_SA_TOKEN_PATH", "/var/run/secrets/vault/token"),
|
|
GiteaCredsPath: envOr("REPOSPAWNER_GITEA_CREDS_PATH", "gitea/creds/repospawner"),
|
|
WoodpeckerServer: envOr("WOODPECKER_SERVER", "https://ci.k8s.syd1.au.unkin.net"),
|
|
WoodpeckerTokenFile: envOr("REPOSPAWNER_WOODPECKER_TOKEN_FILE", "/etc/repospawner/woodpecker/token"),
|
|
WoodpeckerSecret: envOr("REPOSPAWNER_WOODPECKER_SECRET", "repospawner-woodpecker"),
|
|
GroupsHeader: envOr("REPOSPAWNER_GROUPS_HEADER", "X-Forwarded-Groups"),
|
|
AllowedGroups: ParseGroups(envOr("REPOSPAWNER_ALLOWED_GROUPS", "akP-repospawner-user")),
|
|
}
|
|
|
|
if len(c.AllowedGroups) == 0 {
|
|
return nil, fmt.Errorf("REPOSPAWNER_ALLOWED_GROUPS must name at least one group")
|
|
}
|
|
if strings.TrimSpace(c.GroupsHeader) == "" {
|
|
return nil, fmt.Errorf("REPOSPAWNER_GROUPS_HEADER must not be empty")
|
|
}
|
|
if strings.TrimSpace(c.Namespace) == "" {
|
|
return nil, fmt.Errorf("REPOSPAWNER_NAMESPACE must not be empty")
|
|
}
|
|
if strings.TrimSpace(c.JobServiceAccount) == "" {
|
|
return nil, fmt.Errorf("REPOSPAWNER_JOB_SERVICE_ACCOUNT must not be empty")
|
|
}
|
|
if strings.TrimSpace(c.VaultK8sMount) == "" || strings.TrimSpace(c.VaultK8sRole) == "" {
|
|
return nil, fmt.Errorf("REPOSPAWNER_VAULT_K8S_MOUNT and REPOSPAWNER_VAULT_K8S_ROLE must not be empty")
|
|
}
|
|
if strings.TrimSpace(c.GiteaCredsPath) == "" {
|
|
return nil, fmt.Errorf("REPOSPAWNER_GITEA_CREDS_PATH must not be empty")
|
|
}
|
|
if err := requireHTTP("GITEA_URL", c.GiteaURL); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := requireHTTP("VAULT_ADDR", c.VaultAddr); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := requireHTTP("WOODPECKER_SERVER", c.WoodpeckerServer); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, _, err := SplitRepo(c.TFGitRepo); err != nil {
|
|
return nil, fmt.Errorf("REPOSPAWNER_TFGIT_REPO: %w", err)
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// SplitRepo splits an "owner/name" reference.
|
|
func SplitRepo(s string) (owner, name string, err error) {
|
|
owner, name, ok := strings.Cut(strings.TrimSpace(s), "/")
|
|
if !ok || owner == "" || name == "" || strings.Contains(name, "/") {
|
|
return "", "", fmt.Errorf("%q is not owner/name", s)
|
|
}
|
|
return owner, name, nil
|
|
}
|
|
|
|
// ParseGroups splits a group list tolerating both comma and whitespace
|
|
// separation, dropping empties. oauth2-proxy emits comma-separated groups but
|
|
// deployments hand-write the allow-list.
|
|
func ParseGroups(s string) []string {
|
|
fields := strings.FieldsFunc(s, func(r rune) bool {
|
|
return r == ',' || r == ' ' || r == '\t' || r == '\n' || r == '\r' || r == ';'
|
|
})
|
|
out := make([]string, 0, len(fields))
|
|
for _, f := range fields {
|
|
if f = strings.TrimSpace(f); f != "" {
|
|
out = append(out, f)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func requireHTTP(name, val string) error {
|
|
if !strings.HasPrefix(val, "http://") && !strings.HasPrefix(val, "https://") {
|
|
return fmt.Errorf("%s %q must be an http(s) URL", name, val)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func envOr(key, def string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|