Initial commit: Go rewrite of node-lookup
Query PuppetDB for node facts via CLI. Replaces the original Python script. - XDG config (~/.config/node-lookup/config.yaml) with env var overrides - All flags from original tool preserved (-n, -F, -R, -m, --pm, -1, -2, -C, -A, -j) - config init / config show subcommands - Unit tests (23), Makefile, GoReleaser config, pre-commit hooks 💘 Generated with Crush Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
This commit is contained in:
+323
@@ -0,0 +1,323 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ---- helpers ----------------------------------------------------------------
|
||||
|
||||
func mustMarshal(v interface{}) []byte {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func newTestServer(t *testing.T, facts []fact) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(facts)
|
||||
}))
|
||||
}
|
||||
|
||||
func rawJSON(v interface{}) json.RawMessage {
|
||||
b, _ := json.Marshal(v)
|
||||
return json.RawMessage(b)
|
||||
}
|
||||
|
||||
// ---- buildQuery -------------------------------------------------------------
|
||||
|
||||
func TestBuildQuery_NoFilters(t *testing.T) {
|
||||
q := buildQuery("", "", "", "", "enc_role", false)
|
||||
if !strings.Contains(q, "enc_role") {
|
||||
t.Fatalf("expected default role query, got %s", q)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildQuery_Node(t *testing.T) {
|
||||
q := buildQuery("host1", "", "", "", "enc_role", false)
|
||||
if !strings.Contains(q, "certname") || !strings.Contains(q, "host1") {
|
||||
t.Fatalf("unexpected query: %s", q)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildQuery_FactAndMatch(t *testing.T) {
|
||||
q := buildQuery("", "region", "syd1", "", "enc_role", false)
|
||||
if !strings.Contains(q, "region") || !strings.Contains(q, "syd1") {
|
||||
t.Fatalf("unexpected query: %s", q)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildQuery_PartialMatch(t *testing.T) {
|
||||
q := buildQuery("", "enc_role", "", "dns", "enc_role", false)
|
||||
if !strings.Contains(q, "~") || !strings.Contains(q, "dns") {
|
||||
t.Fatalf("expected partial match query, got %s", q)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildQuery_ShowRole(t *testing.T) {
|
||||
q := buildQuery("", "", "", "", "my_role_fact", true)
|
||||
if !strings.Contains(q, "my_role_fact") {
|
||||
t.Fatalf("expected role fact in query, got %s", q)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildQuery_CustomRoleFact(t *testing.T) {
|
||||
q := buildQuery("", "", "", "", "custom_role", true)
|
||||
if !strings.Contains(q, "custom_role") {
|
||||
t.Fatalf("expected custom role fact, got %s", q)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- valueString ------------------------------------------------------------
|
||||
|
||||
func TestValueString_String(t *testing.T) {
|
||||
raw := rawJSON("hello")
|
||||
if got := valueString(raw); got != "hello" {
|
||||
t.Fatalf("expected hello, got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueString_Number(t *testing.T) {
|
||||
raw := rawJSON(42)
|
||||
if got := valueString(raw); got != "42" {
|
||||
t.Fatalf("expected 42, got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueString_Object(t *testing.T) {
|
||||
raw := json.RawMessage(`{"a":1}`)
|
||||
got := valueString(raw)
|
||||
if got != `{"a":1}` {
|
||||
t.Fatalf("expected compact JSON, got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- processResults ---------------------------------------------------------
|
||||
|
||||
func TestProcessResults_Sorted(t *testing.T) {
|
||||
facts := []fact{
|
||||
{Certname: "z-host", Value: rawJSON("val1")},
|
||||
{Certname: "a-host", Value: rawJSON("val2")},
|
||||
}
|
||||
out := processResults(facts)
|
||||
if out[0] != "a-host val2" || out[1] != "z-host val1" {
|
||||
t.Fatalf("unexpected order: %v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessResults_Empty(t *testing.T) {
|
||||
out := processResults(nil)
|
||||
if len(out) != 0 {
|
||||
t.Fatalf("expected empty, got %v", out)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- countResults -----------------------------------------------------------
|
||||
|
||||
func TestCountResults_Basic(t *testing.T) {
|
||||
lines := []string{
|
||||
"host1 syd1",
|
||||
"host2 syd1",
|
||||
"host3 mel1",
|
||||
}
|
||||
out := countResults(lines)
|
||||
found := map[string]bool{}
|
||||
for _, l := range out {
|
||||
found[l] = true
|
||||
}
|
||||
if !found["2: syd1"] {
|
||||
t.Fatalf("expected '2: syd1' in %v", out)
|
||||
}
|
||||
if !found["1: mel1"] {
|
||||
t.Fatalf("expected '1: mel1' in %v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountResults_Sorted(t *testing.T) {
|
||||
lines := []string{"h1 z", "h2 a", "h3 a"}
|
||||
out := countResults(lines)
|
||||
// lexicographic sort: "1: z" < "2: a"
|
||||
if out[0] != "1: z" || out[1] != "2: a" {
|
||||
t.Fatalf("unexpected order: %v", out)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- queryPuppetDB ----------------------------------------------------------
|
||||
|
||||
func TestQueryPuppetDB_Success(t *testing.T) {
|
||||
want := []fact{
|
||||
{Certname: "node1", Name: "enc_role", Value: rawJSON("roles::dns")},
|
||||
}
|
||||
srv := newTestServer(t, want)
|
||||
defer srv.Close()
|
||||
|
||||
got, err := queryPuppetDB(srv.URL+"/pdb/query/v4/facts", `["=","name","enc_role"]`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 1 || got[0].Certname != "node1" {
|
||||
t.Fatalf("unexpected result: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryPuppetDB_HTTPError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
_, err := queryPuppetDB(srv.URL+"/pdb/query/v4/facts", `["=","name","enc_role"]`)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 404")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryPuppetDB_BadJSON(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("not json"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
_, err := queryPuppetDB(srv.URL+"/pdb/query/v4/facts", `["=","name","enc_role"]`)
|
||||
if err == nil {
|
||||
t.Fatal("expected decode error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryPuppetDB_ConnectionRefused(t *testing.T) {
|
||||
_, err := queryPuppetDB("http://127.0.0.1:1/facts", `["=","name","enc_role"]`)
|
||||
if err == nil {
|
||||
t.Fatal("expected connection error")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- config -----------------------------------------------------------------
|
||||
|
||||
func TestLoadConfig_Defaults(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
t.Setenv("NODE_LOOKUP_URL", "")
|
||||
t.Setenv("NODE_LOOKUP_ROLE_FACT", "")
|
||||
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.PuppetDBURL != defaultPuppetDBURL {
|
||||
t.Fatalf("expected default URL, got %s", cfg.PuppetDBURL)
|
||||
}
|
||||
if cfg.RoleFact != defaultRoleFact {
|
||||
t.Fatalf("expected default role fact, got %s", cfg.RoleFact)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_EnvOverride(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
t.Setenv("NODE_LOOKUP_URL", "http://custom:9090/facts")
|
||||
t.Setenv("NODE_LOOKUP_ROLE_FACT", "my_role")
|
||||
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.PuppetDBURL != "http://custom:9090/facts" {
|
||||
t.Fatalf("env override failed: %s", cfg.PuppetDBURL)
|
||||
}
|
||||
if cfg.RoleFact != "my_role" {
|
||||
t.Fatalf("env override failed: %s", cfg.RoleFact)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_FileOverride(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", dir)
|
||||
t.Setenv("NODE_LOOKUP_URL", "")
|
||||
t.Setenv("NODE_LOOKUP_ROLE_FACT", "")
|
||||
|
||||
cfgDir := filepath.Join(dir, appName)
|
||||
os.MkdirAll(cfgDir, 0o755)
|
||||
os.WriteFile(filepath.Join(cfgDir, configFileName), []byte("puppetdb_url: http://file:8080/facts\nrole_fact: file_role\n"), 0o644)
|
||||
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.PuppetDBURL != "http://file:8080/facts" {
|
||||
t.Fatalf("file override failed: %s", cfg.PuppetDBURL)
|
||||
}
|
||||
if cfg.RoleFact != "file_role" {
|
||||
t.Fatalf("file override failed: %s", cfg.RoleFact)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_EnvOverridesFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", dir)
|
||||
t.Setenv("NODE_LOOKUP_URL", "http://env:9999/facts")
|
||||
t.Setenv("NODE_LOOKUP_ROLE_FACT", "")
|
||||
|
||||
cfgDir := filepath.Join(dir, appName)
|
||||
os.MkdirAll(cfgDir, 0o755)
|
||||
os.WriteFile(filepath.Join(cfgDir, configFileName), []byte("puppetdb_url: http://file:8080/facts\n"), 0o644)
|
||||
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.PuppetDBURL != "http://env:9999/facts" {
|
||||
t.Fatalf("env should beat file, got %s", cfg.PuppetDBURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_InvalidYAML(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", dir)
|
||||
t.Setenv("NODE_LOOKUP_URL", "")
|
||||
t.Setenv("NODE_LOOKUP_ROLE_FACT", "")
|
||||
|
||||
cfgDir := filepath.Join(dir, appName)
|
||||
os.MkdirAll(cfgDir, 0o755)
|
||||
os.WriteFile(filepath.Join(cfgDir, configFileName), []byte(":\tinvalid: yaml:\n"), 0o644)
|
||||
|
||||
_, err := loadConfig()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid YAML")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteDefaultConfig(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", dir)
|
||||
|
||||
if err := writeDefaultConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, appName, configFileName)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(data), "puppetdb_url") {
|
||||
t.Fatalf("config file missing puppetdb_url: %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteDefaultConfig_AlreadyExists(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", dir)
|
||||
|
||||
writeDefaultConfig()
|
||||
err := writeDefaultConfig()
|
||||
if err == nil {
|
||||
t.Fatal("expected error when config already exists")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user