Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion server/internal/api/apiv1/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,28 @@ func validateAuthFileGUCs(conf map[string]any, path validation.Path) []error {
return errs
}

func validateConfLibraries(conf map[string]any, path validation.Path) []error {
// param names are case-insensitive, so comparison is done after casting to lowercase
for key, val := range conf {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add a comment here noting that we can't do a simple lookup because parameter names are case-insensitive? Your implementation is good, and I want to make sure we don't accidentally change it in the future.

if strings.ToLower(strings.TrimSpace(key)) != "shared_preload_libraries" {
continue
}
strVal, ok := val.(string)
if !ok {
err := fmt.Errorf("%q must be a string", key)
return []error{validation.NewError(err, path.AppendMapKey(key))}
}
for _, lib := range strings.Split(strVal, ",") {
if strings.TrimSpace(lib) == "spock" {
return nil
}
}
err := errors.New(`"spock" must be included in shared_preload_libraries`)
return []error{validation.NewError(err, path.AppendMapKey(key))}
}
return nil // key absent; defaults will be applied, including spock
}

// validatePgHbaConf checks that every non-comment pg_hba_conf entry parses.
// Blank and comment lines are allowed and skipped. Validation is intentionally
// minimal — see server/internal/postgres/hba/parse.go.
Expand Down Expand Up @@ -134,8 +156,9 @@ func validateDatabaseSpec(orchestrator config.Orchestrator, databaseID string, s
}

// Reject postgresql_conf GUCs that would make user-supplied pg_hba/pg_ident
// entries ineffective, then validate the entries themselves.
// entries ineffective or remove spock, then validate the entries themselves.
errs = append(errs, validateAuthFileGUCs(spec.PostgresqlConf, validation.NewPath("postgresql_conf"))...)
errs = append(errs, validateConfLibraries(spec.PostgresqlConf, validation.NewPath("postgresql_conf"))...)
errs = append(errs, validatePgHbaConf(spec.PgHbaConf, validation.NewPath("pg_hba_conf"))...)
errs = append(errs, validatePgIdentConf(spec.PgIdentConf, validation.NewPath("pg_ident_conf"))...)

Expand Down Expand Up @@ -234,6 +257,14 @@ func validateDatabaseUpdate(old *database.Spec, new *api.DatabaseSpec) error {
errs = append(errs, validateServiceSpec(svc, svcPath, isExistingService, old.DatabaseID, new.DatabaseUsers, newNodeNames)...)
}

// Validate that shared_preload_libraries, if explicitly set, still includes
// "spock" — both at the spec level and on individual nodes.
errs = append(errs, validateConfLibraries(new.PostgresqlConf, validation.NewPath("postgresql_conf"))...)
for i, n := range new.Nodes {
nodePath := validation.NewPath("nodes", validation.ArrayIndexElement(i))
errs = append(errs, validateConfLibraries(n.PostgresqlConf, nodePath.Append("postgresql_conf"))...)
}

return errors.Join(errs...)
}

Expand Down Expand Up @@ -281,6 +312,7 @@ func validateNode(
}

errs = append(errs, validateAuthFileGUCs(node.PostgresqlConf, path.Append("postgresql_conf"))...)
errs = append(errs, validateConfLibraries(node.PostgresqlConf, path.Append("postgresql_conf"))...)
errs = append(errs, validatePgHbaConf(node.PgHbaConf, path.Append("pg_hba_conf"))...)
errs = append(errs, validatePgIdentConf(node.PgIdentConf, path.Append("pg_ident_conf"))...)

Expand Down
117 changes: 117 additions & 0 deletions server/internal/api/apiv1/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,92 @@ func TestValidateBackupConfig(t *testing.T) {
}
}

func TestValidateDatabaseUpdate_LibraryConf(t *testing.T) {
for _, tc := range []struct {
name string
old *database.Spec
new *api.DatabaseSpec
expected []string
}{
{
name: "absent valid",
old: &database.Spec{},
new: &api.DatabaseSpec{},
},
{
name: "invalid spec-level conf missing spock",
old: &database.Spec{},
new: &api.DatabaseSpec{
PostgresqlConf: map[string]any{"shared_preload_libraries": "pg_stat_statements"},
},
expected: []string{`"spock" must be included in shared_preload_libraries`},
},
{
name: "invalid node-level conf missing spock",
old: &database.Spec{},
new: &api.DatabaseSpec{
Nodes: []*api.DatabaseNodeSpec{
{PostgresqlConf: map[string]any{"shared_preload_libraries": "pg_stat_statements"}},
},
},
expected: []string{`"spock" must be included in shared_preload_libraries`},
},
} {
t.Run(tc.name, func(t *testing.T) {
err := validateDatabaseUpdate(tc.old, tc.new)
if len(tc.expected) < 1 {
assert.NoError(t, err)
} else {
for _, expected := range tc.expected {
assert.ErrorContains(t, err, expected)
}
}
})
}
}

func TestValidateConfLibraries(t *testing.T) {
for _, tc := range []struct {
name string
conf map[string]any
expected []string
}{
{
name: "absent valid",
},
{
name: "valid spock conf",
conf: map[string]any{"shared_preload_libraries": "snowflake, spock"},
},
{
name: "valid uppercase param name",
conf: map[string]any{"SHARED_PRELOAD_LIBRARIES": "snowflake, spock"},
},
{
name: "valid conf spock and custom",
conf: map[string]any{"shared_preload_libraries": "snowflake, spock, custom_lib"},
},
{
name: "invalid conf missing spock",
conf: map[string]any{"shared_preload_libraries": "snowflake"},
expected: []string{
`"spock" must be included in shared_preload_libraries`,
},
},
} {
t.Run(tc.name, func(t *testing.T) {
err := errors.Join(validateConfLibraries(tc.conf, nil)...)
if len(tc.expected) < 1 {
assert.NoError(t, err)
} else {
for _, expected := range tc.expected {
assert.ErrorContains(t, err, expected)
}
}
})
}
}

func TestValidateNode(t *testing.T) {
for _, tc := range []struct {
name string
Expand Down Expand Up @@ -574,6 +660,20 @@ func TestValidateNode(t *testing.T) {
"restore_config.repository.base_path: base_path must be absolute for posix repositories",
},
},
{
name: "invalid lib conf",
orchestrator: config.OrchestratorSwarm,
db: &api.DatabaseSpec{},
node: &api.DatabaseNodeSpec{
HostIds: []api.Identifier{
api.Identifier("host-1"),
},
PostgresqlConf: map[string]any{"shared_preload_libraries": "snowflake"},
},
expected: []string{
`"spock" must be included in shared_preload_libraries`,
},
},
} {
t.Run(tc.name, func(t *testing.T) {
err := errors.Join(validateNode(tc.orchestrator, tc.db, tc.node, nil)...)
Expand Down Expand Up @@ -1307,6 +1407,23 @@ func TestValidateDatabaseSpec(t *testing.T) {
`nodes[0].postgresql_conf[HBA_FILE]: "HBA_FILE" is not allowed`,
},
},
{
name: "invalid lib conf",
spec: &api.DatabaseSpec{
Nodes: []*api.DatabaseNodeSpec{
{
Name: "n1",
HostIds: []api.Identifier{
api.Identifier("host-1"),
},
PostgresqlConf: map[string]any{"shared_preload_libraries": "snowflake"},
},
},
},
expected: []string{
`"spock" must be included in shared_preload_libraries`,
},
},
} {
t.Run(tc.name, func(t *testing.T) {
err := validateDatabaseSpec(config.OrchestratorSwarm, "test-db", tc.spec)
Expand Down