fix: map unknown role/environment to 422, set environment in one UPDATE
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

This commit is contained in:
2026-10-23 21:39:41 +11:00
parent 8c1bfda4c5
commit 033a5b0a61
7 changed files with 131 additions and 11 deletions
+34 -4
View File
@@ -127,13 +127,13 @@ func TestNodeCRUDAndForeignKeys(t *testing.T) {
// node referencing an unknown role must fail the FK
badRole := &models.Node{Certname: "h1", Role: "roles::ghost", Environment: "testing"}
if err := testDB.UpsertNode(ctx, badRole); err == nil {
t.Error("expected FK violation for unknown role")
if err := testDB.UpsertNode(ctx, badRole); !errors.Is(err, ErrInvalidReference) {
t.Errorf("UpsertNode unknown role = %v, want ErrInvalidReference", err)
}
// node referencing an unknown environment must fail the FK
badEnv := &models.Node{Certname: "h1", Role: "roles::base", Environment: "ghost"}
if err := testDB.UpsertNode(ctx, badEnv); err == nil {
t.Error("expected FK violation for unknown environment")
if err := testDB.UpsertNode(ctx, badEnv); !errors.Is(err, ErrInvalidReference) {
t.Errorf("UpsertNode unknown environment = %v, want ErrInvalidReference", err)
}
n := &models.Node{Certname: "h1", Role: "roles::base", Environment: "testing", Params: map[string]any{"x": "y"}}
@@ -168,3 +168,33 @@ func TestNodeCRUDAndForeignKeys(t *testing.T) {
t.Errorf("DeleteNode missing = %v, want ErrNotFound", err)
}
}
func TestSetNodeEnvironment(t *testing.T) {
requireDB(t)
clean(t)
seed(t)
ctx := context.Background()
if err := testDB.UpsertStatus(ctx, &models.Status{Name: "production"}); err != nil {
t.Fatal(err)
}
n := &models.Node{Certname: "h1", Role: "roles::base", Environment: "production", Params: map[string]any{"x": "y"}}
if err := testDB.UpsertNode(ctx, n); err != nil {
t.Fatal(err)
}
got, err := testDB.SetNodeEnvironment(ctx, "h1", "testing")
if err != nil || got.Environment != "testing" || got.Role != "roles::base" || got.Params["x"] != "y" {
t.Fatalf("SetNodeEnvironment = %+v, %v", got, err)
}
stored, _ := testDB.GetNode(ctx, "h1")
if stored.Environment != "testing" || stored.Params["x"] != "y" {
t.Errorf("stored node = %+v", stored)
}
if _, err := testDB.SetNodeEnvironment(ctx, "h1", "ghost"); !errors.Is(err, ErrInvalidReference) {
t.Errorf("unknown environment = %v, want ErrInvalidReference", err)
}
if _, err := testDB.SetNodeEnvironment(ctx, "missing", "testing"); !errors.Is(err, ErrNotFound) {
t.Errorf("unknown node = %v, want ErrNotFound", err)
}
}
+39 -1
View File
@@ -6,10 +6,24 @@ import (
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"git.unkin.net/unkin/encapi/pkg/models"
)
// ErrInvalidReference is returned when a node names a role or environment that
// does not exist.
var ErrInvalidReference = errors.New("unknown role or environment")
// fkErr maps a foreign-key violation to ErrInvalidReference.
func fkErr(err error) error {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23503" {
return ErrInvalidReference
}
return err
}
// UpsertNode creates or updates a host-to-role assignment. The referenced role
// and environment must already exist (enforced by foreign keys).
func (db *DB) UpsertNode(ctx context.Context, n *models.Node) error {
@@ -27,11 +41,35 @@ func (db *DB) UpsertNode(ctx context.Context, n *models.Node) error {
updated_at = NOW()
`, n.Certname, n.Role, n.Environment, params)
if err != nil {
return fmt.Errorf("upsert node %q: %w", n.Certname, err)
return fmt.Errorf("upsert node %q: %w", n.Certname, fkErr(err))
}
return nil
}
// SetNodeEnvironment changes only a node's environment in a single statement and
// returns the updated node, ErrNotFound, or ErrInvalidReference.
func (db *DB) SetNodeEnvironment(ctx context.Context, certname, environment string) (*models.Node, error) {
var (
n models.Node
params []byte
)
err := db.Pool.QueryRow(ctx, `
UPDATE nodes SET environment = $2, updated_at = NOW()
WHERE certname = $1
RETURNING certname, role, environment, params
`, certname, environment).Scan(&n.Certname, &n.Role, &n.Environment, &params)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("set environment for node %q: %w", certname, fkErr(err))
}
if n.Params, err = unmarshalParams(params); err != nil {
return nil, fmt.Errorf("decode params for node %q: %w", certname, err)
}
return &n, nil
}
// GetNode returns a single node or ErrNotFound.
func (db *DB) GetNode(ctx context.Context, certname string) (*models.Node, error) {
var (