Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/google/go-containerregistry v0.20.7
github.com/gorilla/websocket v1.5.3
github.com/itchyny/json2yaml v0.1.4
github.com/kernel/hypeman-go v0.16.1-0.20260323172303-508a8c69feb3
github.com/kernel/hypeman-go v0.17.0
github.com/knadh/koanf/parsers/yaml v1.1.0
github.com/knadh/koanf/providers/env v1.1.0
github.com/knadh/koanf/providers/file v1.2.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnV
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/itchyny/json2yaml v0.1.4 h1:/pErVOXGG5iTyXHi/QKR4y3uzhLjGTEmmJIy97YT+k8=
github.com/itchyny/json2yaml v0.1.4/go.mod h1:6iudhBZdarpjLFRNj+clWLAkGft+9uCcjAZYXUH9eGI=
github.com/kernel/hypeman-go v0.16.1-0.20260323172303-508a8c69feb3 h1:g6qT9G/Qrxqqdl9gjqTnhDAHlePxV68OyQjlqXA6WX4=
github.com/kernel/hypeman-go v0.16.1-0.20260323172303-508a8c69feb3/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI=
github.com/kernel/hypeman-go v0.17.0 h1:OaGS0pFUwXYaFtlXaIQleokgNM7Z+KO0mGhL953yiMQ=
github.com/kernel/hypeman-go v0.17.0/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
Expand Down
174 changes: 174 additions & 0 deletions pkg/cmd/autostandbycmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package cmd

import (
"context"
"fmt"
"os"
"strconv"
"strings"

"github.com/kernel/hypeman-go"
"github.com/kernel/hypeman-go/option"
"github.com/tidwall/gjson"
"github.com/urfave/cli/v3"
)

var autoStandbyCmd = cli.Command{
Name: "auto-standby",
Aliases: []string{"autostandby"},
Usage: "Inspect auto-standby configuration and status",
Commands: []*cli.Command{
&autoStandbyStatusCmd,
},
HideHelpCommand: true,
}

var autoStandbyStatusCmd = cli.Command{
Name: "status",
Usage: "Get auto-standby status for an instance",
ArgsUsage: "<instance>",
Action: handleAutoStandbyStatus,
HideHelpCommand: true,
}

func handleAutoStandbyStatus(ctx context.Context, cmd *cli.Command) error {
args := cmd.Args().Slice()
if len(args) < 1 {
return fmt.Errorf("instance ID or name required\nUsage: hypeman auto-standby status <instance>")
}

client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)
instanceID, err := ResolveInstance(ctx, &client, args[0])
if err != nil {
return err
}

var opts []option.RequestOption
if cmd.Root().Bool("debug") {
opts = append(opts, debugMiddlewareOption)
}

var res []byte
opts = append(opts, option.WithResponseBodyInto(&res))
_, err = client.Instances.AutoStandby.Status(ctx, instanceID, opts...)
if err != nil {
return err
}

format := cmd.Root().String("format")
transform := cmd.Root().String("transform")

obj := gjson.ParseBytes(res)

if format == "auto" {
status := obj.Get("status").String()
enabled := obj.Get("enabled").Bool()
configured := obj.Get("configured").Bool()
supported := obj.Get("supported").Bool()
idleTimeout := obj.Get("idle_timeout").String()
reason := obj.Get("reason").String()
trackingMode := obj.Get("tracking_mode").String()
connections := obj.Get("active_inbound_connections").Int()

if idleTimeout == "" {
idleTimeout = "-"
}
if reason == "" {
reason = "-"
}

fmt.Printf("%-14s %s\n", "STATUS", status)
fmt.Printf("%-14s %t\n", "ENABLED", enabled)
fmt.Printf("%-14s %t\n", "CONFIGURED", configured)
fmt.Printf("%-14s %t\n", "SUPPORTED", supported)
fmt.Printf("%-14s %s\n", "IDLE TIMEOUT", idleTimeout)
fmt.Printf("%-14s %s\n", "REASON", reason)
fmt.Printf("%-14s %s\n", "TRACKING", trackingMode)
fmt.Printf("%-14s %d\n", "CONNECTIONS", connections)

if idleSince := obj.Get("idle_since").String(); idleSince != "" {
fmt.Printf("%-14s %s\n", "IDLE SINCE", idleSince)
}
if nextStandby := obj.Get("next_standby_at").String(); nextStandby != "" {
fmt.Printf("%-14s %s\n", "NEXT STANDBY", nextStandby)
}
return nil
}

return ShowJSON(os.Stdout, "auto-standby status", obj, format, transform)
}

func buildAutoStandbyPolicy(cmd *cli.Command, prefix string) (hypeman.AutoStandbyPolicyParam, bool, error) {
var policy hypeman.AutoStandbyPolicyParam

enabledFlag := prefix + "enabled"
idleTimeoutFlag := prefix + "idle-timeout"
ignoreDestinationPortFlag := prefix + "ignore-destination-port"
ignoreSourceCIDRFlag := prefix + "ignore-source-cidr"

enabledSet := cmd.IsSet(enabledFlag)
idleTimeout := cmd.String(idleTimeoutFlag)
ignoreSourceCIDRs := cleanStringValues(cmd.StringSlice(ignoreSourceCIDRFlag))
ignoreDestinationPorts, err := parseAutoStandbyPorts(cmd.StringSlice(ignoreDestinationPortFlag), ignoreDestinationPortFlag)
if err != nil {
return hypeman.AutoStandbyPolicyParam{}, false, err
}

if !enabledSet && idleTimeout == "" && len(ignoreDestinationPorts) == 0 && len(ignoreSourceCIDRs) == 0 {
return hypeman.AutoStandbyPolicyParam{}, false, nil
}

if enabledSet {
policy.Enabled = hypeman.Opt(cmd.Bool(enabledFlag))
} else {
policy.Enabled = hypeman.Opt(true)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update command implicitly forces auto-standby enabled

Medium Severity

The buildAutoStandbyPolicy function defaults auto-standby to enabled=true if the --enabled flag isn't explicitly set, even when other policy flags are present. While suitable for instance creation, this can silently re-enable auto-standby on instances where it was intentionally disabled if a user updates another policy setting.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit de05818. Configure here.


if idleTimeout != "" {
policy.IdleTimeout = hypeman.Opt(idleTimeout)
}
if len(ignoreDestinationPorts) > 0 {
policy.IgnoreDestinationPorts = ignoreDestinationPorts
}
if len(ignoreSourceCIDRs) > 0 {
policy.IgnoreSourceCidrs = ignoreSourceCIDRs
}

return policy, true, nil
}

func parseAutoStandbyPorts(rawPorts []string, flagName string) ([]int64, error) {
ports := make([]int64, 0, len(rawPorts))
for _, rawPort := range rawPorts {
value := strings.TrimSpace(rawPort)
if value == "" {
continue
}

port, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid %s value %q: %w", flagName, rawPort, err)
}
if port < 1 || port > 65535 {
return nil, fmt.Errorf("%s must be between 1 and 65535: %q", flagName, rawPort)
}

ports = append(ports, port)
}

return ports, nil
}

func cleanStringValues(values []string) []string {
cleaned := make([]string, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}

cleaned = append(cleaned, value)
}

return cleaned
}
2 changes: 2 additions & 0 deletions pkg/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,10 @@ func init() {
&psCmd,
&statsCmd,
&updateCmd,
&autoStandbyCmd,
&inspectCmd,
&logsCmd,
&waitCmd,
&rmCmd,
&stopCmd,
&startCmd,
Expand Down
35 changes: 35 additions & 0 deletions pkg/cmd/coveragecmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package cmd

import (
"testing"

"github.com/kernel/hypeman-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseInstanceWaitState(t *testing.T) {
t.Run("accepts mixed-case state names", func(t *testing.T) {
state, err := parseInstanceWaitState("rUnNiNg")
require.NoError(t, err)
assert.Equal(t, hypeman.InstanceWaitParamsStateRunning, state)
})

t.Run("rejects unsupported state names", func(t *testing.T) {
_, err := parseInstanceWaitState("booting")
require.EqualError(t, err, "invalid state: booting (must be Created, Initializing, Running, Paused, Shutdown, Stopped, Standby, or Unknown)")
})
}

func TestParseAutoStandbyPorts(t *testing.T) {
t.Run("parses valid port values", func(t *testing.T) {
ports, err := parseAutoStandbyPorts([]string{"80", " 443 "}, "ignore-destination-port")
require.NoError(t, err)
assert.Equal(t, []int64{80, 443}, ports)
})

t.Run("rejects out-of-range ports", func(t *testing.T) {
_, err := parseAutoStandbyPorts([]string{"70000"}, "ignore-destination-port")
require.EqualError(t, err, `ignore-destination-port must be between 1 and 65535: "70000"`)
})
}
23 changes: 23 additions & 0 deletions pkg/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,22 @@ Examples:
Name: "network-egress-mode",
Usage: `Egress enforcement mode: "all" or "http_https_only"`,
},
&cli.BoolFlag{
Name: "auto-standby-enabled",
Usage: "Enable Linux-only automatic standby based on inbound TCP activity",
},
&cli.StringFlag{
Name: "auto-standby-idle-timeout",
Usage: `How long the instance must be idle before entering standby (e.g., "10m")`,
},
&cli.StringSliceFlag{
Name: "auto-standby-ignore-destination-port",
Usage: "TCP destination port that should not keep the instance awake (can be repeated)",
},
&cli.StringSliceFlag{
Name: "auto-standby-ignore-source-cidr",
Usage: "Client CIDR that should not keep the instance awake (can be repeated)",
},
// Boot option flags
&cli.BoolFlag{
Name: "skip-guest-agent",
Expand Down Expand Up @@ -231,6 +247,13 @@ func handleRun(ctx context.Context, cmd *cli.Command) error {
}
params.Credentials = credentials
}
autoStandbyPolicy, autoStandbySet, err := buildAutoStandbyPolicy(cmd, "auto-standby-")
if err != nil {
return err
}
if autoStandbySet {
params.AutoStandby = autoStandbyPolicy
}

// Network configuration
networkEnabled := cmd.Bool("network")
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/snapshotcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ var snapshotCmd = cli.Command{
Commands: []*cli.Command{
&snapshotCreateCmd,
&snapshotRestoreCmd,
&snapshotScheduleCmd,
&snapshotListCmd,
&snapshotGetCmd,
&snapshotDeleteCmd,
Expand Down
Loading
Loading