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

NVIDIA / skyhook / 20150581929

11 Dec 2025 11:16PM UTC coverage: 77.153%. First build
20150581929

Pull #125

github

t0mmylam
feat: Consolidate CLI e2e tests with proper assertions and CI integration
Pull Request #125: feat: Create CLI e2e tests with assertions and CI integration

1085 of 1296 new or added lines in 13 files covered. (83.72%)

5724 of 7419 relevant lines covered (77.15%)

0.88 hits per line

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

79.05
/operator/internal/cli/node/node_list.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
        "text/tabwriter"
29

30
        "github.com/spf13/cobra"
31
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32
        "sigs.k8s.io/yaml"
33

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

39
// nodeListOptions holds the options for the node list command
40
type nodeListOptions struct {
41
        skyhookName string
42
        output      string
43
}
44

45
// BindToCmd binds the options to the command flags
46
func (o *nodeListOptions) BindToCmd(cmd *cobra.Command) {
1✔
47
        cmd.Flags().StringVar(&o.skyhookName, "skyhook", "", "Name of the Skyhook CR (required)")
1✔
48
        cmd.Flags().StringVarP(&o.output, "output", "o", "table", "Output format: table, json, yaml")
1✔
49

1✔
50
        _ = cmd.MarkFlagRequired("skyhook")
1✔
51
}
1✔
52

53
// NewListCmd creates the node list command
54
func NewListCmd(ctx *cliContext.CLIContext) *cobra.Command {
1✔
55
        opts := &nodeListOptions{}
1✔
56

1✔
57
        cmd := &cobra.Command{
1✔
58
                Use:   "list",
1✔
59
                Short: "List all nodes targeted by a Skyhook",
1✔
60
                Long: `List all nodes that have activity for a specific Skyhook.
1✔
61

1✔
62
This command shows all nodes that have Skyhook state annotations for the
1✔
63
specified Skyhook CR, along with a summary of package completion status.`,
1✔
64
                Example: `  # List all nodes targeted by gpu-init Skyhook
1✔
65
  kubectl skyhook node list --skyhook gpu-init
1✔
66

1✔
67
  # Output as JSON
1✔
68
  kubectl skyhook node list --skyhook gpu-init -o json`,
1✔
69
                RunE: func(cmd *cobra.Command, args []string) error {
1✔
NEW
70
                        if opts.skyhookName == "" {
×
NEW
71
                                return fmt.Errorf("--skyhook flag is required")
×
NEW
72
                        }
×
73

NEW
74
                        clientFactory := client.NewFactory(ctx.GlobalFlags.ConfigFlags)
×
NEW
75
                        kubeClient, err := clientFactory.Client()
×
NEW
76
                        if err != nil {
×
NEW
77
                                return fmt.Errorf("initializing kubernetes client: %w", err)
×
NEW
78
                        }
×
79

NEW
80
                        return runNodeList(cmd.Context(), cmd.OutOrStdout(), kubeClient, opts)
×
81
                },
82
        }
83

84
        opts.BindToCmd(cmd)
1✔
85

1✔
86
        return cmd
1✔
87
}
88

89
// nodeListEntry represents a node in the list output
90
type nodeListEntry struct {
91
        NodeName         string `json:"nodeName"`
92
        Status           string `json:"status"`
93
        PackagesComplete int    `json:"packagesComplete"`
94
        PackagesTotal    int    `json:"packagesTotal"`
95
}
96

