f296056360
ci/woodpecker/tag/release Pipeline failed
## Why `node-lookup` output is handy for pivoting to Puppetboard, but there was no quick way to turn a list of hosts into Puppetboard node-page URLs, or to see when each host last ran Puppet. These two small tools close that gap and ship in the **same RPM** so they're available wherever `node-lookup` is. ## Changes - Add **`pburl`**: reads hostnames from args or piped `node-lookup` output (first field of each line, de-duped) and prints `<host> <puppetboard-node-page-url>`. - Add **`pblastreport`**: prints `<host>\t<last-report-time>\t<url>` using `report_timestamp` from the PuppetDB v4 `nodes` endpoint. Supports `--relative`/`-r` (relative age) and `--timezone`/`-z <IANA>` (default: local timezone). - Add **`internal/puppet`** package shared by both tools: config load, PuppetDB `nodes` query, Puppetboard URL construction (`<base>/node/<certname>`), and no-TTY-safe stdin host reading. - Add **`puppetboard_url`** config key (env `NODE_LOOKUP_PUPPETBOARD_URL`, default `https://puppetboard.k8s.syd1.au.unkin.net`) to the shared config so `config init`/`config show` scaffold it for the whole tool family. `node-lookup`'s own query behaviour is unchanged. - Build all three binaries individually (each is its own `main` package — a single `go build ./...` can't emit multiple mains) and generate per-binary bash/zsh/fish completions in the Makefile, `build-rpm.sh`, and nfpm spec. - Cross-compile and attach all three tools per os/arch in the release pipeline; extend `.gitignore`; `go mod tidy` promotes cobra/yaml to direct deps. - Document the tools, config key, and env var in `AGENTS.md`. ## Testing - `go test -race ./...` passes (new tests cover config precedence, `nodes` endpoint derivation, host-page URLs, `LookupNode`, stdin host parsing, and the report-time formatting incl. timezone/relative/edge cases). - Built the RPM locally and confirmed it installs all 3 binaries + 9 completion files. - Smoke-tested both tools end-to-end against a mock PuppetDB (timezone conversion, relative time, and error handling all correct). No cross-repo changes needed: the release reuses the existing `default` ServiceAccount and the artifactapi `rpm-internal` upload. Reviewed-on: #15 Co-authored-by: Ben Vincent <ben@unkin.net> Co-committed-by: Ben Vincent <ben@unkin.net>
67 lines
1.8 KiB
Go
67 lines
1.8 KiB
Go
package puppet
|
|
|
|
import (
|
|
"bufio"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// StdinReader returns a buffered reader over f and true only when f actually
|
|
// carries piped/redirected data. Terminals and character devices such as
|
|
// /dev/null return false, and an empty pipe or empty file (immediate EOF on
|
|
// peek) also returns false. This mirrors node-lookup's no-TTY behaviour: when
|
|
// invoked without a real pipe the caller can fall back to arguments instead of
|
|
// blocking on or silently consuming empty input.
|
|
func StdinReader(f *os.File) (*bufio.Reader, bool) {
|
|
fi, err := f.Stat()
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
if (fi.Mode() & os.ModeCharDevice) != 0 {
|
|
return nil, false // terminal or /dev/null
|
|
}
|
|
r := bufio.NewReader(f)
|
|
if _, err := r.Peek(1); err != nil {
|
|
return nil, false // empty pipe / empty file (EOF)
|
|
}
|
|
return r, true
|
|
}
|
|
|
|
// ReadHosts resolves the list of hostnames to act on. Explicit args win; failing
|
|
// that it reads the first whitespace-separated field of each non-empty line from
|
|
// stdin (so `node-lookup -R | pburl` and `node-lookup -1 | pburl` both work).
|
|
// Order is preserved and duplicates are removed. Returns nil when neither args
|
|
// nor piped stdin data are present.
|
|
func ReadHosts(stdin *os.File, args []string) []string {
|
|
if len(args) > 0 {
|
|
return dedupe(args)
|
|
}
|
|
r, ok := StdinReader(stdin)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
var hosts []string
|
|
sc := bufio.NewScanner(r)
|
|
for sc.Scan() {
|
|
fields := strings.Fields(sc.Text())
|
|
if len(fields) == 0 {
|
|
continue
|
|
}
|
|
hosts = append(hosts, fields[0])
|
|
}
|
|
return dedupe(hosts)
|
|
}
|
|
|
|
func dedupe(in []string) []string {
|
|
seen := make(map[string]struct{}, len(in))
|
|
out := make([]string, 0, len(in))
|
|
for _, s := range in {
|
|
if _, ok := seen[s]; ok {
|
|
continue
|
|
}
|
|
seen[s] = struct{}{}
|
|
out = append(out, s)
|
|
}
|
|
return out
|
|
}
|