Relocate packaging: RPM, shell completions, no-TTY fix, repaired tests (#13)
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
## Why The unit tests stopped compiling after the `--pm` → `-p`/`-i` match-modifier refactor was left uncommitted, there was no RPM/completions distribution story, and invoking the tool without a TTY against an empty pipe silently returned nothing. This makes the project releasable and safe to run from agents/CI. ## Changes - Make stdin handling robust: replace the fragile `!isTerminal` check with `stdinReader()`, which only reads node names when stdin is a real pipe/redirect carrying data. Terminals, `/dev/null`, and empty/closed pipes now fall through to a normal query, so running without a TTY behaves like an interactive run. - Repair and expand `main_test.go` to match the current `buildQuery`/`run` signatures; add coverage for the match modifiers, all output modes, config precedence, and the new `stdinReader` logic. `httptest` stubs PuppetDB (no live deps). - Add nfpm packaging (`packaging/nfpm.yaml`, `scripts/build-rpm.sh`): installs the binary to `/usr/bin/node-lookup` and bundles generated bash/zsh/fish completions under the standard system paths. - Rework the Makefile to build into `dist/` and add `completions`/`rpm` targets. - Split PR CI into `build`, `test`, and `pre-commit` workflows and extend `release` to build the RPM and `PUT` it to the artifactapi `rpm-internal` repo. Every step sets a `serviceAccount` and k8s resources. The project directory has also been relocated under `prodenv`. Reviewed-on: #13 Co-authored-by: Ben Vincent <ben@unkin.net> Co-committed-by: Ben Vincent <ben@unkin.net>
This commit was merged in pull request #13.
This commit is contained in:
@@ -110,7 +110,7 @@ type fact struct {
|
||||
Value json.RawMessage `json:"value"`
|
||||
}
|
||||
|
||||
func buildQuery(node, factName, match, partialMatch, roleFact string, showRole bool) string {
|
||||
func buildQuery(node, factName, match, roleFact string, showRole, partial, inverse bool) string {
|
||||
type filter = []interface{}
|
||||
var filters []filter
|
||||
|
||||
@@ -124,9 +124,16 @@ func buildQuery(node, factName, match, partialMatch, roleFact string, showRole b
|
||||
}
|
||||
|
||||
if match != "" {
|
||||
filters = append(filters, filter{"=", "value", match})
|
||||
} else if partialMatch != "" {
|
||||
filters = append(filters, filter{"~", "value", partialMatch})
|
||||
op := "="
|
||||
if partial {
|
||||
op = "~"
|
||||
}
|
||||
inner := filter{op, "value", match}
|
||||
if inverse {
|
||||
filters = append(filters, filter{"not", inner})
|
||||
} else {
|
||||
filters = append(filters, inner)
|
||||
}
|
||||
}
|
||||
|
||||
if len(filters) == 0 {
|
||||
@@ -207,12 +214,26 @@ func countResults(lines []string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func isTerminal(f *os.File) bool {
|
||||
// stdinReader returns a buffered reader over f and true only when f actually
|
||||
// carries piped/redirected data to consume as node names. 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 means running without a
|
||||
// TTY — e.g. invoked by an agent or CI where stdin is /dev/null or a closed
|
||||
// pipe — falls through to a normal query instead of silently consuming empty
|
||||
// input and printing nothing.
|
||||
func stdinReader(f *os.File) (*bufio.Reader, bool) {
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
return nil, false
|
||||
}
|
||||
return (fi.Mode() & os.ModeCharDevice) != 0
|
||||
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
|
||||
}
|
||||
|
||||
func allFactsForNode(puppetDBURL, node string) ([]fact, error) {
|
||||
@@ -220,7 +241,7 @@ func allFactsForNode(puppetDBURL, node string) ([]fact, error) {
|
||||
return queryPuppetDB(puppetDBURL, string(query))
|
||||
}
|
||||
|
||||
func run(cfg config, nodeName, factName, match, partialMatch string, showRole, nodeOnly, valueOnly, count, ansible, jsonMode, allFacts bool) error {
|
||||
func run(cfg config, nodeName, factName, match string, showRole, partial, inverse, nodeOnly, valueOnly, count, ansible, jsonMode, allFacts bool) error {
|
||||
signal.Ignore(syscall.SIGPIPE)
|
||||
|
||||
if allFacts {
|
||||
@@ -241,12 +262,15 @@ func run(cfg config, nodeName, factName, match, partialMatch string, showRole, n
|
||||
if (nodeOnly || valueOnly || count || ansible) && !showRole && factName == "" {
|
||||
return fmt.Errorf("-R or -F must be used with -1, -2, -C, or -A")
|
||||
}
|
||||
if (match != "" || partial || inverse) && !showRole && factName == "" {
|
||||
return fmt.Errorf("-R or -F must be used with -m, -p, or -i")
|
||||
}
|
||||
|
||||
var collected []fact
|
||||
var stdinLines []string
|
||||
|
||||
doQuery := func(node string) error {
|
||||
query := buildQuery(node, factName, match, partialMatch, cfg.RoleFact, showRole)
|
||||
query := buildQuery(node, factName, match, cfg.RoleFact, showRole, partial, inverse)
|
||||
facts, err := queryPuppetDB(cfg.PuppetDBURL, query)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -255,8 +279,8 @@ func run(cfg config, nodeName, factName, match, partialMatch string, showRole, n
|
||||
return nil
|
||||
}
|
||||
|
||||
if nodeName == "" && !isTerminal(os.Stdin) {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
if reader, ok := stdinReader(os.Stdin); ok && nodeName == "" {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
if count {
|
||||
for scanner.Scan() {
|
||||
stdinLines = append(stdinLines, scanner.Text())
|
||||
@@ -349,18 +373,19 @@ func main() {
|
||||
}
|
||||
|
||||
var (
|
||||
nodeName string
|
||||
factName string
|
||||
showRole bool
|
||||
match string
|
||||
partialMatch string
|
||||
nodeOnly bool
|
||||
valueOnly bool
|
||||
count bool
|
||||
ansible bool
|
||||
jsonMode bool
|
||||
allFacts bool
|
||||
puppetDBURL string
|
||||
nodeName string
|
||||
factName string
|
||||
showRole bool
|
||||
match string
|
||||
partial bool
|
||||
inverse bool
|
||||
nodeOnly bool
|
||||
valueOnly bool
|
||||
count bool
|
||||
ansible bool
|
||||
jsonMode bool
|
||||
allFacts bool
|
||||
puppetDBURL string
|
||||
)
|
||||
|
||||
rootCmd := &cobra.Command{
|
||||
@@ -373,7 +398,7 @@ func main() {
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return run(cfg, nodeName, factName, match, partialMatch, showRole, nodeOnly, valueOnly, count, ansible, jsonMode, allFacts)
|
||||
return run(cfg, nodeName, factName, match, showRole, partial, inverse, nodeOnly, valueOnly, count, ansible, jsonMode, allFacts)
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
@@ -382,8 +407,9 @@ func main() {
|
||||
f.StringVarP(&nodeName, "node", "n", "", "Node name")
|
||||
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 match on value")
|
||||
f.StringVarP(&match, "match", "m", "", "Value to match (use with -p and/or -i)")
|
||||
f.BoolVarP(&partial, "partial", "p", false, "Partial/regex match modifier (combine with -m)")
|
||||
f.BoolVarP(&inverse, "inverse", "i", false, "Inverse match modifier (combine with -m)")
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user