Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 499903ab2b | |||
| e0633aa114 | |||
| 9e3cef8ef8 | |||
| 35edc9c547 | |||
| 182bd326b8 | |||
| 2aaffd7e31 |
+13
-1
@@ -3,7 +3,19 @@ when:
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: golang:1.25
|
||||
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder:0.1.2-alma9
|
||||
environment:
|
||||
GOCACHE_S3_BUCKET: gocache
|
||||
GOCACHE_S3_REGION: us-east-1
|
||||
GOCACHE_S3_ENDPOINT_URL: https://s3.ceph.unkin.net
|
||||
GOCACHE_S3_PATH_STYLE: "true"
|
||||
GOCACHE_KEY_PREFIX: ci-node-lookup
|
||||
GOCACHE_METRICS: "true"
|
||||
GOCACHEPROG: go-cache-plugin --cache-dir=/tmp/gocache
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: GOCACHE_AWS_ACCESS_KEY_ID
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: GOCACHE_AWS_SECRET_ACCESS_KEY
|
||||
commands:
|
||||
- make build
|
||||
backend_options:
|
||||
|
||||
@@ -3,7 +3,19 @@ when:
|
||||
|
||||
steps:
|
||||
- name: pre-commit
|
||||
image: git.unkin.net/unkin/almalinux9-gobuilder:20260606
|
||||
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder:0.1.2-alma9
|
||||
environment:
|
||||
GOCACHE_S3_BUCKET: gocache
|
||||
GOCACHE_S3_REGION: us-east-1
|
||||
GOCACHE_S3_ENDPOINT_URL: https://s3.ceph.unkin.net
|
||||
GOCACHE_S3_PATH_STYLE: "true"
|
||||
GOCACHE_KEY_PREFIX: ci-node-lookup
|
||||
GOCACHE_METRICS: "true"
|
||||
GOCACHEPROG: go-cache-plugin --cache-dir=/tmp/gocache
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: GOCACHE_AWS_ACCESS_KEY_ID
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: GOCACHE_AWS_SECRET_ACCESS_KEY
|
||||
commands:
|
||||
- uvx pre-commit run --all-files
|
||||
backend_options:
|
||||
|
||||
+61
-24
@@ -3,7 +3,19 @@ when:
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: golang:1.25
|
||||
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder:0.1.2-alma9
|
||||
environment:
|
||||
GOCACHE_S3_BUCKET: gocache
|
||||
GOCACHE_S3_REGION: us-east-1
|
||||
GOCACHE_S3_ENDPOINT_URL: https://s3.ceph.unkin.net
|
||||
GOCACHE_S3_PATH_STYLE: "true"
|
||||
GOCACHE_KEY_PREFIX: ci-node-lookup
|
||||
GOCACHE_METRICS: "true"
|
||||
GOCACHEPROG: go-cache-plugin --cache-dir=/tmp/gocache
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: GOCACHE_AWS_ACCESS_KEY_ID
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: GOCACHE_AWS_SECRET_ACCESS_KEY
|
||||
commands:
|
||||
- go test -race ./...
|
||||
backend_options:
|
||||
@@ -21,16 +33,32 @@ steps:
|
||||
# cross-platform binaries attached to the Gitea release. Each tool is a
|
||||
# separate main package, so they are built individually per os/arch.
|
||||
- name: build
|
||||
image: git.unkin.net/unkin/almalinux9-gobuilder:20260606
|
||||
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder:0.1.2-alma9
|
||||
environment:
|
||||
GOCACHE_S3_BUCKET: gocache
|
||||
GOCACHE_S3_REGION: us-east-1
|
||||
GOCACHE_S3_ENDPOINT_URL: https://s3.ceph.unkin.net
|
||||
GOCACHE_S3_PATH_STYLE: "true"
|
||||
GOCACHE_KEY_PREFIX: ci-node-lookup
|
||||
GOCACHE_METRICS: "true"
|
||||
GOCACHEPROG: go-cache-plugin --cache-dir=/tmp/gocache
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: GOCACHE_AWS_ACCESS_KEY_ID
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: GOCACHE_AWS_SECRET_ACCESS_KEY
|
||||
commands:
|
||||
- make build VERSION=${CI_COMMIT_TAG}
|
||||
# Shell variables/expansions are escaped as $$ so Woodpecker leaves them
|
||||
# for the shell instead of substituting them (as pipeline vars) at parse
|
||||
# time. ${CI_COMMIT_TAG} is a real Woodpecker var and stays single-$.
|
||||
- |
|
||||
for entry in "node-lookup:." "pburl:./cmd/pburl" "pblastreport:./cmd/pblastreport"; do
|
||||
name="${entry%%:*}"; pkg="${entry##*:}"
|
||||
name="$${entry%%:*}"; pkg="$${entry##*:}"
|
||||
for osarch in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64; do
|
||||
GOOS="${osarch%/*}" GOARCH="${osarch#*/}" \
|
||||
os="$${osarch%/*}"; arch="$${osarch#*/}"
|
||||
GOOS="$$os" GOARCH="$$arch" \
|
||||
go build -ldflags="-s -w -X main.version=${CI_COMMIT_TAG}" \
|
||||
-o "${name}-${osarch%/*}-${osarch#*/}" "${pkg}"
|
||||
-o "$${name}-$${os}-$${arch}" "$$pkg"
|
||||
done
|
||||
done
|
||||
depends_on: [test]
|
||||
@@ -47,7 +75,7 @@ steps:
|
||||
|
||||
# Package the built binary + generated shell completions into an RPM.
|
||||
- name: package
|
||||
image: git.unkin.net/unkin/almalinux9-rpmbuilder:latest
|
||||
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/rpmbuilder:0.1.0-alma9
|
||||
commands:
|
||||
- ./scripts/build-rpm.sh ${CI_COMMIT_TAG}
|
||||
depends_on: [build]
|
||||
@@ -107,26 +135,35 @@ steps:
|
||||
- |
|
||||
curl --output /usr/local/bin/tea https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/gitea-dl/tea/0.12.0/tea-0.12.0-linux-amd64 && chmod +x /usr/local/bin/tea
|
||||
tea logins add --name gitea --url https://git.unkin.net --token "$${RELEASER_TOKEN}" --no-version-check
|
||||
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}" --pretty=format:"- %s")
|
||||
# $$ escapes shell vars/substitutions so Woodpecker doesn't blank them
|
||||
# at parse time; ${CI_COMMIT_TAG}/${CI_REPO} are real Woodpecker vars.
|
||||
# Find the previous release tag for the changelog range. Several tags can
|
||||
# point at the same commit (e.g. v0.5.3 and v0.5.4), so we skip tags on
|
||||
# the current commit and pick the newest semver tag that is a real
|
||||
# ancestor of this one -- describe HEAD^ would jump too far back.
|
||||
CUR_SHA=$$(git rev-list -n1 "${CI_COMMIT_TAG}")
|
||||
PREV_TAG=""
|
||||
for t in $$(git tag --sort=-v:refname); do
|
||||
[ "$$t" = "${CI_COMMIT_TAG}" ] && continue
|
||||
[ "$$(git rev-list -n1 "$$t")" = "$$CUR_SHA" ] && continue
|
||||
if git merge-base --is-ancestor "$$t" "${CI_COMMIT_TAG}" 2>/dev/null; then
|
||||
PREV_TAG="$$t"; break
|
||||
fi
|
||||
done
|
||||
if [ -n "$$PREV_TAG" ]; then
|
||||
NOTES=$$(git log "$${PREV_TAG}..${CI_COMMIT_TAG}" --pretty=format:"- %s")
|
||||
else
|
||||
NOTES=$(git log --pretty=format:"- %s")
|
||||
NOTES=$$(git log --pretty=format:"- %s")
|
||||
fi
|
||||
tea releases create --tag "${CI_COMMIT_TAG}" --title "${CI_COMMIT_TAG}" --note "${NOTES}" --login gitea --repo "${CI_REPO}"
|
||||
tea releases assets create "${CI_COMMIT_TAG}" \
|
||||
node-lookup-linux-amd64 \
|
||||
node-lookup-linux-arm64 \
|
||||
node-lookup-darwin-amd64 \
|
||||
node-lookup-darwin-arm64 \
|
||||
pburl-linux-amd64 \
|
||||
pburl-linux-arm64 \
|
||||
pburl-darwin-amd64 \
|
||||
pburl-darwin-arm64 \
|
||||
pblastreport-linux-amd64 \
|
||||
pblastreport-linux-arm64 \
|
||||
pblastreport-darwin-amd64 \
|
||||
pblastreport-darwin-arm64 \
|
||||
tea releases create --tag "${CI_COMMIT_TAG}" --title "${CI_COMMIT_TAG}" --note "$${NOTES}" --login gitea --repo "${CI_REPO}"
|
||||
# The build step writes the 12 cross-compiled binaries into the workspace
|
||||
# root; the package step writes the RPM to dist/. Generate a checksums
|
||||
# manifest over everything we attach so downloads can be verified.
|
||||
RPM=$$(ls dist/*.rpm 2>/dev/null | head -1)
|
||||
ASSETS="node-lookup-linux-amd64 node-lookup-linux-arm64 node-lookup-darwin-amd64 node-lookup-darwin-arm64 pburl-linux-amd64 pburl-linux-arm64 pburl-darwin-amd64 pburl-darwin-arm64 pblastreport-linux-amd64 pblastreport-linux-arm64 pblastreport-darwin-amd64 pblastreport-darwin-arm64"
|
||||
[ -n "$$RPM" ] && ASSETS="$$ASSETS $$RPM"
|
||||
sha256sum $$ASSETS > sha256sums.txt
|
||||
tea releases assets create "${CI_COMMIT_TAG}" $$ASSETS sha256sums.txt \
|
||||
--login gitea --repo "${CI_REPO}"
|
||||
depends_on: [upload-rpm]
|
||||
backend_options:
|
||||
|
||||
+13
-1
@@ -18,7 +18,19 @@ steps:
|
||||
cpu: 2
|
||||
|
||||
- name: test
|
||||
image: golang:1.25
|
||||
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder:0.1.2-alma9
|
||||
environment:
|
||||
GOCACHE_S3_BUCKET: gocache
|
||||
GOCACHE_S3_REGION: us-east-1
|
||||
GOCACHE_S3_ENDPOINT_URL: https://s3.ceph.unkin.net
|
||||
GOCACHE_S3_PATH_STYLE: "true"
|
||||
GOCACHE_KEY_PREFIX: ci-node-lookup
|
||||
GOCACHE_METRICS: "true"
|
||||
GOCACHEPROG: go-cache-plugin --cache-dir=/tmp/gocache
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: GOCACHE_AWS_ACCESS_KEY_ID
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: GOCACHE_AWS_SECRET_ACCESS_KEY
|
||||
commands:
|
||||
- go test -v -race ./...
|
||||
backend_options:
|
||||
|
||||
@@ -138,10 +138,12 @@ Show the active configuration (after all overrides applied):
|
||||
| `NODE_LOOKUP_URL` | `puppetdb_url` | PuppetDB facts endpoint |
|
||||
| `NODE_LOOKUP_ROLE_FACT` | `role_fact` | Fact name used by `-R` flag |
|
||||
| `NODE_LOOKUP_PUPPETBOARD_URL` | `puppetboard_url` | Puppetboard base URL (pburl / pblastreport) |
|
||||
| `NODE_LOOKUP_DOMAIN` | `domain` | Domain appended to short (dotless) `-n` node names (default `main.unkin.net`) |
|
||||
|
||||
### CLI flag
|
||||
### CLI flags
|
||||
|
||||
`--url <url>` overrides the PuppetDB URL for a single invocation (highest precedence).
|
||||
`--domain <domain>` overrides the auto-qualify domain for a single invocation.
|
||||
|
||||
## Code Patterns
|
||||
|
||||
@@ -152,6 +154,7 @@ Show the active configuration (after all overrides applied):
|
||||
- **`queryPuppetDB(url, query)`**: takes the URL as a parameter — never reads globals.
|
||||
- **`processResults()`**: iterates facts, returns sorted `"certname value"` strings. JSON string values are unquoted; other JSON types rendered as compact JSON.
|
||||
- **Output modes**: JSON (`-j`), count (`-C`), Ansible YAML (`-A`), node-only (`-1`), value-only (`-2`), default (node + value). `-j` and `-A` share `factsByHost()`, so both attach the queried fact(s) per host — as an object under the host (`-j`) or as inventory host vars (`-A`).
|
||||
- **Short node names / `qualifyNode()`**: a `-n` value (and stdin-sourced node names) with no dot is auto-qualified to `<name>.<domain>` (domain defaults to `main.unkin.net`, overridable via `--domain`/`NODE_LOOKUP_DOMAIN`), so `-n ausyd1nxvm2120` resolves the same as its FQDN. A name that already contains a dot (any domain) is left unchanged; a single trailing dot is stripped; empty input is preserved.
|
||||
- **Stdin support**: `stdinReader()` reads node names from stdin only when it is a real pipe/redirect carrying data (and no `-n` given). Terminals, `/dev/null`, and empty/closed pipes fall through to a normal query — so running without a TTY (e.g. invoked by an agent or CI) behaves like an interactive run instead of consuming empty input.
|
||||
- **SIGPIPE handling**: `signal.Ignore(syscall.SIGPIPE)` so pipes to `head` etc. work cleanly.
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ const (
|
||||
defaultPuppetDBURL = "http://puppetdbapi.service.consul:8080/pdb/query/v4/facts"
|
||||
defaultRoleFact = "enc_role"
|
||||
defaultPuppetboardURL = "https://puppetboard.k8s.syd1.au.unkin.net"
|
||||
defaultDomain = "main.unkin.net"
|
||||
configFileName = "config.yaml"
|
||||
appName = "node-lookup"
|
||||
)
|
||||
@@ -37,6 +38,8 @@ type config struct {
|
||||
// the shared config file also configures the companion tools (pburl,
|
||||
// pblastreport) that read this same file.
|
||||
PuppetboardURL string `yaml:"puppetboard_url"`
|
||||
// Domain is appended to a short (dotless) -n node name to form its FQDN.
|
||||
Domain string `yaml:"domain"`
|
||||
}
|
||||
|
||||
func defaultConfig() config {
|
||||
@@ -44,6 +47,7 @@ func defaultConfig() config {
|
||||
PuppetDBURL: defaultPuppetDBURL,
|
||||
RoleFact: defaultRoleFact,
|
||||
PuppetboardURL: defaultPuppetboardURL,
|
||||
Domain: defaultDomain,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +91,9 @@ func loadConfig() (config, error) {
|
||||
if v := os.Getenv("NODE_LOOKUP_PUPPETBOARD_URL"); v != "" {
|
||||
cfg.PuppetboardURL = v
|
||||
}
|
||||
if v := os.Getenv("NODE_LOOKUP_DOMAIN"); v != "" {
|
||||
cfg.Domain = v
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
@@ -105,7 +112,7 @@ func writeDefaultConfig() error {
|
||||
|
||||
cfg := defaultConfig()
|
||||
data, _ := yaml.Marshal(cfg)
|
||||
header := []byte("# node-lookup configuration\n# Fields can be overridden with env vars: NODE_LOOKUP_URL, NODE_LOOKUP_ROLE_FACT, NODE_LOOKUP_PUPPETBOARD_URL\n# puppetboard_url is used by the companion tools (pburl, pblastreport).\n\n")
|
||||
header := []byte("# node-lookup configuration\n# Fields can be overridden with env vars: NODE_LOOKUP_URL, NODE_LOOKUP_ROLE_FACT, NODE_LOOKUP_PUPPETBOARD_URL, NODE_LOOKUP_DOMAIN\n# puppetboard_url is used by the companion tools (pburl, pblastreport).\n# domain is appended to short (dotless) -n node names to form their FQDN.\n\n")
|
||||
if err := os.WriteFile(path, append(header, data...), 0o644); err != nil {
|
||||
return fmt.Errorf("writing config: %w", err)
|
||||
}
|
||||
@@ -144,6 +151,21 @@ func nameFilter(names []string) []interface{} {
|
||||
return or
|
||||
}
|
||||
|
||||
// qualifyNode auto-qualifies a short (dotless) node name by appending
|
||||
// ".<domain>", so `-n ausyd1nxvm2120` resolves the same as its FQDN. A name
|
||||
// that already contains a dot is treated as already-qualified (including names
|
||||
// in other domains like *.k8s.syd1.au.unkin.net) and returned unchanged. A
|
||||
// single trailing dot is stripped first, so a dotless name with a trailing dot
|
||||
// is still qualified. Empty input is returned unchanged to preserve the
|
||||
// existing "no node given" behavior.
|
||||
func qualifyNode(name, domain string) string {
|
||||
name = strings.TrimSuffix(name, ".")
|
||||
if name == "" || strings.Contains(name, ".") {
|
||||
return name
|
||||
}
|
||||
return name + "." + domain
|
||||
}
|
||||
|
||||
func buildQuery(node, factName, match, roleFact string, showRole, partial, inverse bool) string {
|
||||
type filter = []interface{}
|
||||
var filters []filter
|
||||
@@ -318,6 +340,8 @@ func allFactsForNode(puppetDBURL, node string) ([]fact, error) {
|
||||
func run(cfg config, nodeName, factName, match string, showRole, partial, inverse, nodeOnly, valueOnly, count, ansible, jsonMode, allFacts bool) error {
|
||||
signal.Ignore(syscall.SIGPIPE)
|
||||
|
||||
nodeName = qualifyNode(nodeName, cfg.Domain)
|
||||
|
||||
if allFacts {
|
||||
if nodeName == "" {
|
||||
return fmt.Errorf("-a requires -n")
|
||||
@@ -333,15 +357,38 @@ func run(cfg config, nodeName, factName, match string, showRole, partial, invers
|
||||
return nil
|
||||
}
|
||||
|
||||
if (nodeOnly || valueOnly || count || ansible) && !showRole && factName == "" {
|
||||
return fmt.Errorf("-R or -F must be used with -1, -2, -C, or -A")
|
||||
hasQuery := showRole || factName != ""
|
||||
// Bare -C counts already-looked-up piped lines; only it needs stdin to validate.
|
||||
bareCount := count && !hasQuery && !nodeOnly && !valueOnly && !ansible && !jsonMode && nodeName == ""
|
||||
countErr := fmt.Errorf("-R or -F must be used with -1, -2, -C, or -A")
|
||||
|
||||
if (nodeOnly || valueOnly || (count && !bareCount) || ansible) && !hasQuery {
|
||||
return countErr
|
||||
}
|
||||
if (match != "" || partial || inverse) && !showRole && factName == "" {
|
||||
if (match != "" || partial || inverse) && !hasQuery {
|
||||
return fmt.Errorf("-R or -F must be used with -m, -p, or -i")
|
||||
}
|
||||
|
||||
var reader *bufio.Reader
|
||||
piped := false
|
||||
if nodeName == "" {
|
||||
reader, piped = stdinReader(os.Stdin)
|
||||
}
|
||||
|
||||
if bareCount {
|
||||
if !piped {
|
||||
return countErr
|
||||
}
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
fmt.Println(strings.Join(countResults(lines), "\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
var collected []fact
|
||||
var stdinLines []string
|
||||
|
||||
doQuery := func(node string) error {
|
||||
query := buildQuery(node, factName, match, cfg.RoleFact, showRole, partial, inverse)
|
||||
@@ -353,21 +400,15 @@ func run(cfg config, nodeName, factName, match string, showRole, partial, invers
|
||||
return nil
|
||||
}
|
||||
|
||||
if reader, ok := stdinReader(os.Stdin); ok && nodeName == "" {
|
||||
if piped {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
if count {
|
||||
for scanner.Scan() {
|
||||
stdinLines = append(stdinLines, scanner.Text())
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) == 0 {
|
||||
continue
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
if err := doQuery(qualifyNode(fields[0], cfg.Domain)); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error:", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -386,11 +427,7 @@ func run(cfg config, nodeName, factName, match string, showRole, partial, invers
|
||||
_ = enc.Encode(factsByHost(collected, factName, cfg.RoleFact, showRole))
|
||||
|
||||
case count:
|
||||
values := stdinLines
|
||||
if len(values) == 0 {
|
||||
values = returnData
|
||||
}
|
||||
fmt.Println(strings.Join(countResults(values), "\n"))
|
||||
fmt.Println(strings.Join(countResults(returnData), "\n"))
|
||||
|
||||
case ansible:
|
||||
// Attach each host's queried fact(s) as inventory host vars, e.g.
|
||||
@@ -446,6 +483,7 @@ func main() {
|
||||
jsonMode bool
|
||||
allFacts bool
|
||||
puppetDBURL string
|
||||
domain string
|
||||
)
|
||||
|
||||
rootCmd := &cobra.Command{
|
||||
@@ -461,6 +499,9 @@ func main() {
|
||||
if cmd.Flags().Changed("url") {
|
||||
cfg.PuppetDBURL = puppetDBURL
|
||||
}
|
||||
if cmd.Flags().Changed("domain") {
|
||||
cfg.Domain = domain
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -482,6 +523,7 @@ func main() {
|
||||
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)")
|
||||
f.StringVar(&domain, "domain", cfg.Domain, "Domain appended to short (dotless) -n node names (overrides config and NODE_LOOKUP_DOMAIN)")
|
||||
rootCmd.PersistentFlags().StringVar(&puppetDBURL, "url", cfg.PuppetDBURL, "PuppetDB facts URL (overrides config and NODE_LOOKUP_URL)")
|
||||
|
||||
configCmd := &cobra.Command{
|
||||
@@ -506,6 +548,7 @@ func main() {
|
||||
fmt.Printf("puppetdb_url : %s\n", cfg.PuppetDBURL)
|
||||
fmt.Printf("role_fact : %s\n", cfg.RoleFact)
|
||||
fmt.Printf("puppetboard_url: %s\n", cfg.PuppetboardURL)
|
||||
fmt.Printf("domain : %s\n", cfg.Domain)
|
||||
return nil
|
||||
},
|
||||
SilenceUsage: true,
|
||||
|
||||
+195
@@ -8,7 +8,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -167,6 +169,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 +428,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) {
|
||||
@@ -827,3 +874,151 @@ func TestRun_AllFacts_PrintsSortedByName(t *testing.T) {
|
||||
t.Fatalf("facts not sorted by name: %v", lines)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- run: count with piped stdin --------------------------------------------
|
||||
|
||||
func withStdin(t *testing.T, content string) {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "stdin")
|
||||
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
old := os.Stdin
|
||||
os.Stdin = f
|
||||
t.Cleanup(func() { os.Stdin = old; _ = f.Close() })
|
||||
}
|
||||
|
||||
const pipedLookup = "hosta.main.unkin.net puppetdbapi.service.consul\nhostb.main.unkin.net puppetdbapi.service.consul\n"
|
||||
|
||||
func TestRun_CountPipedWithRoleQueriesEachNode(t *testing.T) {
|
||||
withStdin(t, pipedLookup)
|
||||
var queried atomic.Int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
queried.Add(1)
|
||||
_ = json.NewEncoder(w).Encode([]fact{{Certname: "h", Name: "enc_role", Value: rawJSON("roles::vault")}})
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
cfg := config{PuppetDBURL: srv.URL, RoleFact: "enc_role"}
|
||||
|
||||
out := strings.TrimSpace(captureStdout(t, func() {
|
||||
if err := run(cfg, "", "", "", true, false, false, false, false, true, false, false, false); err != nil {
|
||||
t.Fatalf("run returned error: %v", err)
|
||||
}
|
||||
}))
|
||||
if out != "2: roles::vault" {
|
||||
t.Fatalf("expected role counts of piped nodes, got: %q", out)
|
||||
}
|
||||
if n := queried.Load(); n != 2 {
|
||||
t.Fatalf("expected one query per piped node, got %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_CountPipedWithoutQueryCountsInput(t *testing.T) {
|
||||
withStdin(t, pipedLookup+"hostc.main.unkin.net other\n")
|
||||
cfg := config{PuppetDBURL: "http://unused", RoleFact: "enc_role"}
|
||||
|
||||
out := strings.TrimSpace(captureStdout(t, func() {
|
||||
if err := run(cfg, "", "", "", false, false, false, false, false, true, false, false, false); err != nil {
|
||||
t.Fatalf("run returned error: %v", err)
|
||||
}
|
||||
}))
|
||||
if out != "1: other\n2: puppetdbapi.service.consul" {
|
||||
t.Fatalf("expected counts of piped values, got: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_CountWithoutStdinRequiresRoleOrFact(t *testing.T) {
|
||||
devnull, err := os.Open(os.DevNull)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
old := os.Stdin
|
||||
os.Stdin = devnull
|
||||
t.Cleanup(func() { os.Stdin = old; _ = devnull.Close() })
|
||||
|
||||
err = run(config{PuppetDBURL: "http://unused"}, "", "", "", false, false, false, false, false, true, false, false, false)
|
||||
if err == nil || !strings.Contains(err.Error(), "-R or -F") {
|
||||
t.Fatalf("expected -R/-F requirement error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_CountPipedWithFactQueriesEachNode(t *testing.T) {
|
||||
withStdin(t, pipedLookup)
|
||||
var queried atomic.Int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.URL.Query().Get("query"), "osfamily") {
|
||||
t.Errorf("expected osfamily query, got %q", r.URL.Query().Get("query"))
|
||||
}
|
||||
queried.Add(1)
|
||||
_ = json.NewEncoder(w).Encode([]fact{{Certname: "h", Name: "osfamily", Value: rawJSON("RedHat")}})
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
cfg := config{PuppetDBURL: srv.URL, RoleFact: "enc_role"}
|
||||
|
||||
out := strings.TrimSpace(captureStdout(t, func() {
|
||||
if err := run(cfg, "", "osfamily", "", false, false, false, false, false, true, false, false, false); err != nil {
|
||||
t.Fatalf("run returned error: %v", err)
|
||||
}
|
||||
}))
|
||||
if out != "2: RedHat" {
|
||||
t.Fatalf("expected fact counts of piped nodes, got: %q", out)
|
||||
}
|
||||
if n := queried.Load(); n != 2 {
|
||||
t.Fatalf("expected one query per piped node, got %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_CountPipedWithOtherOutputFlagsRequiresRoleOrFact(t *testing.T) {
|
||||
for name, mutate := range map[string]func(*runArgs){
|
||||
"json": func(a *runArgs) { a.jsonMode = true },
|
||||
"nodeOnly": func(a *runArgs) { a.nodeOnly = true },
|
||||
"valueOnly": func(a *runArgs) { a.valueOnly = true },
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
withStdin(t, pipedLookup)
|
||||
a := runArgs{cfg: config{PuppetDBURL: "http://unused"}, count: true}
|
||||
mutate(&a)
|
||||
var err error
|
||||
out := captureStdout(t, func() {
|
||||
err = run(a.cfg, "", "", "", false, false, false, a.nodeOnly, a.valueOnly, true, false, a.jsonMode, false)
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "-R or -F") {
|
||||
t.Fatalf("expected -R/-F requirement error, got: %v (output %q)", err, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_InvalidFlagsErrorWithoutReadingOpenPipe(t *testing.T) {
|
||||
for name, a := range map[string]runArgs{
|
||||
"match": {match: "foo"},
|
||||
"countJSON": {count: true, jsonMode: true},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
old := os.Stdin
|
||||
os.Stdin = r
|
||||
t.Cleanup(func() { os.Stdin = old; _ = w.Close(); _ = r.Close() })
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- run(config{PuppetDBURL: "http://unused"}, "", "", a.match, false, false, false, false, false, a.count, false, a.jsonMode, false)
|
||||
}()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err == nil || !strings.Contains(err.Error(), "-R or -F") {
|
||||
t.Fatalf("expected -R/-F requirement error, got: %v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("run blocked on an open, empty stdin pipe before validating flags")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user