Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
4d683075a1
|
|||
|
f7af38f9ea
|
@@ -25,6 +25,7 @@ The current schema defines one `pawsql` block with a listener, an optional TLS `
|
|||||||
|
|
||||||
- `upstream <host:port>` for an external PostgreSQL server.
|
- `upstream <host:port>` for an external PostgreSQL server.
|
||||||
- `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.
|
- `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
|
```text
|
||||||
pawsql {
|
pawsql {
|
||||||
@@ -48,11 +49,7 @@ pawsql {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Create the referenced secret before deploying 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.
|
||||||
|
|
||||||
```sh
|
|
||||||
docker secret create barkstack_application_postgres_password /secure/path/application-postgres-password
|
|
||||||
```
|
|
||||||
|
|
||||||
## Watch validated changes
|
## Watch validated changes
|
||||||
|
|
||||||
|
|||||||
89
parser.go
89
parser.go
@@ -48,10 +48,51 @@ type parser struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) parse() (Config, error) {
|
func (p *parser) parse() (Config, error) {
|
||||||
|
var cfg Config
|
||||||
|
pawSQLSeen := false
|
||||||
|
for {
|
||||||
p.skipNewlines()
|
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
|
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 {
|
if err := p.expect(tokenOpenBrace, "{"); err != nil {
|
||||||
return Config{}, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
@@ -60,7 +101,7 @@ func (p *parser) parse() (Config, error) {
|
|||||||
p.skipNewlines()
|
p.skipNewlines()
|
||||||
if p.current().kind == tokenCloseBrace {
|
if p.current().kind == tokenCloseBrace {
|
||||||
p.index++
|
p.index++
|
||||||
break
|
return cfg, nil
|
||||||
}
|
}
|
||||||
if p.current().kind == tokenEOF {
|
if p.current().kind == tokenEOF {
|
||||||
return Config{}, p.errorf("expected } to close pawsql block")
|
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)
|
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
|
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) {
|
func (p *parser) parseTLS() (TLSConfig, error) {
|
||||||
if err := p.expect(tokenOpenBrace, "{"); err != nil {
|
if err := p.expect(tokenOpenBrace, "{"); err != nil {
|
||||||
|
|||||||
@@ -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) {
|
func TestParseMalformedBlocksReportLine(t *testing.T) {
|
||||||
_, err := Parse([]byte("pawsql {\n tls {\n cert cert.pem\n"))
|
_, err := Parse([]byte("pawsql {\n tls {\n cert cert.pem\n"))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ type Config struct {
|
|||||||
Listen string
|
Listen string
|
||||||
TLS TLSConfig
|
TLS TLSConfig
|
||||||
Databases []DatabaseConfig
|
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.
|
// TLSConfig identifies the certificate material used to terminate client TLS.
|
||||||
|
|||||||
21
validate.go
21
validate.go
@@ -14,7 +14,15 @@ func NormalizeHostname(hostname string) string {
|
|||||||
return strings.TrimSuffix(strings.ToLower(strings.TrimSpace(hostname)), ".")
|
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_]*$`)
|
var secretReferencePattern = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)
|
||||||
|
|
||||||
@@ -40,6 +48,14 @@ func (c Config) Validate() error {
|
|||||||
if len(c.Databases) == 0 {
|
if len(c.Databases) == 0 {
|
||||||
errs = append(errs, errors.New("at least one database route is required"))
|
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))
|
seenHostnames := make(map[string]string, len(c.Databases))
|
||||||
seenNames := make(map[string]struct{}, 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 {
|
if err := validatePostgres(*database.Postgres); err != nil {
|
||||||
errs = append(errs, fmt.Errorf("database %q: postgres: %w", name, err))
|
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) == "" {
|
} else if strings.TrimSpace(database.Upstream) == "" {
|
||||||
errs = append(errs, fmt.Errorf("database %q: upstream or postgres is required", name))
|
errs = append(errs, fmt.Errorf("database %q: upstream or postgres is required", name))
|
||||||
} else if err := validateAddress(database.Upstream); err != nil {
|
} else if err := validateAddress(database.Upstream); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user