• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NVIDIA / skyhook / 20353746630

18 Dec 2025 10:50PM UTC coverage: 75.716% (-0.2%) from 75.958%
20353746630

Pull #133

github

web-flow
Merge a731af90a into 19dce4787
Pull Request #133: fix: cleanup cli code

29 of 63 new or added lines in 9 files covered. (46.03%)

11 existing lines in 6 files now uncovered.

5818 of 7684 relevant lines covered (75.72%)

1.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.08
/operator/cmd/cli/app/node/node_status.go
1
/*
2
 * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
 * SPDX-License-Identifier: Apache-2.0
4
 *
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18

19
package node
20

21
import (
22
        "context"
23
        "encoding/json"
24
        "fmt"
25
        "io"
26
        "sort"
27
        "strings"
28

29
        "github.com/spf13/cobra"
30
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31

32
        "github.com/NVIDIA/skyhook/operator/api/v1alpha1"
33
        "github.com/NVIDIA/skyhook/operator/internal/cli/client"
34
        cliContext "github.com/NVIDIA/skyhook/operator/internal/cli/context"
35
        "github.com/NVIDIA/skyhook/operator/internal/cli/utils"
36
)
37

38
const nodeStateAnnotationPrefix = v1alpha1.METADATA_PREFIX + "/nodeState_"
39

40
// nodeStatusOptions holds the options for the node status command
41
type nodeStatusOptions struct {
42
        skyhookName string
43
}
44

45
// BindToCmd binds the options to the command flags
46
func (o *nodeStatusOptions) BindToCmd(cmd *cobra.Command) {
1✔
47
        cmd.Flags().StringVar(&o.skyhookName, "skyhook", "", "Filter by Skyhook name")
1✔
48
}
1✔
49

50
// NewStatusCmd creates the node status command
51
func NewStatusCmd(ctx *cliContext.CLIContext) *cobra.Command {
1✔
52
        opts := &nodeStatusOptions{}
1✔
53

1✔
54
        cmd := &cobra.Command{
1✔
55
                Use:   "status [node-name...]",
1✔
56
                Short: "Show all Skyhook activity on specific node(s)",
1✔
57
                Long: `Show all Skyhook activity on specific node(s) by reading node annotations.
1✔
58

1✔
59
This command displays a summary of all Skyhook CRs that have activity on the 
1✔
60
specified node(s), including overall status and package completion counts.
1✔
61

1✔
62
If no node name is provided, all nodes with Skyhook annotations are shown.
1✔
63
Node names can be exact matches or regex patterns.`,
1✔
64
                Example: `  # Show all Skyhook activity on a specific node
1✔
65
  kubectl skyhook node status worker-1
1✔
66

1✔
67
  # Show Skyhook activity on multiple nodes
1✔
68
  kubectl skyhook node status worker-1 worker-2 worker-3
1✔
69

1✔
70
  # Show Skyhook activity on nodes matching a pattern
1✔
71
  kubectl skyhook node status "worker-.*"
1✔
72

1✔
73
  # Filter by specific Skyhook
1✔
74
  kubectl skyhook node status worker-1 --skyhook gpu-init
1✔
75

1✔
76
  # View all nodes with Skyhook activity
1✔
77
  kubectl skyhook node status
1✔
78

1✔
79
  # Output as JSON
1✔
80
  kubectl skyhook node status worker-1 -o json
1✔
81

1✔
82
  # Output with package details
1✔
83
  kubectl skyhook node status worker-1 -o wide`,
1✔
84
                RunE: func(cmd *cobra.Command, args []string) error {
1✔
85
                        clientFactory := client.NewFactory(ctx.GlobalFlags.ConfigFlags)
×
86
                        kubeClient, err := clientFactory.Client()
×
87
                        if err != nil {
×
88
                                return fmt.Errorf("initializing kubernetes client: %w", err)
×
89
                        }
×
90

NEW
91
                        return runNodeStatus(cmd.Context(), kubeClient, args, opts, ctx)
×
92
                },
93
        }
94

95
        opts.BindToCmd(cmd)
1✔
96

1✔
97
        return cmd
1✔
98
}
99

100
// nodeSkyhookSummary represents a summary of Skyhook activity on a node
101
type nodeSkyhookSummary struct {
102
        NodeName         string                 `json:"nodeName"`
103
        SkyhookName      string                 `json:"skyhookName"`
104
        Status           string                 `json:"status"`
105
        PackagesComplete int                    `json:"packagesComplete"`
106
        PackagesTotal    int                    `json:"packagesTotal"`
107
        Packages         []nodeSkyhookPkgStatus `json:"packages,omitempty"`
108
}
109

110
// nodeSkyhookPkgStatus represents the status of a single package
111
type nodeSkyhookPkgStatus struct {
112
        Name     string `json:"name"`
113
        Version  string `json:"version"`
114
        Stage    string `json:"stage"`
115
        State    string `json:"state"`
116
        Restarts int32  `json:"restarts"`
117
        Image    string `json:"image,omitempty"`
118
}
119

120
func runNodeStatus(ctx context.Context, kubeClient *client.Client, nodePatterns []string, opts *nodeStatusOptions, cliCtx *cliContext.CLIContext) error {
1✔
121
        out := cliCtx.Config().OutputWriter
1✔
122
        // Get all nodes
1✔
123
        nodeList, err := kubeClient.Kubernetes().CoreV1().Nodes().List(ctx, metav1.ListOptions{})
1✔
124
        if err != nil {
1✔
125
                return fmt.Errorf("listing nodes: %w", err)
×
126
        }
×
127

128
        // Collect all node names for pattern matching
129
        allNodeNames := make([]string, 0, len(nodeList.Items))
1✔
130
        for _, node := range nodeList.Items {
2✔
131
                allNodeNames = append(allNodeNames, node.Name)
1✔
132
        }
1✔
133

134
        // Filter nodes by pattern if specified
135
        var targetNodes []string
1✔
136
        if len(nodePatterns) > 0 {
2✔
137
                targetNodes, err = utils.MatchNodes(nodePatterns, allNodeNames)
1✔
138
                if err != nil {
1✔
139
                        return fmt.Errorf("matching nodes: %w", err)
×
140
                }
×
141
                if len(targetNodes) == 0 {
1✔
142
                        _, _ = fmt.Fprintf(out, "No nodes matched the specified patterns\n")
×
143
                        return nil
×
144
                }
×
145
        } else {
1✔
146
                targetNodes = allNodeNames
1✔
147
        }
1✔
148

149
        targetNodeSet := make(map[string]bool)
1✔
150
        for _, n := range targetNodes {
2✔
151
                targetNodeSet[n] = true
1✔
152
        }
1✔
153

154
        // Collect status from all nodes with Skyhook annotations
155
        var summaries []nodeSkyhookSummary
1✔
156

1✔
157
        for _, node := range nodeList.Items {
2✔
158
                if !targetNodeSet[node.Name] {
1✔
159
                        continue
×
160
                }
161

162
                // Find all Skyhook annotations on this node
163
                for annotationKey, annotationValue := range node.Annotations {
2✔
164
                        if !strings.HasPrefix(annotationKey, nodeStateAnnotationPrefix) {
1✔
165
                                continue
×
166
                        }
167

168
                        skyhookName := strings.TrimPrefix(annotationKey, nodeStateAnnotationPrefix)
1✔
169

1✔
170
                        // Filter by skyhook name if specified
1✔
171
                        if opts.skyhookName != "" && skyhookName != opts.skyhookName {
2✔
172
                                continue
1✔
173
                        }
174

175
                        var nodeState v1alpha1.NodeState
1✔
176
                        if err := json.Unmarshal([]byte(annotationValue), &nodeState); err != nil {
1✔
NEW
177
                                if cliCtx.GlobalFlags.Verbose {
×
NEW
178
                                        _, _ = fmt.Fprintf(cliCtx.Config().ErrorWriter, "Warning: skipping node %q skyhook %q - invalid annotation: %v\n", node.Name, skyhookName, err)
×
NEW
179
                                }
×
UNCOV
180
                                continue // Skip invalid annotations
×
181
                        }
182

183
                        packages := make([]nodeSkyhookPkgStatus, 0, len(nodeState))
1✔
184
                        completeCount := 0
1✔
185
                        hasError := false
1✔
186
                        hasInProgress := false
1✔
187

1✔
188
                        for _, pkgStatus := range nodeState {
2✔
189
                                packages = append(packages, nodeSkyhookPkgStatus{
1✔
190
                                        Name:     pkgStatus.Name,
1✔
191
                                        Version:  pkgStatus.Version,
1✔
192
                                        Stage:    string(pkgStatus.Stage),
1✔
193
                                        State:    string(pkgStatus.State),
1✔
194
                                        Restarts: pkgStatus.Restarts,
1✔
195
                                        Image:    pkgStatus.Image,
1✔
196
                                })
1✔
197

1✔
198
                                switch pkgStatus.State {
1✔
199
                                case v1alpha1.StateComplete:
1✔
200
                                        completeCount++
1✔
201
                                case v1alpha1.StateErroring:
1✔
202
                                        hasError = true
1✔
203
                                case v1alpha1.StateInProgress:
1✔
204
                                        hasInProgress = true
1✔
205
                                }
206
                        }
207

208
                        // Determine overall status
209
                        status := string(v1alpha1.StateUnknown)
1✔
210
                        if hasError {
2✔
211
                                status = string(v1alpha1.StateErroring)
1✔
212
                        } else if completeCount == len(packages) && len(packages) > 0 {
3✔
213
                                status = string(v1alpha1.StateComplete)
1✔
214
                        } else if hasInProgress || completeCount > 0 {
3✔
215
                                status = string(v1alpha1.StateInProgress)
1✔
216
                        }
1✔
217

218
                        // Sort packages by name
219
                        sort.Slice(packages, func(i, j int) bool {
2✔
220
                                return packages[i].Name < packages[j].Name
1✔
221
                        })
1✔
222

223
                        summaries = append(summaries, nodeSkyhookSummary{
1✔
224
                                NodeName:         node.Name,
1✔
225
                                SkyhookName:      skyhookName,
1✔
226
                                Status:           status,
1✔
227
                                PackagesComplete: completeCount,
1✔
228
                                PackagesTotal:    len(packages),
1✔
229
                                Packages:         packages,
1✔
230
                        })
1✔
231
                }
232
        }
233

234
        // Sort by node name, then skyhook name
235
        sort.Slice(summaries, func(i, j int) bool {
2✔
236
                if summaries[i].NodeName != summaries[j].NodeName {
2✔
237
                        return summaries[i].NodeName < summaries[j].NodeName
1✔
238
                }
1✔
239
                return summaries[i].SkyhookName < summaries[j].SkyhookName
1✔
240
        })
241

242
        if len(summaries) == 0 {
2✔
243
                _, _ = fmt.Fprintf(out, "No Skyhook activity found on specified nodes\n")
1✔
244
                return nil
1✔
245
        }
1✔
246

247
        // Output based on format
248
        switch cliCtx.GlobalFlags.OutputFormat {
1✔
249
        case utils.OutputFormatJSON:
1✔
250
                return utils.OutputJSON(out, summaries)
1✔
NEW
251
        case utils.OutputFormatYAML:
×
252
                return utils.OutputYAML(out, summaries)
×
NEW
253
        case utils.OutputFormatWide:
×
254
                return outputNodeStatusWide(out, summaries)
×
255
        default:
1✔
256
                return outputNodeStatusTable(out, summaries)
1✔
257
        }
258
}
259

260
// nodeStatusTableConfig returns the table configuration for node status output
261
func nodeStatusTableConfig() utils.TableConfig[nodeSkyhookSummary] {
1✔
262
        return utils.TableConfig[nodeSkyhookSummary]{
1✔
263
                Headers: []string{"NODE", "SKYHOOK", "STATUS", "PACKAGES"},
1✔
264
                Extract: func(s nodeSkyhookSummary) []string {
2✔
265
                        return []string{
1✔
266
                                s.NodeName,
1✔
267
                                s.SkyhookName,
1✔
268
                                s.Status,
1✔
269
                                fmt.Sprintf("%d/%d", s.PackagesComplete, s.PackagesTotal),
1✔
270
                        }
1✔
271
                },
1✔
272
        }
273
}
274

275
func outputNodeStatusTable(out io.Writer, summaries []nodeSkyhookSummary) error {
1✔
276
        return utils.OutputTable(out, nodeStatusTableConfig(), summaries)
1✔
277
}
1✔
278

279
// nodeStatusWideEntry represents a flattened entry for wide output (one row per package)
280
type nodeStatusWideEntry struct {
281
        NodeName    string
282
        SkyhookName string
283
        Package     nodeSkyhookPkgStatus
284
}
285

286
func outputNodeStatusWide(out io.Writer, summaries []nodeSkyhookSummary) error {
1✔
287
        // Wide output shows one row per package, not per summary
1✔
288
        cfg := utils.TableConfig[nodeStatusWideEntry]{
1✔
289
                Headers: []string{"NODE", "SKYHOOK", "PACKAGE", "VERSION", "STAGE", "STATE"},
1✔
290
                Extract: func(e nodeStatusWideEntry) []string {
2✔
291
                        return []string{
1✔
292
                                e.NodeName,
1✔
293
                                e.SkyhookName,
1✔
294
                                e.Package.Name,
1✔
295
                                e.Package.Version,
1✔
296
                                e.Package.Stage,
1✔
297
                                e.Package.State,
1✔
298
                        }
1✔
299
                },
1✔
300
                WideHeaders: []string{"RESTARTS", "IMAGE"},
301
                WideExtract: func(e nodeStatusWideEntry) []string {
1✔
302
                        return []string{fmt.Sprintf("%d", e.Package.Restarts), e.Package.Image}
1✔
303
                },
1✔
304
        }
305

306
        // Flatten summaries to per-package entries
307
        var entries []nodeStatusWideEntry
1✔
308
        for _, s := range summaries {
2✔
309
                for _, pkg := range s.Packages {
2✔
310
                        entries = append(entries, nodeStatusWideEntry{
1✔
311
                                NodeName:    s.NodeName,
1✔
312
                                SkyhookName: s.SkyhookName,
1✔
313
                                Package:     pkg,
1✔
314
                        })
1✔
315
                }
1✔
316
        }
317

318
        return utils.OutputWide(out, cfg, entries)
1✔
319
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc