Skip to content

Commit 968fef8

Browse files
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
1 parent 415d0b5 commit 968fef8

File tree

10 files changed

+569
-3
lines changed

10 files changed

+569
-3
lines changed

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: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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+
return ShowJSON(os.Stdout, "auto-standby status", gjson.ParseBytes(res), format, transform)
61+
}
62+
63+
func buildAutoStandbyPolicy(cmd *cli.Command, prefix string) (hypeman.AutoStandbyPolicyParam, bool, error) {
64+
var policy hypeman.AutoStandbyPolicyParam
65+
66+
enabledFlag := prefix + "enabled"
67+
idleTimeoutFlag := prefix + "idle-timeout"
68+
ignoreDestinationPortFlag := prefix + "ignore-destination-port"
69+
ignoreSourceCIDRFlag := prefix + "ignore-source-cidr"
70+
71+
enabledSet := cmd.IsSet(enabledFlag)
72+
idleTimeout := cmd.String(idleTimeoutFlag)
73+
ignoreSourceCIDRs := cleanStringValues(cmd.StringSlice(ignoreSourceCIDRFlag))
74+
ignoreDestinationPorts, err := parseAutoStandbyPorts(cmd.StringSlice(ignoreDestinationPortFlag), ignoreDestinationPortFlag)
75+
if err != nil {
76+
return hypeman.AutoStandbyPolicyParam{}, false, err
77+
}
78+
79+
if !enabledSet && idleTimeout == "" && len(ignoreDestinationPorts) == 0 && len(ignoreSourceCIDRs) == 0 {
80+
return hypeman.AutoStandbyPolicyParam{}, false, nil
81+
}
82+
83+
if enabledSet {
84+
policy.Enabled = hypeman.Opt(cmd.Bool(enabledFlag))
85+
} else {
86+
policy.Enabled = hypeman.Opt(true)
87+
}
88+
89+
if idleTimeout != "" {
90+
policy.IdleTimeout = hypeman.Opt(idleTimeout)
91+
}
92+
if len(ignoreDestinationPorts) > 0 {
93+
policy.IgnoreDestinationPorts = ignoreDestinationPorts
94+
}
95+
if len(ignoreSourceCIDRs) > 0 {
96+
policy.IgnoreSourceCidrs = ignoreSourceCIDRs
97+
}
98+
99+
return policy, true, nil
100+
}
101+
102+
func parseAutoStandbyPorts(rawPorts []string, flagName string) ([]int64, error) {
103+
ports := make([]int64, 0, len(rawPorts))
104+
for _, rawPort := range rawPorts {
105+
value := strings.TrimSpace(rawPort)
106+
if value == "" {
107+
continue
108+
}
109+
110+
port, err := strconv.ParseInt(value, 10, 64)
111+
if err != nil {
112+
return nil, fmt.Errorf("invalid %s value %q: %w", flagName, rawPort, err)
113+
}
114+
if port < 1 || port > 65535 {
115+
return nil, fmt.Errorf("%s must be between 1 and 65535: %q", flagName, rawPort)
116+
}
117+
118+
ports = append(ports, port)
119+
}
120+
121+
return ports, nil
122+
}
123+
124+
func cleanStringValues(values []string) []string {
125+
cleaned := make([]string, 0, len(values))
126+
for _, value := range values {
127+
value = strings.TrimSpace(value)
128+
if value == "" {
129+
continue
130+
}
131+
132+
cleaned = append(cleaned, value)
133+
}
134+
135+
return cleaned
136+
}

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)