7 Commits

Author SHA1 Message Date
unkinben 3a4c9ea1c1 fix: surface release API errors in woodpecker pipeline (#5)
ci/woodpecker/release/release Pipeline failed
Capture and print the full Gitea API response before parsing the release
ID, and fail explicitly if the ID is empty so the root cause is visible
in CI logs instead of silently producing a malformed asset upload URL.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>

Reviewed-on: #5
2026-03-26 12:48:54 +11:00
unkinben b0d8f57b6f Add merged branch release notes to Gitea release body (#4)
ci/woodpecker/release/release Pipeline failed
Generates release notes from merged branches since last tag and
includes them in the release body via Gitea API.

Reviewed-on: #4
2026-03-25 21:27:45 +11:00
unkinben 45cb378022 Replace gitea-release plugin with curl-based release upload (#3)
Uses basic auth (droneci user) and Gitea API directly to create
the release and upload the binary asset.

Reviewed-on: #3
2026-03-25 19:45:34 +11:00
unkinben f65864af22 feat/version (#2)
Reviewed-on: #2
2026-03-25 19:25:12 +11:00
unkinben 13b0f12edf Merge pull request 'feat/all-facts-flag' (#1) from feat/all-facts-flag into master
Reviewed-on: #1
2026-03-25 17:01:07 +11:00
unkinben 2acff78d02 Add tests for -a (all facts) flag 2026-03-25 15:15:23 +11:00
unkinben e62e69bbbc Add -a flag to show all facts for a node
-a requires -n and prints all PuppetDB facts for the specified node
as 'fact_name  value' sorted alphabetically by fact name, useful for
discovering available facts to query against.
2026-03-25 15:08:18 +11:00
8 changed files with 295 additions and 64 deletions
+8
View File
@@ -0,0 +1,8 @@
when:
- event: pull_request
steps:
- name: lint
image: golangci/golangci-lint:latest
commands:
- golangci-lint run ./...
+8
View File
@@ -0,0 +1,8 @@
when:
- event: pull_request
steps:
- name: pre-commit
image: git.unkin.net/unkin/almalinux9-gobuilder:20260325
commands:
- uvx pre-commit run --all-files
+48
View File
@@ -0,0 +1,48 @@
when:
- event: release
steps:
- name: test
image: golang:latest
commands:
- go test ./...
- name: build
image: golang:latest
commands:
- VERSION=${CI_COMMIT_TAG}
- go build -ldflags="-s -w -X main.version=${VERSION}" -o node-lookup ./...
depends_on: [test]
- name: release
image: git.unkin.net/unkin/almalinux9-base:20260325
environment:
DRONECI_PASSWORD:
from_secret: DRONECI_PASSWORD
commands:
- |
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -n "$PREV_TAG" ]; then
NOTES=$(git log "${PREV_TAG}..${CI_COMMIT_TAG}" --merges --pretty=format:"- %s")
else
NOTES=$(git log --merges --pretty=format:"- %s")
fi
BODY=$(printf '%s' "$NOTES" | sed 's/"/\\"/g; s/$/\\n/' | tr -d '\n')
RELEASE_RESPONSE=$(curl -sf -X POST "https://git.unkin.net/api/v1/repos/${CI_REPO}/releases" \
-u "droneci:$DRONECI_PASSWORD" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"${CI_COMMIT_TAG}\",\"name\":\"${CI_COMMIT_TAG}\",\"body\":\"${BODY}\"}")
echo "Release API response: ${RELEASE_RESPONSE}"
RELEASE_ID=$(echo "${RELEASE_RESPONSE}" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
if [ -z "$RELEASE_ID" ]; then
echo "ERROR: failed to obtain release ID" >&2
exit 1
fi
echo "Release ID: ${RELEASE_ID}"
curl -sf -X POST "https://git.unkin.net/api/v1/repos/${CI_REPO}/releases/${RELEASE_ID}/assets" \
-u "droneci:$DRONECI_PASSWORD" \
-F "attachment=@node-lookup"
backend_options:
kubernetes:
serviceAccountName: default
depends_on: [build]
+8
View File
@@ -0,0 +1,8 @@
when:
- event: pull_request
steps:
- name: unit-tests
image: golang:latest
commands:
- go test -v -race ./...
+2 -3
View File
@@ -29,7 +29,7 @@ Requires Go 1.21+. Dependencies: `github.com/spf13/cobra` (CLI), `gopkg.in/yaml.
./node-lookup -n <hostname> # lookup a specific node
./node-lookup -F <fact_name> # filter by fact name
./node-lookup -m <value> # exact value match
./node-lookup -p <pattern> # partial/regex match on value (also --pm)
./node-lookup --pm <pattern> # partial/regex match on value
./node-lookup -R -1 # node names only
./node-lookup -R -2 # values only
./node-lookup -R -C # count occurrences
@@ -94,7 +94,6 @@ No test suite exists. Manual testing requires access to the Consul/PuppetDB envi
## Gotchas
- `-1`, `-2`, `-C`, and `-A` all require `-R` or `-F`; the tool exits with an error otherwise.
- `-C` (count) with stdin extracts the first field of each line as the node name, queries PuppetDB per node, then counts the resulting values.
- `-C` (count) with stdin reads all lines as pre-fetched `"node value"` output for counting — it does **not** query PuppetDB per line.
- JSON output (`-j`) builds `{ hostname: { factname: value } }` where the fact key is the `-F` value, the `role_fact` config value (if `-R`), or `"value"` as fallback.
- `config init` fails if the config file already exists (will not overwrite).
- `--pm` has shorthand `-p`. Use `-p <pattern>` or `--pm <pattern>` — not `-pm <pattern>` (pflag parses single-dash multi-char as combined shorthands).
+27 -2
View File
@@ -1,7 +1,8 @@
BINARY := node-lookup
GOFLAGS := -ldflags="-s -w"
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
GOFLAGS := -ldflags="-s -w -X main.version=$(VERSION)"
.PHONY: all build test lint clean install
.PHONY: all build test lint clean install patch minor major _release
all: build
@@ -19,3 +20,27 @@ clean:
install:
go install $(GOFLAGS) ./...
# Bump helpers — reads the latest semver tag and creates the next one.
# If no tag exists yet, starts from v0.0.0.
_LATEST := $(shell git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$$' | head -1)
_BASE := $(if $(_LATEST),$(_LATEST),v0.0.0)
_MAJ := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f1)
_MIN := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f2)
_PAT := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f3)
patch:
@NEW=v$(_MAJ).$(_MIN).$(shell expr $(_PAT) + 1); \
git tag $$NEW && echo "Tagged $$NEW" && $(MAKE) _release TAG=$$NEW
minor:
@NEW=v$(_MAJ).$(shell expr $(_MIN) + 1).0; \
git tag $$NEW && echo "Tagged $$NEW" && $(MAKE) _release TAG=$$NEW
major:
@NEW=v$(shell expr $(_MAJ) + 1).0.0; \
git tag $$NEW && echo "Tagged $$NEW" && $(MAKE) _release TAG=$$NEW
_release:
git push origin $(TAG)
tea releases create --tag $(TAG) --title $(TAG)
+63 -36
View File
@@ -25,6 +25,8 @@ const (
appName = "node-lookup"
)
var version = "dev"
// config holds all configurable values. Fields map 1:1 to config file keys,
// env vars (NODE_LOOKUP_*), and (where applicable) CLI flags.
type config struct {
@@ -108,7 +110,7 @@ type fact struct {
Value json.RawMessage `json:"value"`
}
func buildQuery(node, factName, match, partialMatch, inverseMatch, roleFact string, showRole bool) string {
func buildQuery(node, factName, match, partialMatch, roleFact string, showRole bool) string {
type filter = []interface{}
var filters []filter
@@ -125,8 +127,6 @@ func buildQuery(node, factName, match, partialMatch, inverseMatch, roleFact stri
filters = append(filters, filter{"=", "value", match})
} else if partialMatch != "" {
filters = append(filters, filter{"~", "value", partialMatch})
} else if inverseMatch != "" {
filters = append(filters, filter{"not", filter{"~", "value", inverseMatch}})
}
if len(filters) == 0 {
@@ -150,7 +150,7 @@ func queryPuppetDB(puppetDBURL, query string) ([]fact, error) {
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
@@ -176,7 +176,7 @@ func valueString(raw json.RawMessage) string {
func valueAny(raw json.RawMessage) interface{} {
var v interface{}
json.Unmarshal(raw, &v)
_ = json.Unmarshal(raw, &v)
return v
}
@@ -215,34 +215,61 @@ func isTerminal(f *os.File) bool {
return (fi.Mode() & os.ModeCharDevice) != 0
}
func run(cfg config, nodeName, factName, match, partialMatch, inverseMatch string, showRole, nodeOnly, valueOnly, count, ansible, jsonMode bool) error {
func allFactsForNode(puppetDBURL, node string) ([]fact, error) {
query, _ := json.Marshal([]interface{}{"=", "certname", node})
return queryPuppetDB(puppetDBURL, string(query))
}
func run(cfg config, nodeName, factName, match, partialMatch string, showRole, nodeOnly, valueOnly, count, ansible, jsonMode, allFacts bool) error {
signal.Ignore(syscall.SIGPIPE)
if allFacts {
if nodeName == "" {
return fmt.Errorf("-a requires -n")
}
facts, err := allFactsForNode(cfg.PuppetDBURL, nodeName)
if err != nil {
return err
}
sort.Slice(facts, func(i, j int) bool { return facts[i].Name < facts[j].Name })
for _, f := range facts {
fmt.Printf("%-40s %s\n", f.Name, valueString(f.Value))
}
return nil
}
if (nodeOnly || valueOnly || count || ansible) && !showRole && factName == "" {
return fmt.Errorf("-R or -F must be used with -1, -2, -C, or -A")
}
var allFacts []fact
var collected []fact
var stdinLines []string
doQuery := func(node string) error {
query := buildQuery(node, factName, match, partialMatch, inverseMatch, cfg.RoleFact, showRole)
query := buildQuery(node, factName, match, partialMatch, cfg.RoleFact, showRole)
facts, err := queryPuppetDB(cfg.PuppetDBURL, query)
if err != nil {
return err
}
allFacts = append(allFacts, facts...)
collected = append(collected, facts...)
return nil
}
if nodeName == "" && !isTerminal(os.Stdin) {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) == 0 {
continue
if count {
for scanner.Scan() {
stdinLines = append(stdinLines, scanner.Text())
}
if err := doQuery(fields[0]); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
} else {
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) == 0 {
continue
}
if err := doQuery(fields[0]); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
}
}
}
} else {
@@ -251,12 +278,12 @@ func run(cfg config, nodeName, factName, match, partialMatch, inverseMatch strin
}
}
returnData := processResults(allFacts)
returnData := processResults(collected)
switch {
case jsonMode:
hostFactMap := map[string]map[string]interface{}{}
for _, f := range allFacts {
for _, f := range collected {
if _, ok := hostFactMap[f.Certname]; !ok {
hostFactMap[f.Certname] = map[string]interface{}{}
}
@@ -273,10 +300,14 @@ func run(cfg config, nodeName, factName, match, partialMatch, inverseMatch strin
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
enc.SetEscapeHTML(false)
enc.Encode(hostFactMap)
_ = enc.Encode(hostFactMap)
case count:
fmt.Println(strings.Join(countResults(returnData), "\n"))
values := stdinLines
if len(values) == 0 {
values = returnData
}
fmt.Println(strings.Join(countResults(values), "\n"))
case ansible:
hosts := map[string]interface{}{}
@@ -288,7 +319,7 @@ func run(cfg config, nodeName, factName, match, partialMatch, inverseMatch strin
"all": map[string]interface{}{"hosts": hosts},
}
b, _ := yaml.Marshal(inventory)
os.Stdout.Write(b)
_, _ = os.Stdout.Write(b)
case nodeOnly:
for _, line := range returnData {
@@ -311,18 +342,6 @@ func run(cfg config, nodeName, factName, match, partialMatch, inverseMatch strin
}
func main() {
for i, arg := range os.Args {
if arg == "-pm" {
os.Args[i] = "--pm"
} else if strings.HasPrefix(arg, "-pm=") {
os.Args[i] = "--pm=" + arg[4:]
} else if arg == "-im" {
os.Args[i] = "--im"
} else if strings.HasPrefix(arg, "-im=") {
os.Args[i] = "--im=" + arg[4:]
}
}
cfg, err := loadConfig()
if err != nil {
fmt.Fprintln(os.Stderr, "config error:", err)
@@ -335,12 +354,12 @@ func main() {
showRole bool
match string
partialMatch string
inverseMatch string
nodeOnly bool
valueOnly bool
count bool
ansible bool
jsonMode bool
allFacts bool
puppetDBURL string
)
@@ -354,7 +373,7 @@ func main() {
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return run(cfg, nodeName, factName, match, partialMatch, inverseMatch, showRole, nodeOnly, valueOnly, count, ansible, jsonMode)
return run(cfg, nodeName, factName, match, partialMatch, showRole, nodeOnly, valueOnly, count, ansible, jsonMode, allFacts)
},
SilenceUsage: true,
}
@@ -364,13 +383,13 @@ func main() {
f.StringVarP(&factName, "fact", "F", "", "Fact name")
f.BoolVarP(&showRole, "role", "R", false, "Show role fact ("+defaultRoleFact+" by default)")
f.StringVarP(&match, "match", "m", "", "Exact value match")
f.StringVar(&partialMatch, "pm", "", "Partial/regex match on value")
f.StringVar(&inverseMatch, "im", "", "Inverse partial/regex match on value")
f.StringVar(&partialMatch, "pm", "", "Partial match on value")
f.BoolVarP(&nodeOnly, "nodeonly", "1", false, "Show only the node name")
f.BoolVarP(&valueOnly, "valueonly", "2", false, "Show only the value")
f.BoolVarP(&count, "count", "C", false, "Count fact occurrences")
f.BoolVarP(&ansible, "ansible", "A", false, "Output as Ansible inventory")
f.BoolVarP(&jsonMode, "json", "j", false, "Emit valid JSON for all output")
f.BoolVarP(&allFacts, "all", "a", false, "Show all facts for a node (requires -n)")
rootCmd.PersistentFlags().StringVar(&puppetDBURL, "url", cfg.PuppetDBURL, "PuppetDB facts URL (overrides config and NODE_LOOKUP_URL)")
configCmd := &cobra.Command{
@@ -402,6 +421,14 @@ func main() {
configCmd.AddCommand(configInitCmd, configShowCmd)
rootCmd.AddCommand(configCmd)
versionCmd := &cobra.Command{
Use: "version",
Short: "Print the version",
Run: func(cmd *cobra.Command, args []string) { fmt.Println(version) },
SilenceUsage: true,
}
rootCmd.AddCommand(versionCmd)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
+131 -23
View File
@@ -2,6 +2,7 @@ package main
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
@@ -12,19 +13,11 @@ import (
// ---- 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)
_ = json.NewEncoder(w).Encode(facts)
}))
}
@@ -36,42 +29,42 @@ func rawJSON(v interface{}) json.RawMessage {
// ---- buildQuery -------------------------------------------------------------
func TestBuildQuery_NoFilters(t *testing.T) {
q := buildQuery("", "", "", "", "", "enc_role", false)
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)
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)
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)
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)
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)
q := buildQuery("", "", "", "", "custom_role", true)
if !strings.Contains(q, "custom_role") {
t.Fatalf("expected custom role fact, got %s", q)
}
@@ -183,7 +176,7 @@ func TestQueryPuppetDB_HTTPError(t *testing.T) {
func TestQueryPuppetDB_BadJSON(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("not json"))
_, _ = w.Write([]byte("not json"))
}))
defer srv.Close()
@@ -243,8 +236,12 @@ func TestLoadConfig_FileOverride(t *testing.T) {
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)
if err := os.MkdirAll(cfgDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(cfgDir, configFileName), []byte("puppetdb_url: http://file:8080/facts\nrole_fact: file_role\n"), 0o644); err != nil {
t.Fatal(err)
}
cfg, err := loadConfig()
if err != nil {
@@ -265,8 +262,12 @@ func TestLoadConfig_EnvOverridesFile(t *testing.T) {
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)
if err := os.MkdirAll(cfgDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(cfgDir, configFileName), []byte("puppetdb_url: http://file:8080/facts\n"), 0o644); err != nil {
t.Fatal(err)
}
cfg, err := loadConfig()
if err != nil {
@@ -284,8 +285,12 @@ func TestLoadConfig_InvalidYAML(t *testing.T) {
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)
if err := os.MkdirAll(cfgDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(cfgDir, configFileName), []byte(":\tinvalid: yaml:\n"), 0o644); err != nil {
t.Fatal(err)
}
_, err := loadConfig()
if err == nil {
@@ -315,9 +320,112 @@ func TestWriteDefaultConfig_AlreadyExists(t *testing.T) {
dir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", dir)
writeDefaultConfig()
if err := writeDefaultConfig(); err != nil {
t.Fatal(err)
}
err := writeDefaultConfig()
if err == nil {
t.Fatal("expected error when config already exists")
}
}
// ---- allFactsForNode --------------------------------------------------------
func TestAllFactsForNode_ReturnsSortedFacts(t *testing.T) {
facts := []fact{
{Certname: "node1", Name: "zebra", Value: rawJSON("z-val")},
{Certname: "node1", Name: "alpha", Value: rawJSON("a-val")},
{Certname: "node1", Name: "middle", Value: rawJSON(42)},
}
srv := newTestServer(t, facts)
defer srv.Close()
got, err := allFactsForNode(srv.URL+"/pdb/query/v4/facts", "node1")
if err != nil {
t.Fatal(err)
}
if len(got) != 3 {
t.Fatalf("expected 3 facts, got %d", len(got))
}
}
func TestAllFactsForNode_QueryContainsCertname(t *testing.T) {
var receivedQuery string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedQuery = r.URL.Query().Get("query")
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode([]fact{})
}))
defer srv.Close()
_, _ = allFactsForNode(srv.URL+"/pdb/query/v4/facts", "mynode.example.com")
if !strings.Contains(receivedQuery, "mynode.example.com") {
t.Fatalf("expected certname in query, got: %s", receivedQuery)
}
if !strings.Contains(receivedQuery, "certname") {
t.Fatalf("expected 'certname' in query, got: %s", receivedQuery)
}
}
func TestAllFactsForNode_HTTPError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal error", http.StatusInternalServerError)
}))
defer srv.Close()
_, err := allFactsForNode(srv.URL+"/pdb/query/v4/facts", "node1")
if err == nil {
t.Fatal("expected error for HTTP 500")
}
}
// ---- run with -a flag -------------------------------------------------------
func TestRun_AllFacts_RequiresNode(t *testing.T) {
cfg := config{PuppetDBURL: "http://unused", RoleFact: "enc_role"}
err := run(cfg, "", "", "", "", false, false, false, false, false, false, true)
if err == nil || !strings.Contains(err.Error(), "-a requires -n") {
t.Fatalf("expected -a requires -n error, got: %v", err)
}
}
func TestRun_AllFacts_PrintsSortedByName(t *testing.T) {
facts := []fact{
{Certname: "node1", Name: "zzz_fact", Value: rawJSON("last")},
{Certname: "node1", Name: "aaa_fact", Value: rawJSON("first")},
{Certname: "node1", Name: "mmm_fact", Value: rawJSON(true)},
}
srv := newTestServer(t, facts)
defer srv.Close()
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
cfg := config{PuppetDBURL: srv.URL + "/pdb/query/v4/facts", RoleFact: "enc_role"}
err := run(cfg, "node1", "", "", "", false, false, false, false, false, false, true)
_ = w.Close()
os.Stdout = old
var buf strings.Builder
_, _ = io.Copy(&buf, r)
out := buf.String()
if err != nil {
t.Fatal(err)
}
lines := strings.Split(strings.TrimSpace(out), "\n")
if len(lines) != 3 {
t.Fatalf("expected 3 lines, got %d: %q", len(lines), out)
}
if !strings.HasPrefix(lines[0], "aaa_fact") {
t.Errorf("first line should be aaa_fact, got: %s", lines[0])
}
if !strings.HasPrefix(lines[1], "mmm_fact") {
t.Errorf("second line should be mmm_fact, got: %s", lines[1])
}
if !strings.HasPrefix(lines[2], "zzz_fact") {
t.Errorf("third line should be zzz_fact, got: %s", lines[2])
}
}