Files
unkinben 373d21a744 initial implementation: encapi ENC server + CLI
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
2026-07-04 23:45:15 +10:00

87 lines
2.6 KiB
Go

// Package distro resolves per-host provisioning parameters (e.g. epel version,
// operating system release) from an external kickstart/distro API.
//
// This is the seam that will let encapi take over the provisioning-param half
// of Cobbler's old ENC output. Today those params (epel, tree,
// operatingsystemrelease, from_cobbler) are unused by the Puppet manifests, so
// the resolver is OFF by default: with no API configured, Resolve returns nil
// and no distro params are injected.
package distro
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
// Resolver returns provisioning parameters for a host, or nil if none apply.
type Resolver interface {
Resolve(ctx context.Context, certname string) (map[string]any, error)
}
// Noop is the default resolver: it injects nothing.
type Noop struct{}
// Resolve always returns nil.
func (Noop) Resolve(context.Context, string) (map[string]any, error) { return nil, nil }
// HTTPResolver queries an external distro API of the form
// GET {BaseURL}/{certname} -> {"params": {...}} (or a bare JSON object).
type HTTPResolver struct {
BaseURL string
Client *http.Client
}
// New returns a Noop resolver when baseURL is empty, otherwise an HTTPResolver.
func New(baseURL string) Resolver {
if baseURL == "" {
return Noop{}
}
return &HTTPResolver{
BaseURL: baseURL,
Client: &http.Client{Timeout: 5 * time.Second},
}
}
// Resolve fetches provisioning params for certname. A 404 means "no params for
// this host" and yields nil, nil rather than an error, so ENC rendering never
// fails just because a host is unknown to the distro API.
func (h *HTTPResolver) Resolve(ctx context.Context, certname string) (map[string]any, error) {
endpoint := fmt.Sprintf("%s/%s", h.BaseURL, url.PathEscape(certname))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
resp, err := h.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("distro api request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, nil
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("distro api returned HTTP %d for %q", resp.StatusCode, certname)
}
// Accept either {"params": {...}} or a bare {...} object.
var wrapper struct {
Params map[string]any `json:"params"`
}
dec := json.NewDecoder(resp.Body)
raw := map[string]any{}
if err := dec.Decode(&raw); err != nil {
return nil, fmt.Errorf("decode distro api response: %w", err)
}
if p, ok := raw["params"].(map[string]any); ok {
wrapper.Params = p
} else {
wrapper.Params = raw
}
return wrapper.Params, nil
}