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

NVIDIA / skyhook / 20106791825

10 Dec 2025 04:59PM UTC coverage: 77.027% (+2.1%) from 74.963%
20106791825

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

1059 of 1277 new or added lines in 13 files covered. (82.93%)

1 existing line in 1 file now uncovered.

5700 of 7400 relevant lines covered (77.03%)

0.88 hits per line

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

78.62
/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
// NewListCmd creates the node list command
46
func NewListCmd(ctx *cliContext.CLIContext) *cobra.Command {
1✔
47
        opts := &nodeListOptions{}
1✔
48

1✔
49
        cmd := &cobra.Command{
1✔
50
                Use:   "list",
1✔
51
                Short: "List all nodes targeted by a Skyhook",
1✔
52
                Long: `List all nodes that have activity for a specific Skyhook.
1✔
53

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

1✔
59
  # Output as JSON
1✔
60
  kubectl skyhook node list --skyhook gpu-init -o json`,
1✔
61
                RunE: func(cmd *cobra.Command, args []string) error {
1✔
NEW
62
                        if opts.skyhookName == "" {
×
NEW
63
                                return fmt.Errorf("--skyhook flag is required")
×
NEW
64
                        }
×
65

NEW
66
                        clientFactory := client.NewFactory(ctx.GlobalFlags.ConfigFlags)
×
NEW
67
                        kubeClient, err := clientFactory.Client()
×
NEW
68
                        if err != nil {
×
NEW
69
                                return fmt.Errorf("initializing kubernetes client: %w", err)
×
NEW
70
                        }
×
71

NEW
72
                        return runNodeList(cmd.Context(), cmd.OutOrStdout(), kubeClient, opts)
×
73
                },
74
        }
75

76
        cmd.Flags().StringVar(&opts.skyhookName, "skyhook", "", "Name of the Skyhook CR (required)")
1✔
77
        cmd.Flags().StringVarP(&opts.output, "output", "o", "table", "Output format: table, json, yaml")
1✔
78

1✔
79
        _ = cmd.MarkFlagRequired("skyhook")
1✔
80

1✔
81
        return cmd
1✔
82
}
83

84
// nodeListEntry represents a node in the list output
85
type nodeListEntry struct {
86
        NodeName         string `json:"nodeName"`
87
        Status           string `json:"status"`
88
        PackagesComplete int    `json:"packagesComplete"`
89
        PackagesTotal    int    `json:"packagesTotal"`
90
}
91

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

99
        annotationKey := nodeStateAnnotationPrefix + opts.skyhookName
1✔
100
        entries := make([]nodeListEntry, 0, len(nodeList.Items))
1✔
101

1✔
102
        for _, node := range nodeList.Items {
2✔
103
                annotation, ok := node.Annotations[annotationKey]
1✔
104
                if !ok {
2✔
105
                        continue
1✔
106
                }
107

108
                var nodeState v1alpha1.NodeState
1✔
109
                if err := json.Unmarshal([]byte(annotation), &nodeState); err != nil {
1✔
NEW
110
                        continue
×
111
                }
112

113
                completeCount := 0
1✔
114
                hasError := false
1✔
115
                hasInProgress := false
1✔
116

1✔
117
                for _, pkgStatus := range nodeState {
2✔
118
                        switch pkgStatus.State {
1✔
119
                        case v1alpha1.StateComplete:
1✔
120
                                completeCount++
1✔
121
                        case v1alpha1.StateErroring:
1✔
122
                                hasError = true
1✔
123
                        case v1alpha1.StateInProgress:
1✔
124
                                hasInProgress = true
1✔
125
                        }
126
                }
127

128
                // Determine overall status
129
                status := string(v1alpha1.StateUnknown)
1✔
130
                if hasError {
2✔
131
                        status = string(v1alpha1.StateErroring)
1✔
132
                } else if completeCount == len(nodeState) && len(nodeState) > 0 {
3✔
133
                        status = string(v1alpha1.StateComplete)
1✔
134
                } else if hasInProgress || completeCount > 0 {
3✔
135
                        status = string(v1alpha1.StateInProgress)
1✔
136
                }
1✔
137

138
                entries = append(entries, nodeListEntry{
1✔
139
                        NodeName:         node.Name,
1✔
140
                        Status:           status,
1✔
141
                        PackagesComplete: completeCount,
1✔
142
                        PackagesTotal:    len(nodeState),
1✔
143
                })
1✔
144
        }
145

146
        // Sort by node name
147
        sort.Slice(entries, func(i, j int) bool {
2✔
148
                return entries[i].NodeName < entries[j].NodeName
1✔
149
        })
1✔
150

151
        if len(entries) == 0 {
2✔
152
                _, _ = fmt.Fprintf(out, "No nodes found for Skyhook %q\n", opts.skyhookName)
1✔
153
                return nil
1✔
154
        }
1✔
155

156
        // Output based on format
157
        switch opts.output {
1✔
158
        case "json":
1✔
159
                return outputNodeListJSON(out, opts.skyhookName, entries)
1✔
NEW
160
        case "yaml":
×
NEW
161
                return outputNodeListYAML(out, opts.skyhookName, entries)
×
162
        default:
1✔
163
                return outputNodeListTable(out, opts.skyhookName, entries)
1✔
164
        }
165
}
166

167
func outputNodeListJSON(out io.Writer, skyhookName string, entries []nodeListEntry) error {
1✔
168
        output := struct {
1✔
169
                SkyhookName string          `json:"skyhookName"`
1✔
170
                Nodes       []nodeListEntry `json:"nodes"`
1✔
171
        }{
1✔
172
                SkyhookName: skyhookName,
1✔
173
                Nodes:       entries,
1✔
174
        }
1✔
175

1✔
176
        data, err := json.MarshalIndent(output, "", "  ")
1✔
177
        if err != nil {
1✔
NEW
178
                return fmt.Errorf("marshaling json: %w", err)
×
NEW
179
        }
×
180
        _, _ = fmt.Fprintln(out, string(data))
1✔
181
        return nil
1✔
182
}
183

NEW
184
func outputNodeListYAML(out io.Writer, skyhookName string, entries []nodeListEntry) error {
×
NEW
185
        output := struct {
×
NEW
186
                SkyhookName string          `yaml:"skyhookName"`
×
NEW
187
                Nodes       []nodeListEntry `yaml:"nodes"`
×
NEW
188
        }{
×
NEW
189
                SkyhookName: skyhookName,
×
NEW
190
                Nodes:       entries,
×
NEW
191
        }
×
NEW
192

×
NEW
193
        data, err := yaml.Marshal(output)
×
NEW
194
        if err != nil {
×
NEW
195
                return fmt.Errorf("marshaling yaml: %w", err)
×
NEW
196
        }
×
NEW
197
        _, _ = fmt.Fprint(out, string(data))
×
NEW
198
        return nil
×
199
}
200

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

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

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

1✔
220
        w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
1✔
221
        _, _ = fmt.Fprintln(w, "NODE\tSTATUS\tPACKAGES")
1✔
222
        _, _ = fmt.Fprintln(w, "----\t------\t--------")
1✔
223

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

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