11package cmd
22
33import (
4+ "bufio"
45 "context"
6+ "errors"
57 "fmt"
8+ "os"
69 "os/user"
10+ "strings"
11+
12+ "golang.org/x/term"
713
814 "github.com/devbytes-cloud/conditioner/pkg/config"
915 "github.com/devbytes-cloud/conditioner/pkg/jsonpatch"
@@ -28,10 +34,13 @@ kubectl conditioner my-node --type DiskPressure --status false --reason KubeletH
2834
2935# Remove a condition from a node
3036kubectl conditioner my-node --type NetworkUnavailable --remove
37+
38+ # Apply a condition to all nodes by piping kubectl output directly
39+ kubectl get nodes -o name | kubectl conditioner --type Ready --status true --reason KubeletReady --message "kubelet is posting ready status"
3140`
3241
3342 long = `The 'conditioner' command allows you to add, update, or remove status conditions on nodes.
34- You need to provide the node name as an argument and use flags to specify the details of the condition.
43+ You need to provide one or more node names as arguments and use flags to specify the details of the condition.
3544The '--type' flag is required and it specifies the type of condition you wish to interact with.
3645The '--status' flag sets the status for the specific status condition and it can be 'true', 'false', or left blank for 'unknown'.
3746The '--reason' flag sets the reason for the specific status condition.
@@ -50,8 +59,8 @@ type ConditionOptions struct {
5059 // IOStreams provides the standard names for iostreams. This is useful for embedding and for unit testing.
5160 genericiooptions.IOStreams
5261
53- // nodeName is the name of the node that the command is being run against.
54- nodeName string
62+ // nodeNames are the names of the nodes that the command is being run against.
63+ nodeNames [] string
5564
5665 // remove is a boolean that indicates whether the condition should be removed.
5766 remove bool
@@ -71,23 +80,31 @@ func NewConditionOptions(streams genericiooptions.IOStreams) *ConditionOptions {
7180 }
7281}
7382
83+ // NewCmdCondition returns a cobra.Command that implements the conditioner subcommand.
84+ // It wires up flags, PreRunE (node name collection from args and stdin), and RunE
85+ // (config loading, completion, and execution).
7486func NewCmdCondition (streams genericiooptions.IOStreams ) * cobra.Command {
7587 o := NewConditionOptions (streams )
7688
7789 cmd := & cobra.Command {
78- Use : "conditioner [node name] [flags]" ,
90+ Use : "conditioner [node name ... ] [flags]" ,
7991 Short : "Manipulate status conditions on a specified node." ,
8092 Long : long ,
8193 Example : example ,
8294 SilenceUsage : true ,
8395 PreRunE : func (cmd * cobra.Command , args []string ) error {
8496 o .args = args
85- if len (o .args ) != 1 {
86- return fmt .Errorf ("must provide a node to be conditioned" )
97+
98+ stdinNames , err := o .readStdinNames ()
99+ if err != nil {
100+ return err
87101 }
88102
89- o .nodeName = o .args [0 ]
90- return nil
103+ merged := make ([]string , 0 , len (stdinNames )+ len (args ))
104+ merged = append (merged , stdinNames ... )
105+ merged = append (merged , args ... )
106+
107+ return o .setNodeNames (merged )
91108 },
92109 RunE : func (c * cobra.Command , args []string ) error {
93110 fs := config.FS {}
@@ -123,6 +140,27 @@ func NewCmdCondition(streams genericiooptions.IOStreams) *cobra.Command {
123140 return cmd
124141}
125142
143+ // setNodeNames validates and normalizes the provided node name arguments, storing the
144+ // results in o.nodeNames. It returns an error if no names are supplied or if any
145+ // name is empty after normalization.
146+ func (o * ConditionOptions ) setNodeNames (args []string ) error {
147+ if len (args ) == 0 {
148+ return fmt .Errorf ("must provide at least one node to be conditioned" )
149+ }
150+
151+ o .nodeNames = make ([]string , 0 , len (args ))
152+ for _ , rawName := range args {
153+ nodeName := normalizeNodeName (rawName )
154+ if nodeName == "" {
155+ return fmt .Errorf ("node name cannot be empty" )
156+ }
157+
158+ o .nodeNames = append (o .nodeNames , nodeName )
159+ }
160+
161+ return nil
162+ }
163+
126164// Complete sets all information required for updating the current context
127165// It retrieves the restConfig from the configFlags and creates a new Kubernetes client.
128166// It also sets the condition status, reason, message, type, and remove flag from the command flags.
@@ -199,7 +237,26 @@ func (o *ConditionOptions) Complete(cmd *cobra.Command, _ []string, config *conf
199237
200238// Run handles the condition applying or removal on nodes.
201239func (o * ConditionOptions ) Run () error {
202- node , err := o .client .CoreV1 ().Nodes ().Get (context .Background (), o .nodeName , metav1.GetOptions {})
240+ var errs []error
241+
242+ for _ , nodeName := range o .nodeNames {
243+ if err := o .runForNode (nodeName ); err != nil {
244+ errs = append (errs , fmt .Errorf ("%s: %w" , nodeName , err ))
245+ }
246+ }
247+
248+ if len (errs ) > 0 {
249+ return errors .Join (errs ... )
250+ }
251+
252+ return nil
253+ }
254+
255+ // runForNode applies or removes the configured condition on a single node. It fetches
256+ // the node from the Kubernetes API, generates the appropriate JSON Patch operation,
257+ // applies it to the node's status, and prints a confirmation message.
258+ func (o * ConditionOptions ) runForNode (nodeName string ) error {
259+ node , err := o .client .CoreV1 ().Nodes ().Get (context .Background (), nodeName , metav1.GetOptions {})
203260 if err != nil {
204261 return err
205262 }
@@ -227,6 +284,39 @@ func (o *ConditionOptions) Run() error {
227284 return nil
228285}
229286
287+ // readStdinNames reads node names from o.In when it is not a TTY. Each non-empty line
288+ // is returned as a raw name; normalization happens later in setNodeNames. It returns
289+ // nil, nil when o.In is an interactive terminal, so interactive invocations are not
290+ // blocked waiting for input.
291+ func (o * ConditionOptions ) readStdinNames () ([]string , error ) {
292+ f , ok := o .In .(* os.File )
293+ if ok && term .IsTerminal (int (f .Fd ())) {
294+ return nil , nil
295+ }
296+
297+ var names []string
298+ scanner := bufio .NewScanner (o .In )
299+ for scanner .Scan () {
300+ line := strings .TrimSpace (scanner .Text ())
301+ if line != "" {
302+ names = append (names , line )
303+ }
304+ }
305+ if err := scanner .Err (); err != nil {
306+ return nil , fmt .Errorf ("reading stdin: %w" , err )
307+ }
308+ return names , nil
309+ }
310+
311+ // normalizeNodeName strips whitespace and the "node/" or "nodes/" prefixes that
312+ // kubectl outputs when using -o name (e.g. "node/worker-01" → "worker-01").
313+ func normalizeNodeName (node string ) string {
314+ node = strings .TrimSpace (node )
315+ node = strings .TrimPrefix (node , "node/" )
316+ node = strings .TrimPrefix (node , "nodes/" )
317+ return node
318+ }
319+
230320// findConditionType is a function that searches for a specific condition type in a slice of NodeCondition objects.
231321// If a match is found, the function returns a pointer to the matching NodeCondition object and its index in the slice.
232322// If no match is found, the function returns nil and -1.
0 commit comments