Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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