Add encapic ENC client
The uv/python ENC script fails its first-invocation dependency resolution on fresh compiler pods (exits 135/2), which breaks puppet agent catalog compilation. encapic is a stdlib-only Go replacement with no runtime dependency resolution. - Add encapic CLI: fetch the cobbler-wire ENC document from encapi (ENCAPI_URL override, in-cluster default compiled in) and reshape it to match the python script byte-for-byte (classes to list, enc_role/enc_env parameters, environment dropped when testing), exiting non-zero on any HTTP/parse failure so the puppet exec ENC fails safe. - Hand-parse the small fixed cobbler-wire YAML and hand-emit the output using the standard library only. - Add table-driven normalisation tests, httptest 200/404/500/timeout tests, and a golden-output test. - Add Makefile (build/test/lint/fmt + patch/minor/major with version ldflags), .gitignore, .pre-commit-config.yaml, and Woodpecker pipelines (build/test/pre-commit on pull_request; release on v* tags).
This commit is contained in:
+126
@@ -0,0 +1,126 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// cobblerWire is the exact cobbler-wire document encapi serves for a node with
|
||||
// a single role in the develop environment.
|
||||
const cobblerWire = "classes:\n" +
|
||||
" roles::infra::storage::vault: {}\n" +
|
||||
"environment: develop\n" +
|
||||
"parameters: {}\n"
|
||||
|
||||
// goldenENC is the reshaped ENC document the previous python script produced
|
||||
// from cobblerWire (verified byte-for-byte against uv/python yaml.dump).
|
||||
const goldenENC = "classes:\n" +
|
||||
"- roles::infra::storage::vault\n" +
|
||||
"environment: develop\n" +
|
||||
"parameters:\n" +
|
||||
" enc_env: develop\n" +
|
||||
" enc_role:\n" +
|
||||
" - roles::infra::storage::vault\n"
|
||||
|
||||
func TestRunGoldenOutput(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/cblr/svc/op/puppet/hostname/host.example" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/x-yaml")
|
||||
_, _ = w.Write([]byte(cobblerWire))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
t.Setenv("ENCAPI_URL", srv.URL)
|
||||
var out bytes.Buffer
|
||||
if err := run([]string{"encapic", "host.example"}, &out); err != nil {
|
||||
t.Fatalf("run returned error: %v", err)
|
||||
}
|
||||
if out.String() != goldenENC {
|
||||
t.Errorf("output mismatch\n--- want ---\n%s\n--- got ---\n%s", goldenENC, out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun404FailsNonZero(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
t.Setenv("ENCAPI_URL", srv.URL)
|
||||
var out bytes.Buffer
|
||||
err := run([]string{"encapic", "ghost.example"}, &out)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error on 404, got nil")
|
||||
}
|
||||
if out.Len() != 0 {
|
||||
t.Errorf("expected no stdout on failure, got %q", out.String())
|
||||
}
|
||||
if !strings.Contains(err.Error(), "404") {
|
||||
t.Errorf("error should mention 404, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun500FailsNonZero(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "boom", http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
t.Setenv("ENCAPI_URL", srv.URL)
|
||||
var out bytes.Buffer
|
||||
if err := run([]string{"encapic", "host.example"}, &out); err == nil {
|
||||
t.Fatalf("expected error on 500, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchTimeout(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
_, _ = w.Write([]byte(cobblerWire))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
// Use a client with a tiny timeout to exercise the timeout path quickly.
|
||||
client := &http.Client{Timeout: 20 * time.Millisecond}
|
||||
resp, err := client.Get(srv.URL + "/cblr/svc/op/puppet/hostname/host.example")
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
t.Fatalf("expected timeout error, got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunUsage(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
if err := run([]string{"encapic"}, &out); err == nil {
|
||||
t.Fatalf("expected usage error with no args")
|
||||
}
|
||||
if err := run([]string{"encapic", "a", "b"}, &out); err == nil {
|
||||
t.Fatalf("expected usage error with too many args")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunVersion(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
if err := run([]string{"encapic", "--version"}, &out); err != nil {
|
||||
t.Fatalf("version returned error: %v", err)
|
||||
}
|
||||
if !strings.Contains(out.String(), "encapic") {
|
||||
t.Errorf("version output = %q", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultBaseURLCompiledIn(t *testing.T) {
|
||||
// Guard against accidental changes to the in-cluster default.
|
||||
os.Unsetenv("ENCAPI_URL")
|
||||
if defaultBaseURL != "http://encapi.encapi.svc.cluster.local" {
|
||||
t.Errorf("defaultBaseURL = %q", defaultBaseURL)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user