Skip to content

Commit 58cf579

Browse files
committed
new gpu availability command
1 parent 96ddfd6 commit 58cf579

File tree

3 files changed

+154
-0
lines changed

3 files changed

+154
-0
lines changed

cmd/gpu.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package cmd
2+
import (
3+
"context"
4+
"fmt"
5+
"io"
6+
"net/http"
7+
"net/url"
8+
"os"
9+
"strconv"
10+
"strings"
11+
"time"
12+
"github.com/spf13/cobra"
13+
"github.com/uitml/frink/internal/cli"
14+
"k8s.io/client-go/rest"
15+
"k8s.io/client-go/tools/clientcmd"
16+
)
17+
type gpuContext struct {
18+
cli.CommandContext
19+
ServiceURL string // URL to the gpu-viewer service; if empty, use API server proxy
20+
Format string // "table" or "oneline"
21+
Color bool // enable ANSI colors
22+
Limit int // only used for oneline
23+
Timeout time.Duration // HTTP timeout
24+
}
25+
func newGPUCmd() *cobra.Command {
26+
ctx := &gpuContext{
27+
Format: "table",
28+
Timeout: 5 * time.Second,
29+
}
30+
cmd := &cobra.Command{
31+
Use: "gpu",
32+
Short: "Show cluster GPU availability",
33+
PreRunE: func(cmd *cobra.Command, args []string) error {
34+
return ctx.Initialize(cmd)
35+
},
36+
RunE: ctx.Run,
37+
}
38+
flags := cmd.Flags()
39+
flags.StringVar(&ctx.ServiceURL, "service-url", "", "GPU viewer service URL (env GPU_VIEWER_URL or API proxy if empty)")
40+
flags.StringVar(&ctx.Format, "format", "table", "Output format: table|oneline")
41+
flags.BoolVar(&ctx.Color, "color", false, "Enable ANSI colors")
42+
flags.IntVar(&ctx.Limit, "limit", 0, "Limit number of nodes (only for --format=oneline)")
43+
flags.DurationVar(&ctx.Timeout, "timeout", 5*time.Second, "HTTP request timeout")
44+
return cmd
45+
}
46+
func (ctx *gpuContext) Run(cmd *cobra.Command, args []string) error {
47+
// 1) If user provided a direct URL (flag or env), use it (works with port-forward or Ingress)
48+
if ctx.ServiceURL != "" || os.Getenv("GPU_VIEWER_URL") != "" {
49+
serviceURL := ctx.ServiceURL
50+
if serviceURL == "" {
51+
serviceURL = os.Getenv("GPU_VIEWER_URL")
52+
}
53+
return fetchDirect(cmd, serviceURL, ctx.Format, ctx.Color, ctx.Limit, ctx.Timeout)
54+
}
55+
// 2) Otherwise, go through the API server service proxy using kubeconfig
56+
return fetchViaAPIServerProxy(cmd, ctx.Format, ctx.Color, ctx.Limit, ctx.Timeout)
57+
}
58+
// Direct HTTP call (port-forward or Ingress)
59+
func fetchDirect(cmd *cobra.Command, baseURL, format string, color bool, limit int, timeout time.Duration) error {
60+
u, err := url.Parse(strings.TrimRight(baseURL, "/"))
61+
if err != nil {
62+
return fmt.Errorf("invalid service-url: %w", err)
63+
}
64+
q := u.Query()
65+
switch strings.ToLower(format) {
66+
case "oneline":
67+
q.Set("format", "oneline")
68+
if limit > 0 {
69+
q.Set("limit", strconv.Itoa(limit))
70+
}
71+
case "table", "":
72+
// default
73+
default:
74+
return fmt.Errorf("unknown format %q (use table or oneline)", format)
75+
}
76+
if color {
77+
q.Set("color", "1")
78+
}
79+
u.RawQuery = q.Encode()
80+
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, u.String(), nil)
81+
resp, err := (&http.Client{Timeout: timeout}).Do(req)
82+
if err != nil {
83+
return fmt.Errorf("request failed: %w", err)
84+
}
85+
defer resp.Body.Close()
86+
if resp.StatusCode != http.StatusOK {
87+
b, _ := io.ReadAll(resp.Body)
88+
return fmt.Errorf("service error: %s: %s", resp.Status, string(b))
89+
}
90+
_, err = io.Copy(cmd.OutOrStdout(), resp.Body)
91+
return err
92+
}
93+
// API server service proxy (no port-forward; requires RBAC to get services/proxy)
94+
func fetchViaAPIServerProxy(cmd *cobra.Command, format string, color bool, limit int, timeout time.Duration) error {
95+
// Pick up the same --context flag your root command defines
96+
ctxFlag, _ := cmd.InheritedFlags().GetString("context")
97+
// Load kubeconfig with overrides (so --context is honored)
98+
loading := clientcmd.NewDefaultClientConfigLoadingRules()
99+
overrides := &clientcmd.ConfigOverrides{}
100+
if ctxFlag != "" {
101+
overrides.CurrentContext = ctxFlag
102+
}
103+
cfg, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loading, overrides).ClientConfig()
104+
if err != nil {
105+
return fmt.Errorf("kubeconfig load failed: %w", err)
106+
}
107+
// Authenticated transport to the API server
108+
transport, err := rest.TransportFor(cfg)
109+
if err != nil {
110+
return fmt.Errorf("transport build failed: %w", err)
111+
}
112+
host := strings.TrimRight(cfg.Host, "/")
113+
ns := "gpu-availability"
114+
svc := "gpu-viewer"
115+
scheme := "http"
116+
portName := "http"
117+
// proxy URL that WORKS in your cluster:
118+
proxyURL := fmt.Sprintf("%s/api/v1/namespaces/%s/services/%s:%s:%s/proxy/",
119+
host, ns, scheme, svc, portName,
120+
)
121+
// Query params
122+
u, _ := url.Parse(proxyURL)
123+
q := u.Query()
124+
switch strings.ToLower(format) {
125+
case "oneline":
126+
q.Set("format", "oneline")
127+
if limit > 0 {
128+
q.Set("limit", strconv.Itoa(limit))
129+
}
130+
case "table", "":
131+
// default
132+
default:
133+
return fmt.Errorf("unknown format %q (use table or oneline)", format)
134+
}
135+
if color {
136+
q.Set("color", "1")
137+
}
138+
u.RawQuery = q.Encode()
139+
// Do request via API server proxy
140+
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, u.String(), nil)
141+
client := &http.Client{Transport: transport, Timeout: timeout}
142+
resp, err := client.Do(req)
143+
if err != nil {
144+
return fmt.Errorf("proxy request failed: %w", err)
145+
}
146+
defer resp.Body.Close()
147+
if resp.StatusCode != http.StatusOK {
148+
b, _ := io.ReadAll(resp.Body)
149+
return fmt.Errorf("service/proxy error: %s: %s", resp.Status, string(b))
150+
}
151+
_, err = io.Copy(cmd.OutOrStdout(), resp.Body)
152+
return err
153+
}

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func newRootCmd() *cobra.Command {
2727
cmd.AddCommand(newRunCmd())
2828
cmd.AddCommand(newVersionCmd())
2929
cmd.AddCommand(newDebugCmd())
30+
cmd.AddCommand(newGPUCmd())
3031
cli.DisableFlagsInUseLine(cmd)
3132

3233
return cmd

frink

43.4 MB
Binary file not shown.

0 commit comments

Comments
 (0)