Skip to content

Commit 41a05de

Browse files
kernel-internal[bot]sjmiller609claude
authored
CLI: Update hypeman SDK to 458c6a08c9db195425776427cc42690d987f8e5c and add new commands/flags (#46)
* CLI: Update hypeman SDK to 458c6a08c9db195425776427cc42690d987f8e5c and add new commands/flags Bump the CLI to the latest hypeman-go release and close the remaining SDK coverage gaps for auto-standby, snapshot schedules, and instance wait operations. Made-with: Cursor * Match output format conventions for new commands Add human-readable default (auto) output for wait, auto-standby status, and snapshot schedule set/get commands, matching the pattern used by resources and reclaim-memory. JSON output remains available via --format json for scripting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Match output format conventions for update auto-standby command Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: kernel-internal[bot] <260533166+kernel-internal[bot]@users.noreply.github.com> Co-authored-by: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 415d0b5 commit 41a05de

10 files changed

Lines changed: 697 additions & 3 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ require (
1111
github.com/google/go-containerregistry v0.20.7
1212
github.com/gorilla/websocket v1.5.3
1313
github.com/itchyny/json2yaml v0.1.4
14-
github.com/kernel/hypeman-go v0.16.1-0.20260323172303-508a8c69feb3
14+
github.com/kernel/hypeman-go v0.17.0
1515
github.com/knadh/koanf/parsers/yaml v1.1.0
1616
github.com/knadh/koanf/providers/env v1.1.0
1717
github.com/knadh/koanf/providers/file v1.2.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnV
7878
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
7979
github.com/itchyny/json2yaml v0.1.4 h1:/pErVOXGG5iTyXHi/QKR4y3uzhLjGTEmmJIy97YT+k8=
8080
github.com/itchyny/json2yaml v0.1.4/go.mod h1:6iudhBZdarpjLFRNj+clWLAkGft+9uCcjAZYXUH9eGI=
81-
github.com/kernel/hypeman-go v0.16.1-0.20260323172303-508a8c69feb3 h1:g6qT9G/Qrxqqdl9gjqTnhDAHlePxV68OyQjlqXA6WX4=
82-
github.com/kernel/hypeman-go v0.16.1-0.20260323172303-508a8c69feb3/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI=
81+
github.com/kernel/hypeman-go v0.17.0 h1:OaGS0pFUwXYaFtlXaIQleokgNM7Z+KO0mGhL953yiMQ=
82+
github.com/kernel/hypeman-go v0.17.0/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI=
8383
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
8484
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
8585
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=

pkg/cmd/autostandbycmd.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/kernel/hypeman-go"
11+
"github.com/kernel/hypeman-go/option"
12+
"github.com/tidwall/gjson"
13+
"github.com/urfave/cli/v3"
14+
)
15+
16+
var autoStandbyCmd = cli.Command{
17+
Name: "auto-standby",
18+
Aliases: []string{"autostandby"},
19+
Usage: "Inspect auto-standby configuration and status",
20+
Commands: []*cli.Command{
21+
&autoStandbyStatusCmd,
22+
},
23+
HideHelpCommand: true,
24+
}
25+
26+
var autoStandbyStatusCmd = cli.Command{
27+
Name: "status",
28+
Usage: "Get auto-standby status for an instance",
29+
ArgsUsage: "<instance>",
30+
Action: handleAutoStandbyStatus,
31+
HideHelpCommand: true,
32+
}
33+
34+
func handleAutoStandbyStatus(ctx context.Context, cmd *cli.Command) error {
35+
args := cmd.Args().Slice()
36+
if len(args) < 1 {
37+
return fmt.Errorf("instance ID or name required\nUsage: hypeman auto-standby status <instance>")
38+
}
39+
40+
client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)
41+
instanceID, err := ResolveInstance(ctx, &client, args[0])
42+
if err != nil {
43+
return err
44+
}
45+
46+
var opts []option.RequestOption
47+
if cmd.Root().Bool("debug") {
48+
opts = append(opts, debugMiddlewareOption)
49+
}
50+
51+
var res []byte
52+
opts = append(opts, option.WithResponseBodyInto(&res))
53+
_, err = client.Instances.AutoStandby.Status(ctx, instanceID, opts...)
54+
if err != nil {
55+
return err
56+
}
57+
58+
format := cmd.Root().String("format")
59+
transform := cmd.Root().String("transform")
60+
61+
obj := gjson.ParseBytes(res)
62+
63+
if format == "auto" {
64+
status := obj.Get("status").String()
65+
enabled := obj.Get("enabled").Bool()
66+
configured := obj.Get("configured").Bool()
67+
supported := obj.Get("supported").Bool()
68+
idleTimeout := obj.Get("idle_timeout").String()
69+
reason := obj.Get("reason").String()
70+
trackingMode := obj.Get("tracking_mode").String()
71+
connections := obj.Get("active_inbound_connections").Int()
72+
73+
if idleTimeout == "" {
74+
idleTimeout = "-"
75+
}
76+
if reason == "" {
77+
reason = "-"
78+
}
79+
80+
fmt.Printf("%-14s %s\n", "STATUS", status)
81+
fmt.Printf("%-14s %t\n", "ENABLED", enabled)
82+
fmt.Printf("%-14s %t\n", "CONFIGURED", configured)
83+
fmt.Printf("%-14s %t\n", "SUPPORTED", supported)
84+
fmt.Printf("%-14s %s\n", "IDLE TIMEOUT", idleTimeout)
85+
fmt.Printf("%-14s %s\n", "REASON", reason)
86+
fmt.Printf("%-14s %s\n", "TRACKING", trackingMode)
87+
fmt.Printf("%-14s %d\n", "CONNECTIONS", connections)
88+
89+
if idleSince := obj.Get("idle_since").String(); idleSince != "" {
90+
fmt.Printf("%-14s %s\n", "IDLE SINCE", idleSince)
91+
}
92+
if nextStandby := obj.Get("next_standby_at").String(); nextStandby != "" {
93+
fmt.Printf("%-14s %s\n", "NEXT STANDBY", nextStandby)
94+
}
95+
return nil
96+
}
97+
98+
return ShowJSON(os.Stdout, "auto-standby status", obj, format, transform)
99+
}
100+
101+
func buildAutoStandbyPolicy(cmd *cli.Command, prefix string) (hypeman.AutoStandbyPolicyParam, bool, error) {
102+
var policy hypeman.AutoStandbyPolicyParam
103+
104+
enabledFlag := prefix + "enabled"
105+
idleTimeoutFlag := prefix + "idle-timeout"
106+
ignoreDestinationPortFlag := prefix + "ignore-destination-port"
107+
ignoreSourceCIDRFlag := prefix + "ignore-source-cidr"
108+
109+
enabledSet := cmd.IsSet(enabledFlag)
110+
idleTimeout := cmd.String(idleTimeoutFlag)
111+
ignoreSourceCIDRs := cleanStringValues(cmd.StringSlice(ignoreSourceCIDRFlag))
112+
ignoreDestinationPorts, err := parseAutoStandbyPorts(cmd.StringSlice(ignoreDestinationPortFlag), ignoreDestinationPortFlag)
113+
if err != nil {
114+
return hypeman.AutoStandbyPolicyParam{}, false, err
115+
}
116+
117+
if !enabledSet && idleTimeout == "" && len(ignoreDestinationPorts) == 0 && len(ignoreSourceCIDRs) == 0 {
118+
return hypeman.AutoStandbyPolicyParam{}, false, nil
119+
}
120+
121+
if enabledSet {
122+
policy.Enabled = hypeman.Opt(cmd.Bool(enabledFlag))
123+
} else {
124+
policy.Enabled = hypeman.Opt(true)
125+
}
126+
127+
if idleTimeout != "" {
128+
policy.IdleTimeout = hypeman.Opt(idleTimeout)
129+
}
130+
if len(ignoreDestinationPorts) > 0 {
131+
policy.IgnoreDestinationPorts = ignoreDestinationPorts
132+
}
133+
if len(ignoreSourceCIDRs) > 0 {
134+
policy.IgnoreSourceCidrs = ignoreSourceCIDRs
135+
}
136+
137+
return policy, true, nil
138+
}
139+
140+
func parseAutoStandbyPorts(rawPorts []string, flagName string) ([]int64, error) {
141+
ports := make([]int64, 0, len(rawPorts))
142+
for _, rawPort := range rawPorts {
143+
value := strings.TrimSpace(rawPort)
144+
if value == "" {
145+
continue
146+
}
147+
148+
port, err := strconv.ParseInt(value, 10, 64)
149+
if err != nil {
150+
return nil, fmt.Errorf("invalid %s value %q: %w", flagName, rawPort, err)
151+
}
152+
if port < 1 || port > 65535 {
153+
return nil, fmt.Errorf("%s must be between 1 and 65535: %q", flagName, rawPort)
154+
}
155+
156+
ports = append(ports, port)
157+
}
158+
159+
return ports, nil
160+
}
161+
162+
func cleanStringValues(values []string) []string {
163+
cleaned := make([]string, 0, len(values))
164+
for _, value := range values {
165+
value = strings.TrimSpace(value)
166+
if value == "" {
167+
continue
168+
}
169+
170+
cleaned = append(cleaned, value)
171+
}
172+
173+
return cleaned
174+
}

