Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
4d683075a1
|
|||
|
f7af38f9ea
|
|||
|
34ccc01c26
|
|||
|
c3ad476b1a
|
|||
|
1851797a3f
|
13
README.md
13
README.md
@@ -5,7 +5,7 @@
|
||||
## Install
|
||||
|
||||
```sh
|
||||
go get git.campbellwireless.net/barkstack/barkfile-parser@v0.1.0
|
||||
go get cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2@v2.0.0
|
||||
```
|
||||
|
||||
The module requires Go 1.24 or later.
|
||||
@@ -21,10 +21,11 @@ if err != nil {
|
||||
|
||||
`Load` reads, parses, and validates a file. `Parse` parses bytes when a caller owns file I/O; call `Config.Validate` before applying a parsed configuration.
|
||||
|
||||
The current schema defines one `pawsql` block with a listener, TLS certificate/key paths, and one or more `database` routes. A route has exactly one of:
|
||||
The current schema defines one `pawsql` block with a listener, an optional TLS `cert`/`key` pair, and one or more `database` routes. A route has exactly one of:
|
||||
|
||||
- `upstream <host:port>` for an external PostgreSQL server.
|
||||
- `postgres { ... }` for a managed PostgreSQL container. Its `image`, `volume`, and `password_env` directives are required. `idle_timeout` and `traffic_idle_timeout` accept Go duration strings.
|
||||
- `postgres { ... }` for a managed PostgreSQL container. Its `image`, `volume`, and `password_secret` directives are required. `password_secret` is a short lowercase reference resolved to the Docker secret `barkstack_<reference>`. `idle_timeout` and `traffic_idle_timeout` accept Go duration strings.
|
||||
- `treatvault { ... }` (optional, top-level) configures TreatVault: `file` is the path to the age-encrypted secret source of truth, and `identity_secret` is a short reference for the Docker secret holding the age identity (`barkstack_<reference>`).
|
||||
|
||||
```text
|
||||
pawsql {
|
||||
@@ -40,7 +41,7 @@ pawsql {
|
||||
postgres {
|
||||
image postgres:18
|
||||
volume application-data
|
||||
password_env APPLICATION_POSTGRES_PASSWORD
|
||||
password_secret application_postgres_password
|
||||
idle_timeout 10m
|
||||
traffic_idle_timeout 1h
|
||||
}
|
||||
@@ -48,6 +49,8 @@ pawsql {
|
||||
}
|
||||
```
|
||||
|
||||
With TreatVault configured, create the referenced password through the TreatVault page in the Barkstack Console. `barkstack init` creates the identity Docker secret when absent; the TreatVault service initializes the encrypted file and creates the Docker secret on sync.
|
||||
|
||||
## Watch validated changes
|
||||
|
||||
```go
|
||||
@@ -80,4 +83,4 @@ Gitea Actions runs tests on each `main` push. The release job reads Conventional
|
||||
| `type!:` or `BREAKING CHANGE:` | major |
|
||||
| other types | no release |
|
||||
|
||||
Go consumers update through standard module versions, for example `go get git.campbellwireless.net/barkstack/barkfile-parser@latest`.
|
||||
Go consumers update through standard module versions, for example `go get cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2@latest`.
|
||||
|
||||
2
go.mod
2
go.mod
@@ -1,3 +1,3 @@
|
||||
module git.campbellwireless.net/barkstack/barkfile-parser
|
||||
module cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2
|
||||
|
||||
go 1.24
|
||||
|
||||
97
parser.go
97
parser.go
@@ -48,10 +48,51 @@ type parser struct {
|
||||
}
|
||||
|
||||
func (p *parser) parse() (Config, error) {
|
||||
var cfg Config
|
||||
pawSQLSeen := false
|
||||
for {
|
||||
p.skipNewlines()
|
||||
if err := p.expectWord("pawsql"); err != nil {
|
||||
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,12 +148,50 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) parseTLS() (TLSConfig, error) {
|
||||
if err := p.expect(tokenOpenBrace, "{"); err != nil {
|
||||
@@ -247,11 +326,11 @@ func (p *parser) parsePostgres() (PostgresConfig, error) {
|
||||
return PostgresConfig{}, fmt.Errorf("line %d: volume may only be specified once", directive.line)
|
||||
}
|
||||
cfg.Volume = value
|
||||
case "password_env":
|
||||
if cfg.PasswordEnv != "" {
|
||||
return PostgresConfig{}, fmt.Errorf("line %d: password_env may only be specified once", directive.line)
|
||||
case "password_secret":
|
||||
if cfg.PasswordSecret != "" {
|
||||
return PostgresConfig{}, fmt.Errorf("line %d: password_secret may only be specified once", directive.line)
|
||||
}
|
||||
cfg.PasswordEnv = value
|
||||
cfg.PasswordSecret = value
|
||||
case "idle_timeout":
|
||||
if idleTimeoutSet {
|
||||
return PostgresConfig{}, fmt.Errorf("line %d: idle_timeout may only be specified once", directive.line)
|
||||
|
||||
136
parser_test.go
136
parser_test.go
@@ -81,13 +81,59 @@ func TestValidateMissingFieldsAndDuplicateHostname(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatal("Validate() error = nil")
|
||||
}
|
||||
for _, want := range []string{"TLS private key is required", "upstream or postgres is required", "duplicate hostname"} {
|
||||
for _, want := range []string{"tls requires both cert and key", "upstream or postgres is required", "duplicate hostname"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("Validate() error = %q, missing %q", err, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAllowsConfigurationWithoutTLS(t *testing.T) {
|
||||
cfg := Config{
|
||||
Listen: ":5432",
|
||||
Databases: []DatabaseConfig{{
|
||||
Name: "analytics",
|
||||
Upstream: "postgres-foo:5432",
|
||||
}},
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAndValidateWithoutTLSBlock(t *testing.T) {
|
||||
cfg, err := Parse([]byte(`pawsql {
|
||||
listen :5432
|
||||
database analytics {
|
||||
upstream postgres-foo:5432
|
||||
}
|
||||
}`))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse() error = %v", err)
|
||||
}
|
||||
if cfg.TLS.CertFile != "" || cfg.TLS.KeyFile != "" {
|
||||
t.Errorf("TLS = %#v, want unset", cfg.TLS)
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsHalfConfiguredTLS(t *testing.T) {
|
||||
cfg := Config{
|
||||
Listen: ":5432",
|
||||
TLS: TLSConfig{CertFile: "cert.pem"},
|
||||
Databases: []DatabaseConfig{{
|
||||
Name: "analytics",
|
||||
Upstream: "postgres-foo:5432",
|
||||
}},
|
||||
}
|
||||
err := cfg.Validate()
|
||||
if err == nil || !strings.Contains(err.Error(), "tls requires both cert and key") {
|
||||
t.Fatalf("Validate() error = %v, want tls requires both cert and key", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsMalformedUpstreamAddress(t *testing.T) {
|
||||
cfg := Config{
|
||||
Listen: ":5432",
|
||||
@@ -144,7 +190,7 @@ func TestParsePostgresContainer(t *testing.T) {
|
||||
postgres {
|
||||
image postgres:18
|
||||
volume analytics-data
|
||||
password_env ANALYTICS_POSTGRES_PASSWORD
|
||||
password_secret analytics_password
|
||||
idle_timeout 15m
|
||||
traffic_idle_timeout 1h
|
||||
}
|
||||
@@ -154,7 +200,7 @@ func TestParsePostgresContainer(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
database := cfg.Databases[0]
|
||||
if database.Postgres == nil || database.Postgres.Image != "postgres:18" || database.Postgres.Volume != "analytics-data" || database.Postgres.PasswordEnv != "ANALYTICS_POSTGRES_PASSWORD" || database.Postgres.IdleTimeout != 15*time.Minute || database.Postgres.TrafficIdleTimeout != time.Hour {
|
||||
if database.Postgres == nil || database.Postgres.Image != "postgres:18" || database.Postgres.Volume != "analytics-data" || database.Postgres.PasswordSecret != "analytics_password" || database.Postgres.IdleTimeout != 15*time.Minute || database.Postgres.TrafficIdleTimeout != time.Hour {
|
||||
t.Errorf("Postgres = %#v", database.Postgres)
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
@@ -173,7 +219,7 @@ func TestParseRejectsInvalidPostgresIdleTimeout(t *testing.T) {
|
||||
postgres {
|
||||
image postgres:18
|
||||
volume analytics-data
|
||||
password_env ANALYTICS_POSTGRES_PASSWORD
|
||||
password_secret analytics_password
|
||||
idle_timeout whenever
|
||||
}
|
||||
}
|
||||
@@ -194,7 +240,7 @@ func TestParseRejectsInvalidPostgresTrafficIdleTimeout(t *testing.T) {
|
||||
postgres {
|
||||
image postgres:18
|
||||
volume analytics-data
|
||||
password_env ANALYTICS_POSTGRES_PASSWORD
|
||||
password_secret analytics_password
|
||||
traffic_idle_timeout whenever
|
||||
}
|
||||
}
|
||||
@@ -211,7 +257,7 @@ func TestValidateAllowsSupportedPostgresImages(t *testing.T) {
|
||||
TLS: TLSConfig{CertFile: "cert.pem", KeyFile: "key.pem"},
|
||||
Databases: []DatabaseConfig{{
|
||||
Name: "analytics",
|
||||
Postgres: &PostgresConfig{Image: image, Volume: "analytics-data", PasswordEnv: "ANALYTICS_POSTGRES_PASSWORD"},
|
||||
Postgres: &PostgresConfig{Image: image, Volume: "analytics-data", PasswordSecret: "analytics_password"},
|
||||
}},
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
@@ -226,7 +272,7 @@ func TestValidateRejectsUnsupportedPostgresImage(t *testing.T) {
|
||||
TLS: TLSConfig{CertFile: "cert.pem", KeyFile: "key.pem"},
|
||||
Databases: []DatabaseConfig{{
|
||||
Name: "analytics",
|
||||
Postgres: &PostgresConfig{Image: "postgres:15", Volume: "analytics-data", PasswordEnv: "ANALYTICS_POSTGRES_PASSWORD"},
|
||||
Postgres: &PostgresConfig{Image: "postgres:15", Volume: "analytics-data", PasswordSecret: "analytics_password"},
|
||||
}},
|
||||
}
|
||||
err := cfg.Validate()
|
||||
@@ -235,6 +281,82 @@ func TestValidateRejectsUnsupportedPostgresImage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsInvalidSecretReference(t *testing.T) {
|
||||
for _, reference := range []string{"", "UPPERCASE", "../secret", "has-hyphen", strings.Repeat("a", 55)} {
|
||||
cfg := Config{
|
||||
Listen: ":5432",
|
||||
Databases: []DatabaseConfig{{
|
||||
Name: "analytics",
|
||||
Postgres: &PostgresConfig{
|
||||
Image: "postgres:18",
|
||||
Volume: "analytics-data",
|
||||
PasswordSecret: reference,
|
||||
},
|
||||
}},
|
||||
}
|
||||
err := cfg.Validate()
|
||||
if err == nil || !strings.Contains(err.Error(), "password_secret") {
|
||||
t.Errorf("Validate() reference %q error = %v, want password_secret error", reference, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerSecretNamePrefixesShortReference(t *testing.T) {
|
||||
if got := DockerSecretName("analytics_password"); got != "barkstack_analytics_password" {
|
||||
t.Fatalf("DockerSecretName() = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
18
schema.go
18
schema.go
@@ -8,6 +8,14 @@ type Config struct {
|
||||
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.
|
||||
@@ -26,13 +34,15 @@ type DatabaseConfig struct {
|
||||
Postgres *PostgresConfig
|
||||
}
|
||||
|
||||
// PostgresConfig declares a PawSQL-managed PostgreSQL container. IdleTimeout
|
||||
// starts after the last proxied client session closes. TrafficIdleTimeout
|
||||
// applies to open sessions with no proxied bytes. Zero disables either timeout.
|
||||
// PostgresConfig declares a PawSQL-managed PostgreSQL container. PasswordSecret
|
||||
// is a short Barkfile reference resolved to a Docker secret named
|
||||
// barkstack_<reference>. IdleTimeout starts after the last proxied client
|
||||
// session closes. TrafficIdleTimeout applies to open sessions with no proxied
|
||||
// bytes. Zero disables either timeout.
|
||||
type PostgresConfig struct {
|
||||
Image string
|
||||
Volume string
|
||||
PasswordEnv string
|
||||
PasswordSecret string
|
||||
IdleTimeout time.Duration
|
||||
TrafficIdleTimeout time.Duration
|
||||
}
|
||||
|
||||
49
validate.go
49
validate.go
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -13,6 +14,24 @@ func NormalizeHostname(hostname string) string {
|
||||
return strings.TrimSuffix(strings.ToLower(strings.TrimSpace(hostname)), ".")
|
||||
}
|
||||
|
||||
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_]*$`)
|
||||
|
||||
// DockerSecretName returns the namespaced Docker object name for a validated
|
||||
// short Barkfile secret reference.
|
||||
func DockerSecretName(reference string) string {
|
||||
return DockerSecretPrefix + reference
|
||||
}
|
||||
|
||||
// Validate verifies configuration invariants that do not require opening files.
|
||||
func (c Config) Validate() error {
|
||||
var errs []error
|
||||
@@ -21,15 +40,22 @@ func (c Config) Validate() error {
|
||||
} else if err := validateListenAddress(c.Listen); err != nil {
|
||||
errs = append(errs, fmt.Errorf("listen address %q: %w", c.Listen, err))
|
||||
}
|
||||
if strings.TrimSpace(c.TLS.CertFile) == "" {
|
||||
errs = append(errs, errors.New("TLS certificate is required"))
|
||||
}
|
||||
if strings.TrimSpace(c.TLS.KeyFile) == "" {
|
||||
errs = append(errs, errors.New("TLS private key is required"))
|
||||
certificateSet := strings.TrimSpace(c.TLS.CertFile) != ""
|
||||
keySet := strings.TrimSpace(c.TLS.KeyFile) != ""
|
||||
if certificateSet != keySet {
|
||||
errs = append(errs, errors.New("tls requires both cert and key"))
|
||||
}
|
||||
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))
|
||||
@@ -57,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 {
|
||||
@@ -75,8 +104,8 @@ func validatePostgres(postgres PostgresConfig) error {
|
||||
if strings.TrimSpace(postgres.Volume) == "" {
|
||||
return errors.New("volume is required")
|
||||
}
|
||||
if strings.TrimSpace(postgres.PasswordEnv) == "" {
|
||||
return errors.New("password_env is required")
|
||||
if !ValidSecretReference(postgres.PasswordSecret) {
|
||||
return fmt.Errorf("password_secret %q must start with a lowercase letter, contain only lowercase letters, digits, or underscores, and be at most 54 characters", postgres.PasswordSecret)
|
||||
}
|
||||
if postgres.IdleTimeout < 0 {
|
||||
return errors.New("idle_timeout cannot be negative")
|
||||
@@ -87,6 +116,12 @@ func validatePostgres(postgres PostgresConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidSecretReference reports whether reference is safe for use as a short
|
||||
// Barkfile secret name and as part of a Docker secret name.
|
||||
func ValidSecretReference(reference string) bool {
|
||||
return len(reference) <= 54 && secretReferencePattern.MatchString(reference)
|
||||
}
|
||||
|
||||
func validateListenAddress(address string) error {
|
||||
_, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user