Package as RPM, add completions, fix no-TTY behavior, repair tests
The test suite no longer compiled after the -p/-i match-modifier refactor, there was no RPM packaging or shell-completion story, and running without a TTY against an empty pipe silently returned nothing. - Replace the fragile !isTerminal stdin check with stdinReader(): only read node names when stdin is a real pipe/redirect carrying data, so /dev/null, terminals, and empty/closed pipes fall through to a normal query (works when invoked by an agent/CI with no TTY). - Rewrite main_test.go to match the current buildQuery/run signatures and add coverage for match modifiers, output modes, config precedence, and the new stdinReader logic. - Add nfpm packaging (packaging/nfpm.yaml, scripts/build-rpm.sh) that installs the binary to /usr/bin and bundles generated bash/zsh/fish completions. - Rework the Makefile to build into dist/ and add completions/rpm targets. - Split the PR CI into build + test + pre-commit workflows and extend release to build the RPM and PUT it to the artifactapi rpm-internal repo; every step now sets a serviceAccount and k8s resources.
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