97
func runNodeList(ctx context.Context, out io.Writer, kubeClient *client.Client, opts *nodeListOptions) error {
1✔
98
        // Get all nodes
1✔
99
        nodeList, err := kubeClient.Kubernetes().CoreV1().Nodes().List(ctx, metav1.ListOptions{})
1✔
100
        if err != nil {
1✔
NEW
101
                return fmt.Errorf("listing nodes: %w", err)
×
NEW
102
        }
×
103

104
        annotationKey := nodeStateAnnotationPrefix + opts.skyhookName
1✔
105
        entries := make([]nodeListEntry, 0, len(nodeList.Items))
1✔
106

1✔
107
        for _, node := range nodeList.Items {
2✔
108
                annotation, ok := node.Annotations[annotationKey]
1✔
109
                if !ok {
2✔
110
                        continue
1✔
111
                }
112

113
                var nodeState v1alpha1.NodeState
1✔
114
                if err := json.Unmarshal([]byte(annotation), &nodeState); err != nil {
1✔
NEW
115
                        continue
×
116
                }
117

118
                completeCount := 0
1✔
119
                hasError := false
1✔
120
                hasInProgress := false
1✔
121

1✔
122
                for _, pkgStatus := range nodeState {
2✔
123
                        switch pkgStatus.State {
1✔
124
                        case v1alpha1.StateComplete:
1✔
125
                                completeCount++
1✔
126
                        case v1alpha1.StateErroring:
1✔
127
                                hasError = true
1✔
128
                        case v1alpha1.StateInProgress:
1✔
129
                                hasInProgress = true
1✔
130
                        }
131
                }
132

133
                // Determine overall status
134
                status := string(v1alpha1.StateUnknown)
1✔
135
                if hasError {
2✔
136
                        status = string(v1alpha1.StateErroring)
1✔
137
                } else if completeCount == len(nodeState) && len(nodeState) > 0 {
3✔
138
                        status = string(v1alpha1.StateComplete)
1✔
139
                } else if hasInProgress || completeCount > 0 {
3✔
140
                        status = string(v1alpha1.StateInProgress)
1✔
141
                }
1✔
142

143
                entries = append(entries, nodeListEntry{
1✔
144
                        NodeName:         node.Name,
1✔
145
                        Status:           status,
1✔
146
                        PackagesComplete: completeCount,
1✔
147
                        PackagesTotal:    len(nodeState),
1✔
148
                })
1✔
149
        }
150

151
        // Sort by node name
152
        sort.Slice(entries, func(i, j int) bool {
2✔
153
                return entries[i].NodeName < entries[j].NodeName
1✔
154
        })
1✔
155

156
        if len(entries) == 0 {
2✔
157
                _, _ = fmt.Fprintf(out, "No nodes found for Skyhook %q\n", opts.skyhookName)
1✔
158
                return nil
1✔
159
        }
1✔
160

161
        // Output based on format
162
        switch opts.output {
1✔
163
        case "json":
1✔
164
                return outputNodeListJSON(out, opts.skyhookName, entries)
1✔
NEW
165
        case "yaml":
×
NEW
166
                return outputNodeListYAML(out, opts.skyhookName, entries)
×
167
        default:
1✔
168
                return outputNodeListTable(out, opts.skyhookName, entries)
1✔
169
        }
170
}
171

172
func outputNodeListJSON(out io.Writer, skyhookName string, entries []nodeListEntry) error {
1✔
173
        output := struct {
1✔
174
                SkyhookName string          `json:"skyhookName"`
1✔
175
                Nodes       []nodeListEntry `json:"nodes"`
1✔
176
        }{
1✔
177
                SkyhookName: skyhookName,
1✔
178
                Nodes:       entries,
1✔
179
        }
1✔
180

1✔
181
        data, err := json.MarshalIndent(output, "", "  ")
1✔
182
        if err != nil {
1✔
NEW
183
                return fmt.Errorf("marshaling json: %w", err)
×
NEW
184
        }
×
185
        _, _ = fmt.Fprintln(out, string(data))
1✔
186
        return nil
1✔
187
}
188

NEW
189
func outputNodeListYAML(out io.Writer, skyhookName string, entries []nodeListEntry) error {
×
NEW
190
        output := struct {
×
NEW
191
                SkyhookName string          `yaml:"skyhookName"`
×
NEW
192
                Nodes       []nodeListEntry `yaml:"nodes"`
×
NEW
193
        }{
×
NEW
194
                SkyhookName: skyhookName,
×
NEW
195
                Nodes:       entries,
×
NEW
196
        }
×
NEW
197

×
NEW
198
        data, err := yaml.Marshal(output)
×
NEW
199
        if err != nil {
×
NEW
200
                return fmt.Errorf("marshaling yaml: %w", err)
×
NEW
201
        }
×
NEW
202
        _, _ = fmt.Fprint(out, string(data))
×
NEW
203
        return nil
×
204
}
205

206
func outputNodeListTable(out io.Writer, skyhookName string, entries []nodeListEntry) error {
1✔
207
        _, _ = fmt.Fprintf(out, "Skyhook: %s\n\n", skyhookName)
1✔
208

1✔
209
        // Calculate summary
1✔
210
        totalNodes := len(entries)
1✔
211
        completeNodes := 0
1✔
212
        errorNodes := 0
1✔
213
        for _, e := range entries {
2✔
214
                switch e.Status {
1✔
215
                case string(v1alpha1.StateComplete):
1✔
216
                        completeNodes++
1✔
217
                case string(v1alpha1.StateErroring):
1✔
218
                        errorNodes++
1✔
219
                }
220
        }
221

222
        _, _ = fmt.Fprintf(out, "Summary: %d nodes (%d complete, %d erroring, %d in progress)\n\n",
1✔
223
                totalNodes, completeNodes, errorNodes, totalNodes-completeNodes-errorNodes)
1✔
224

1✔
225
        w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
1✔
226
        _, _ = fmt.Fprintln(w, "NODE\tSTATUS\tPACKAGES")
1✔
227
        _, _ = fmt.Fprintln(w, "----\t------\t--------")
1✔
228

1✔
229
        for _, e := range entries {
2✔
230
                status := e.Status
1✔
231
                if e.Status == string(v1alpha1.StateErroring) {
2✔
232
                        status = strings.ToUpper(status)
1✔
233
                }
1✔
234
                _, _ = fmt.Fprintf(w, "%s\t%s\t%d/%d\n",
1✔
235
                        e.NodeName, status, e.PackagesComplete, e.PackagesTotal)
1✔
236
        }
237

238
        return w.Flush()
1✔
239
}
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