1 Commits

Author SHA1 Message Date
f7af38f9ea feat: add treatvault block and TreatVault label constants
All checks were successful
Test and Release Module / test (push) Successful in 20s
Test and Release Module / release (push) Successful in 11s
2026-09-16 15:47:46 -04:00
4 changed files with 168 additions and 12 deletions

View File

@@ -48,10 +48,51 @@ type parser struct {
}
func (p *parser) parse() (Config, error) {
p.skipNewlines()
if err := p.expectWord("pawsql"); err != nil {
return Config{}, err
var cfg Config
pawSQLSeen := false
for {
p.skipNewlines()
if p.current().kind == tokenEOF {
break
}
if p.current().kind != tokenWord {
return Config{}, p.errorf("expected top-level service block")
}
switch p.current().text {
case "pawsql":
if pawSQLSeen {
return Config{}, p.errorf("pawsql may only be specified once")
}
pawSQLSeen = true
p.index++
pawSQL, err := p.parsePawSQL()
if err != nil {
return Config{}, err
}
cfg.Listen = pawSQL.Listen
cfg.TLS = pawSQL.TLS
cfg.Databases = pawSQL.Databases
case "treatvault":
if cfg.TreatVault != nil {
return Config{}, p.errorf("treatvault may only be specified once")
}
p.index++
treatVault, err := p.parseTreatVault()
if err != nil {
return Config{}, err
}
cfg.TreatVault = &treatVault
default:
return Config{}, p.errorf("unknown top-level service %q", p.current().text)
}
}
if !pawSQLSeen {
return Config{}, p.errorf("pawsql block is required")
}
return cfg, nil
}
func (p *parser) parsePawSQL() (Config, error) {
if err := p.expect(tokenOpenBrace, "{"); err != nil {
return Config{}, err
}
@@ -60,7 +101,7 @@ func (p *parser) parse() (Config, error) {
p.skipNewlines()
if p.current().kind == tokenCloseBrace {
p.index++
break
return cfg, nil
}
if p.current().kind == tokenEOF {
return Config{}, p.errorf("expected } to close pawsql block")
@@ -107,11 +148,49 @@ func (p *parser) parse() (Config, error) {
return Config{}, p.errorf("unknown directive %q", p.current().text)
}
}
p.skipNewlines()
if p.current().kind != tokenEOF {
return Config{}, p.errorf("unexpected content after pawsql block")
}
func (p *parser) parseTreatVault() (TreatVaultConfig, error) {
if err := p.expect(tokenOpenBrace, "{"); err != nil {
return TreatVaultConfig{}, err
}
var cfg TreatVaultConfig
for {
p.skipNewlines()
if p.current().kind == tokenCloseBrace {
p.index++
return cfg, nil
}
if p.current().kind == tokenEOF {
return TreatVaultConfig{}, p.errorf("expected } to close treatvault block")
}
directive := p.current()
if directive.kind != tokenWord {
return TreatVaultConfig{}, p.errorf("expected treatvault directive")
}
p.index++
value, err := p.value("treatvault value")
if err != nil {
return TreatVaultConfig{}, err
}
switch directive.text {
case "file":
if cfg.File != "" {
return TreatVaultConfig{}, fmt.Errorf("line %d: file may only be specified once", directive.line)
}
cfg.File = value
case "identity_secret":
if cfg.IdentitySecret != "" {
return TreatVaultConfig{}, fmt.Errorf("line %d: identity_secret may only be specified once", directive.line)
}
cfg.IdentitySecret = value
default:
return TreatVaultConfig{}, fmt.Errorf("line %d: unknown treatvault directive %q", directive.line, directive.text)
}
if err := p.endLine(); err != nil {
return TreatVaultConfig{}, err
}
}
return cfg, nil
}
func (p *parser) parseTLS() (TLSConfig, error) {

View File

@@ -307,6 +307,56 @@ func TestDockerSecretNamePrefixesShortReference(t *testing.T) {
}
}
func TestParseTreatVaultConfiguration(t *testing.T) {
cfg, err := Parse([]byte(`treatvault {
file ./secrets/treatvault.age
identity_secret treatvault_identity
}
pawsql {
listen :5432
database analytics {
postgres {
image postgres:18
volume analytics-data
password_secret analytics_password
}
}
}`))
if err != nil {
t.Fatal(err)
}
if cfg.TreatVault == nil || cfg.TreatVault.File != "./secrets/treatvault.age" || cfg.TreatVault.IdentitySecret != "treatvault_identity" {
t.Fatalf("TreatVault = %#v", cfg.TreatVault)
}
if err := cfg.Validate(); err != nil {
t.Fatalf("Validate() error = %v", err)
}
}
func TestValidateRejectsInvalidTreatVaultConfiguration(t *testing.T) {
cfg := Config{
Listen: ":5432",
TreatVault: &TreatVaultConfig{IdentitySecret: "UPPERCASE"},
Databases: []DatabaseConfig{{
Name: "analytics",
Postgres: &PostgresConfig{
Image: "postgres:18",
Volume: "analytics-data",
PasswordSecret: "UPPERCASE",
},
}},
}
err := cfg.Validate()
if err == nil {
t.Fatal("Validate() error = nil")
}
for _, want := range []string{"treatvault file is required", "treatvault identity_secret", "password_secret must not reference"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("Validate() error = %q, missing %q", err, want)
}
}
}
func TestParseMalformedBlocksReportLine(t *testing.T) {
_, err := Parse([]byte("pawsql {\n tls {\n cert cert.pem\n"))
if err == nil {

View File

@@ -5,9 +5,17 @@ import "time"
// Config is the typed representation of a Barkfile.
type Config struct {
Listen string
TLS TLSConfig
Databases []DatabaseConfig
Listen string
TLS TLSConfig
Databases []DatabaseConfig
TreatVault *TreatVaultConfig
}
// TreatVaultConfig identifies the encrypted secret source of truth and the
// bootstrap Docker secret containing its age X25519 identity.
type TreatVaultConfig struct {
File string
IdentitySecret string
}
// TLSConfig identifies the certificate material used to terminate client TLS.

View File

@@ -14,7 +14,15 @@ func NormalizeHostname(hostname string) string {
return strings.TrimSuffix(strings.ToLower(strings.TrimSpace(hostname)), ".")
}
const DockerSecretPrefix = "barkstack_"
const (
DockerSecretPrefix = "barkstack_"
TreatVaultManagedLabel = "io.barkstack.treatvault"
TreatVaultSecretNameLabel = "io.barkstack.treatvault.name"
TreatVaultVaultIDLabel = "io.barkstack.treatvault.vault"
TreatVaultRevisionLabel = "io.barkstack.treatvault.revision"
TreatVaultConsumerLabel = "io.barkstack.treatvault.secrets"
TreatVaultNamesLabel = "io.barkstack.treatvault.names"
)
var secretReferencePattern = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)
@@ -40,6 +48,14 @@ func (c Config) Validate() error {
if len(c.Databases) == 0 {
errs = append(errs, errors.New("at least one database route is required"))
}
if c.TreatVault != nil {
if strings.TrimSpace(c.TreatVault.File) == "" {
errs = append(errs, errors.New("treatvault file is required"))
}
if !ValidSecretReference(c.TreatVault.IdentitySecret) {
errs = append(errs, fmt.Errorf("treatvault identity_secret %q must start with a lowercase letter, contain only lowercase letters, digits, or underscores, and be at most 54 characters", c.TreatVault.IdentitySecret))
}
}
seenHostnames := make(map[string]string, len(c.Databases))
seenNames := make(map[string]struct{}, len(c.Databases))
@@ -67,6 +83,9 @@ func (c Config) Validate() error {
if err := validatePostgres(*database.Postgres); err != nil {
errs = append(errs, fmt.Errorf("database %q: postgres: %w", name, err))
}
if c.TreatVault != nil && database.Postgres.PasswordSecret == c.TreatVault.IdentitySecret {
errs = append(errs, fmt.Errorf("database %q: password_secret must not reference the TreatVault identity secret", name))
}
} else if strings.TrimSpace(database.Upstream) == "" {
errs = append(errs, fmt.Errorf("database %q: upstream or postgres is required", name))
} else if err := validateAddress(database.Upstream); err != nil {