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
87 lines
2.6 KiB
Go
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
|
|
}
|