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>
98 lines
2.9 KiB
Go
98 lines
2.9 KiB
Go
// Command pblastreport shows each host's last Puppet report time alongside its
|
|
// Puppetboard node-page URL.
|
|
//
|
|
// It reads hostnames from its arguments or from piped node-lookup output,
|
|
// queries PuppetDB for each node's report_timestamp, and prints a
|
|
// tab-separated "<host> <last-report> <puppetboard-url>" line:
|
|
//
|
|
// node-lookup -R | pblastreport
|
|
// pblastreport --relative host1.example.net
|
|
// pblastreport --timezone Asia/Singapore host1.example.net
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"node-lookup/internal/puppet"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var version = "dev"
|
|
|
|
func main() {
|
|
cfg, err := puppet.Load()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "config error:", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
var (
|
|
relative bool
|
|
tz string
|
|
boardURL string
|
|
pdbURL string
|
|
)
|
|
|
|
root := &cobra.Command{
|
|
Use: "pblastreport [host...]",
|
|
Short: "Show each host's last Puppet report time and Puppetboard URL.",
|
|
Long: "Reads hostnames from arguments or piped node-lookup output and prints, per\n" +
|
|
"host, the time of its last Puppet report and its Puppetboard node-page URL.\n" +
|
|
"Times are shown in the local timezone unless --timezone is given, or as a\n" +
|
|
"relative age with --relative. Example: node-lookup -R | pblastreport -r",
|
|
Args: cobra.ArbitraryArgs,
|
|
SilenceUsage: true,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if cmd.Flags().Changed("puppetboard-url") {
|
|
cfg.PuppetboardURL = boardURL
|
|
}
|
|
if cmd.Flags().Changed("url") {
|
|
cfg.PuppetDBURL = pdbURL
|
|
}
|
|
|
|
loc := time.Local
|
|
if tz != "" {
|
|
l, err := time.LoadLocation(tz)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid timezone %q: %w", tz, err)
|
|
}
|
|
loc = l
|
|
}
|
|
|
|
hosts := puppet.ReadHosts(os.Stdin, args)
|
|
if len(hosts) == 0 {
|
|
return fmt.Errorf("no hosts given (pass as arguments or pipe node-lookup output)")
|
|
}
|
|
|
|
nodesURL := puppet.NodesEndpoint(cfg.PuppetDBURL)
|
|
now := time.Now()
|
|
for _, h := range hosts {
|
|
node, lookupErr := puppet.LookupNode(nodesURL, h)
|
|
when := formatWhen(node, lookupErr, relative, loc, now)
|
|
fmt.Printf("%s\t%s\t%s\n", h, when, puppet.HostPageURL(cfg.PuppetboardURL, h))
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
f := root.Flags()
|
|
f.BoolVarP(&relative, "relative", "r", false, "Show the report time as a relative age (e.g. '3h ago')")
|
|
f.StringVarP(&tz, "timezone", "z", "", "IANA timezone for the report time (e.g. Asia/Singapore); default local")
|
|
f.StringVar(&boardURL, "puppetboard-url", cfg.PuppetboardURL, "Puppetboard base URL (overrides config and NODE_LOOKUP_PUPPETBOARD_URL)")
|
|
f.StringVar(&pdbURL, "url", cfg.PuppetDBURL, "PuppetDB facts URL (overrides config and NODE_LOOKUP_URL)")
|
|
|
|
root.AddCommand(&cobra.Command{
|
|
Use: "version",
|
|
Short: "Print the version",
|
|
Run: func(cmd *cobra.Command, args []string) { fmt.Println(version) },
|
|
SilenceUsage: true,
|
|
})
|
|
|
|
if err := root.Execute(); err != nil {
|
|
os.Exit(1)
|
|
}
|
|
}
|