373d21a744
Postgres-backed External Node Classifier for Puppet, replacing Cobbler. - encapi HTTP server (chi + pgx): read/write API + two ENC document shapes (reshaped for the exec terminus; cobbler-wire for enc_direct_facts.rb) - encapi-cli: classify/node/role/status CRUD + import-cobbler seeder - pkg/client Go SDK; unit tests across all packages (DB via testcontainers) - Dockerfile (distroless), Makefile, nfpm RPM (encapi-cli + encapi-enc wrapper), Woodpecker CI, docs/cutover.md
76 lines
2.0 KiB
Go
76 lines
2.0 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"git.unkin.net/unkin/encapi/pkg/models"
|
|
)
|
|
|
|
// ErrNotFound is returned when a requested entity does not exist.
|
|
var ErrNotFound = errors.New("not found")
|
|
|
|
// UpsertStatus creates or updates a status (Puppet environment).
|
|
func (db *DB) UpsertStatus(ctx context.Context, s *models.Status) error {
|
|
_, err := db.Pool.Exec(ctx, `
|
|
INSERT INTO statuses (name, description)
|
|
VALUES ($1, $2)
|
|
ON CONFLICT (name) DO UPDATE
|
|
SET description = EXCLUDED.description, updated_at = NOW()
|
|
`, s.Name, s.Description)
|
|
if err != nil {
|
|
return fmt.Errorf("upsert status %q: %w", s.Name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetStatus returns a single status or ErrNotFound.
|
|
func (db *DB) GetStatus(ctx context.Context, name string) (*models.Status, error) {
|
|
var s models.Status
|
|
err := db.Pool.QueryRow(ctx,
|
|
`SELECT name, description FROM statuses WHERE name = $1`, name,
|
|
).Scan(&s.Name, &s.Description)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get status %q: %w", name, err)
|
|
}
|
|
return &s, nil
|
|
}
|
|
|
|
// ListStatuses returns all statuses ordered by name.
|
|
func (db *DB) ListStatuses(ctx context.Context) ([]models.Status, error) {
|
|
rows, err := db.Pool.Query(ctx, `SELECT name, description FROM statuses ORDER BY name`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list statuses: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
out := []models.Status{}
|
|
for rows.Next() {
|
|
var s models.Status
|
|
if err := rows.Scan(&s.Name, &s.Description); err != nil {
|
|
return nil, fmt.Errorf("scan status: %w", err)
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// DeleteStatus removes a status. It fails if any node still references it
|
|
// (enforced by the nodes.environment foreign key).
|
|
func (db *DB) DeleteStatus(ctx context.Context, name string) error {
|
|
tag, err := db.Pool.Exec(ctx, `DELETE FROM statuses WHERE name = $1`, name)
|
|
if err != nil {
|
|
return fmt.Errorf("delete status %q: %w", name, err)
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|