node-lookup: auto-qualify short node names to .main.unkin.net (#19)
ci/woodpecker/tag/release Pipeline was successful

Short `-n` node names silently returned nothing: `node-lookup -R -n ausyd1nxvm2120` found nothing while `-n ausyd1nxvm2120.main.unkin.net` worked, because the PuppetDB `certname` filter needs a FQDN. This auto-qualifies a dotless name before the lookup.

## Changes
- Add `qualifyNode()` pure helper: a dotless name gets `.<domain>` appended; a name already containing a dot (any domain, incl. `*.k8s.syd1.au.unkin.net`) is left unchanged; a single trailing dot is stripped first; empty input is preserved (same "no node" behavior as today).
- Apply normalization to the `-n` value and to stdin-sourced node names in `run()`, so both entry points behave consistently.
- Make the domain configurable: config key `domain`, `NODE_LOOKUP_DOMAIN` env var, and `--domain` flag, all defaulting to `main.unkin.net`.
- Surface `domain` in `config show` / `config init` output and document the new env var/flag/behavior in AGENTS.md.
- Add table-driven `qualifyNode` tests (short name appended, FQDN unchanged, multi-label other-domain FQDN unchanged, trailing-dot handling, empty input, custom domain) and a `NODE_LOOKUP_DOMAIN` env-override test.

Companion tools `pburl`/`pblastreport` take already-qualified hostnames (typically piped from `node-lookup`) via `puppet.ReadHosts` and do not share the `-n` code path, so they are intentionally left out to keep this PR atomic.

## Validation
- `gofmt -l .` clean, `go vet ./...` clean
- `go test -race ./...` pass
- `make build` builds all three binaries

Reviewed-on: #19
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
This commit was merged in pull request #19.
This commit is contained in:
2026-08-15 14:50:35 +10:00
committed by BenVincent
parent 182bd326b8
commit 35edc9c547
3 changed files with 81 additions and 3 deletions
+45
View File
@@ -167,6 +167,35 @@ func TestSplitFactNames(t *testing.T) {
}
}
func TestQualifyNode(t *testing.T) {
const domain = "main.unkin.net"
cases := []struct {
name string
in string
want string
}{
{"short name appends domain", "ausyd1nxvm2120", "ausyd1nxvm2120.main.unkin.net"},
{"fqdn in default domain unchanged", "ausyd1nxvm2120.main.unkin.net", "ausyd1nxvm2120.main.unkin.net"},
{"multi-label fqdn other domain unchanged", "foo.k8s.syd1.au.unkin.net", "foo.k8s.syd1.au.unkin.net"},
{"short name with trailing dot qualified", "ausyd1nxvm2120.", "ausyd1nxvm2120.main.unkin.net"},
{"fqdn with trailing dot stripped", "foo.main.unkin.net.", "foo.main.unkin.net"},
{"empty unchanged", "", ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := qualifyNode(tc.in, domain); got != tc.want {
t.Fatalf("qualifyNode(%q, %q) = %q, want %q", tc.in, domain, got, tc.want)
}
})
}
}
func TestQualifyNode_CustomDomain(t *testing.T) {
if got := qualifyNode("host1", "example.com"); got != "host1.example.com" {
t.Fatalf("qualifyNode with custom domain = %q, want host1.example.com", got)
}
}
func TestBuildQuery_SingleFact_NoOr(t *testing.T) {
q := buildQuery("", "ipaddress", "", "enc_role", false, false, false)
if strings.Contains(q, `"or"`) {
@@ -397,6 +426,22 @@ func TestLoadConfig_Defaults(t *testing.T) {
if cfg.RoleFact != defaultRoleFact {
t.Fatalf("expected default role fact, got %s", cfg.RoleFact)
}
if cfg.Domain != defaultDomain {
t.Fatalf("expected default domain, got %s", cfg.Domain)
}
}
func TestLoadConfig_DomainEnvOverride(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
t.Setenv("NODE_LOOKUP_DOMAIN", "example.com")
cfg, err := loadConfig()
if err != nil {
t.Fatal(err)
}
if cfg.Domain != "example.com" {
t.Fatalf("domain env override failed: %s", cfg.Domain)
}
}
func TestLoadConfig_EnvOverride(t *testing.T) {