pkg/cmd/cmd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,10 @@ func init() {
7777
&psCmd,
7878
&statsCmd,
7979
&updateCmd,
80+
&autoStandbyCmd,
8081
&inspectCmd,
8182
&logsCmd,
83+
&waitCmd,
8284
&rmCmd,
8385
&stopCmd,
8486
&startCmd,

pkg/cmd/coveragecmd_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package cmd
2+
3+
import (
4+
"testing"
5+
6+
"github.com/kernel/hypeman-go"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestParseInstanceWaitState(t *testing.T) {
12+
t.Run("accepts mixed-case state names", func(t *testing.T) {
13+
state, err := parseInstanceWaitState("rUnNiNg")
14+
require.NoError(t, err)
15+
assert.Equal(t, hypeman.InstanceWaitParamsStateRunning, state)
16+
})
17+
18+
t.Run("rejects unsupported state names", func(t *testing.T) {
19+
_, err := parseInstanceWaitState("booting")
20+
require.EqualError(t, err, "invalid state: booting (must be Created, Initializing, Running, Paused, Shutdown, Stopped, Standby, or Unknown)")
21+
})
22+
}
23+
24+
func TestParseAutoStandbyPorts(t *testing.T) {
25+
t.Run("parses valid port values", func(t *testing.T) {
26+
ports, err := parseAutoStandbyPorts([]string{"80", " 443 "}, "ignore-destination-port")
27+
require.NoError(t, err)
28+
assert.Equal(t, []int64{80, 443}, ports)
29+
})
30+
31+
t.Run("rejects out-of-range ports", func(t *testing.T) {
32+
_, err := parseAutoStandbyPorts([]string{"70000"}, "ignore-destination-port")
33+
require.EqualError(t, err, `ignore-destination-port must be between 1 and 65535: "70000"`)
34+
})
35+
}

pkg/cmd/run.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,22 @@ Examples:
113113
Name: "network-egress-mode",
114114
Usage: `Egress enforcement mode: "all" or "http_https_only"`,
115115
},
116+
&cli.BoolFlag{
117+
Name: "auto-standby-enabled",
118+
Usage: "Enable Linux-only automatic standby based on inbound TCP activity",
119+
},
120+
&cli.StringFlag{
121+
Name: "auto-standby-idle-timeout",
122+
Usage: `How long the instance must be idle before entering standby (e.g., "10m")`,
123+
},
124+
&cli.StringSliceFlag{
125+
Name: "auto-standby-ignore-destination-port",
126+
Usage: "TCP destination port that should not keep the instance awake (can be repeated)",
127+
},
128+
&cli.StringSliceFlag{
129+
Name: "auto-standby-ignore-source-cidr",
130+
Usage: "Client CIDR that should not keep the instance awake (can be repeated)",
131+
},
116132
// Boot option flags
117133
&cli.BoolFlag{
118134
Name: "skip-guest-agent",
@@ -231,6 +247,13 @@ func handleRun(ctx context.Context, cmd *cli.Command) error {
231247
}
232248
params.Credentials = credentials
233249
}
250+
autoStandbyPolicy, autoStandbySet, err := buildAutoStandbyPolicy(cmd, "auto-standby-")
251+
if err != nil {
252+
return err
253+
}
254+
if autoStandbySet {
255+
params.AutoStandby = autoStandbyPolicy
256+
}
234257

235258
// Network configuration
236259
networkEnabled := cmd.Bool("network")

pkg/cmd/snapshotcmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var snapshotCmd = cli.Command{
2020
Commands: []*cli.Command{
2121
&snapshotCreateCmd,
2222
&snapshotRestoreCmd,
23+
&snapshotScheduleCmd,
2324
&snapshotListCmd,
2425
&snapshotGetCmd,
2526
&snapshotDeleteCmd,

0 commit comments

Comments
 (0)