Skip to content

Commit 476819b

Browse files
feat: Add Linux auto-standby controller and E2E coverage
1 parent 98416e2 commit 476819b

6 files changed

Lines changed: 268 additions & 5 deletions

File tree

.stats.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
configured_endpoints: 51
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-a1912a634af4109683e626e337c1a1f1ddf488861bd16692f4bf3a4309477d1b.yml
3-
openapi_spec_hash: 10ce32db64b5ec6815d861f18415be48
4-
config_hash: b3559c96c301e1fcdea2d7e9aba1e5e1
1+
configured_endpoints: 52
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-52bc64e89c8406b774158cdb7fdb239dd4c39e6bace06b32e5224b82462f9ffe.yml
3+
openapi_spec_hash: 647ddb91aa6aca7034f2015071c30ce6
4+
config_hash: d81afc6f4fabf65fc9291db9ddd79f87

api.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,16 @@ Methods:
3333

3434
Params Types:
3535

36+
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#AutoStandbyPolicyParam">AutoStandbyPolicyParam</a>
3637
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#SetSnapshotScheduleRequestParam">SetSnapshotScheduleRequestParam</a>
3738
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#SnapshotPolicyParam">SnapshotPolicyParam</a>
3839
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#SnapshotScheduleRetentionParam">SnapshotScheduleRetentionParam</a>
3940
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#VolumeMountParam">VolumeMountParam</a>
4041

4142
Response Types:
4243

44+
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#AutoStandbyPolicy">AutoStandbyPolicy</a>
45+
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#AutoStandbyStatus">AutoStandbyStatus</a>
4346
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Instance">Instance</a>
4447
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceStats">InstanceStats</a>
4548
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#PathInfo">PathInfo</a>
@@ -66,6 +69,12 @@ Methods:
6669
- <code title="post /instances/{id}/stop">client.Instances.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceService.Stop">Stop</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (\*<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Instance">Instance</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
6770
- <code title="get /instances/{id}/wait">client.Instances.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceService.Wait">Wait</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, query <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceWaitParams">InstanceWaitParams</a>) (\*<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#WaitForStateResponse">WaitForStateResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
6871

72+
## AutoStandby
73+
74+
Methods:
75+
76+
- <code title="get /instances/{id}/auto-standby/status">client.Instances.AutoStandby.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceAutoStandbyService.Status">Status</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (\*<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#AutoStandbyStatus">AutoStandbyStatus</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
77+
6978
## Volumes
7079

7180
Methods:

instance.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
// the [NewInstanceService] method instead.
3131
type InstanceService struct {
3232
Options []option.RequestOption
33+
AutoStandby InstanceAutoStandbyService
3334
Volumes InstanceVolumeService
3435
Snapshots InstanceSnapshotService
3536
SnapshotSchedule InstanceSnapshotScheduleService
@@ -41,6 +42,7 @@ type InstanceService struct {
4142
func NewInstanceService(opts ...option.RequestOption) (r InstanceService) {
4243
r = InstanceService{}
4344
r.Options = opts
45+
r.AutoStandby = NewInstanceAutoStandbyService(opts...)
4446
r.Volumes = NewInstanceVolumeService(opts...)
4547
r.Snapshots = NewInstanceSnapshotService(opts...)
4648
r.SnapshotSchedule = NewInstanceSnapshotScheduleService(opts...)
@@ -229,6 +231,154 @@ func (r *InstanceService) Wait(ctx context.Context, id string, query InstanceWai
229231
return res, err
230232
}
231233

234+
// Linux-only automatic standby policy based on active inbound TCP connections
235+
// observed from the host conntrack table.
236+
type AutoStandbyPolicy struct {
237+
// Whether automatic standby is enabled for this instance.
238+
Enabled bool `json:"enabled"`
239+
// How long the instance must have zero qualifying inbound TCP connections before
240+
// Hypeman places it into standby.
241+
IdleTimeout string `json:"idle_timeout"`
242+
// Optional destination TCP ports that should not keep the instance awake.
243+
IgnoreDestinationPorts []int64 `json:"ignore_destination_ports"`
244+
// Optional client CIDRs that should not keep the instance awake.
245+
IgnoreSourceCidrs []string `json:"ignore_source_cidrs"`
246+
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
247+
JSON struct {
248+
Enabled respjson.Field
249+
IdleTimeout respjson.Field
250+
IgnoreDestinationPorts respjson.Field
251+
IgnoreSourceCidrs respjson.Field
252+
ExtraFields map[string]respjson.Field
253+
raw string
254+
} `json:"-"`
255+
}
256+
257+
// Returns the unmodified JSON received from the API
258+
func (r AutoStandbyPolicy) RawJSON() string { return r.JSON.raw }
259+
func (r *AutoStandbyPolicy) UnmarshalJSON(data []byte) error {
260+
return apijson.UnmarshalRoot(data, r)
261+
}
262+
263+
// ToParam converts this AutoStandbyPolicy to a AutoStandbyPolicyParam.
264+
//
265+
// Warning: the fields of the param type will not be present. ToParam should only
266+
// be used at the last possible moment before sending a request. Test for this with
267+
// AutoStandbyPolicyParam.Overrides()
268+
func (r AutoStandbyPolicy) ToParam() AutoStandbyPolicyParam {
269+
return param.Override[AutoStandbyPolicyParam](json.RawMessage(r.RawJSON()))
270+
}
271+
272+
// Linux-only automatic standby policy based on active inbound TCP connections
273+
// observed from the host conntrack table.
274+
type AutoStandbyPolicyParam struct {
275+
// Whether automatic standby is enabled for this instance.
276+
Enabled param.Opt[bool] `json:"enabled,omitzero"`
277+
// How long the instance must have zero qualifying inbound TCP connections before
278+
// Hypeman places it into standby.
279+
IdleTimeout param.Opt[string] `json:"idle_timeout,omitzero"`
280+
// Optional destination TCP ports that should not keep the instance awake.
281+
IgnoreDestinationPorts []int64 `json:"ignore_destination_ports,omitzero"`
282+
// Optional client CIDRs that should not keep the instance awake.
283+
IgnoreSourceCidrs []string `json:"ignore_source_cidrs,omitzero"`
284+
paramObj
285+
}
286+
287+
func (r AutoStandbyPolicyParam) MarshalJSON() (data []byte, err error) {
288+
type shadow AutoStandbyPolicyParam
289+
return param.MarshalObject(r, (*shadow)(&r))
290+
}
291+
func (r *AutoStandbyPolicyParam) UnmarshalJSON(data []byte) error {
292+
return apijson.UnmarshalRoot(data, r)
293+
}
294+
295+
type AutoStandbyStatus struct {
296+
// Number of currently tracked qualifying inbound TCP connections.
297+
ActiveInboundConnections int64 `json:"active_inbound_connections" api:"required"`
298+
// Whether the instance has any auto-standby policy configured.
299+
Configured bool `json:"configured" api:"required"`
300+
// Whether the instance is currently eligible to enter standby.
301+
Eligible bool `json:"eligible" api:"required"`
302+
// Whether the configured auto-standby policy is enabled.
303+
Enabled bool `json:"enabled" api:"required"`
304+
// Any of "unsupported_platform", "policy_missing", "policy_disabled",
305+
// "instance_not_running", "network_disabled", "missing_ip", "has_vgpu",
306+
// "active_inbound_connections", "idle_timeout_not_elapsed", "observer_error",
307+
// "ready_for_standby".
308+
Reason AutoStandbyStatusReason `json:"reason" api:"required"`
309+
// Any of "unsupported", "disabled", "ineligible", "active", "idle_countdown",
310+
// "ready_for_standby", "standby_requested", "error".
311+
Status AutoStandbyStatusStatus `json:"status" api:"required"`
312+
// Whether the current host platform supports auto-standby diagnostics.
313+
Supported bool `json:"supported" api:"required"`
314+
// Diagnostic identifier for the runtime tracking mode in use.
315+
TrackingMode string `json:"tracking_mode" api:"required"`
316+
// Remaining time before the controller attempts standby, when applicable.
317+
CountdownRemaining string `json:"countdown_remaining" api:"nullable"`
318+
// When the controller most recently observed the instance become idle.
319+
IdleSince time.Time `json:"idle_since" api:"nullable" format:"date-time"`
320+
// Configured idle timeout from the auto-standby policy.
321+
IdleTimeout string `json:"idle_timeout" api:"nullable"`
322+
// Timestamp of the most recent qualifying inbound TCP activity the controller
323+
// observed.
324+
LastInboundActivityAt time.Time `json:"last_inbound_activity_at" api:"nullable" format:"date-time"`
325+
// When the controller expects to attempt standby next, if a countdown is active.
326+
NextStandbyAt time.Time `json:"next_standby_at" api:"nullable" format:"date-time"`
327+
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
328+
JSON struct {
329+
ActiveInboundConnections respjson.Field
330+
Configured respjson.Field
331+
Eligible respjson.Field
332+
Enabled respjson.Field
333+
Reason respjson.Field
334+
Status respjson.Field
335+
Supported respjson.Field
336+
TrackingMode respjson.Field
337+
CountdownRemaining respjson.Field
338+
IdleSince respjson.Field
339+
IdleTimeout respjson.Field
340+
LastInboundActivityAt respjson.Field
341+
NextStandbyAt respjson.Field
342+
ExtraFields map[string]respjson.Field
343+
raw string
344+
} `json:"-"`
345+
}
346+
347+
// Returns the unmodified JSON received from the API
348+
func (r AutoStandbyStatus) RawJSON() string { return r.JSON.raw }
349+
func (r *AutoStandbyStatus) UnmarshalJSON(data []byte) error {
350+
return apijson.UnmarshalRoot(data, r)
351+
}
352+
353+
type AutoStandbyStatusReason string
354+
355+
const (
356+
AutoStandbyStatusReasonUnsupportedPlatform AutoStandbyStatusReason = "unsupported_platform"
357+
AutoStandbyStatusReasonPolicyMissing AutoStandbyStatusReason = "policy_missing"
358+
AutoStandbyStatusReasonPolicyDisabled AutoStandbyStatusReason = "policy_disabled"
359+
AutoStandbyStatusReasonInstanceNotRunning AutoStandbyStatusReason = "instance_not_running"
360+
AutoStandbyStatusReasonNetworkDisabled AutoStandbyStatusReason = "network_disabled"
361+
AutoStandbyStatusReasonMissingIP AutoStandbyStatusReason = "missing_ip"
362+
AutoStandbyStatusReasonHasVgpu AutoStandbyStatusReason = "has_vgpu"
363+
AutoStandbyStatusReasonActiveInboundConnections AutoStandbyStatusReason = "active_inbound_connections"
364+
AutoStandbyStatusReasonIdleTimeoutNotElapsed AutoStandbyStatusReason = "idle_timeout_not_elapsed"
365+
AutoStandbyStatusReasonObserverError AutoStandbyStatusReason = "observer_error"
366+
AutoStandbyStatusReasonReadyForStandby AutoStandbyStatusReason = "ready_for_standby"
367+
)
368+
369+
type AutoStandbyStatusStatus string
370+
371+
const (
372+
AutoStandbyStatusStatusUnsupported AutoStandbyStatusStatus = "unsupported"
373+
AutoStandbyStatusStatusDisabled AutoStandbyStatusStatus = "disabled"
374+
AutoStandbyStatusStatusIneligible AutoStandbyStatusStatus = "ineligible"
375+
AutoStandbyStatusStatusActive AutoStandbyStatusStatus = "active"
376+
AutoStandbyStatusStatusIdleCountdown AutoStandbyStatusStatus = "idle_countdown"
377+
AutoStandbyStatusStatusReadyForStandby AutoStandbyStatusStatus = "ready_for_standby"
378+
AutoStandbyStatusStatusStandbyRequested AutoStandbyStatusStatus = "standby_requested"
379+
AutoStandbyStatusStatusError AutoStandbyStatusStatus = "error"
380+
)
381+
232382
type Instance struct {
233383
// Auto-generated unique identifier (CUID2 format)
234384
ID string `json:"id" api:"required"`
@@ -252,6 +402,9 @@ type Instance struct {
252402
// Any of "Created", "Initializing", "Running", "Paused", "Shutdown", "Stopped",
253403
// "Standby", "Unknown".
254404
State InstanceState `json:"state" api:"required"`
405+
// Linux-only automatic standby policy based on active inbound TCP connections
406+
// observed from the host conntrack table.
407+
AutoStandby AutoStandbyPolicy `json:"auto_standby"`
255408
// Disk I/O rate limit (human-readable, e.g., "100MB/s")
256409
DiskIoBps string `json:"disk_io_bps"`
257410
// Environment variables
@@ -297,6 +450,7 @@ type Instance struct {
297450
Image respjson.Field
298451
Name respjson.Field
299452
State respjson.Field
453+
AutoStandby respjson.Field
300454
DiskIoBps respjson.Field
301455
Env respjson.Field
302456
ExitCode respjson.Field
@@ -784,6 +938,9 @@ type InstanceNewParams struct {
784938
SkipKernelHeaders param.Opt[bool] `json:"skip_kernel_headers,omitzero"`
785939
// Number of virtual CPUs
786940
Vcpus param.Opt[int64] `json:"vcpus,omitzero"`
941+
// Linux-only automatic standby policy based on active inbound TCP connections
942+
// observed from the host conntrack table.
943+
AutoStandby AutoStandbyPolicyParam `json:"auto_standby,omitzero"`
787944
// Override image CMD (like docker run <image> <command>). Omit to use image
788945
// default.
789946
Cmd []string `json:"cmd,omitzero"`
@@ -992,6 +1149,9 @@ func init() {
9921149
}
9931150

9941151
type InstanceUpdateParams struct {
1152+
// Linux-only automatic standby policy based on active inbound TCP connections
1153+
// observed from the host conntrack table.
1154+
AutoStandby AutoStandbyPolicyParam `json:"auto_standby,omitzero"`
9951155
// Environment variables to update (merged with existing). Only keys referenced by
9961156
// the instance's existing credential `source.env` bindings are accepted. Use this
9971157
// to rotate real credential values without restarting the VM.

instance_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ func TestInstanceNewWithOptionalParams(t *testing.T) {
3030
_, err := client.Instances.New(context.TODO(), hypeman.InstanceNewParams{
3131
Image: "docker.io/library/alpine:latest",
3232
Name: "my-workload-1",
33-
Cmd: []string{"echo", "hello"},
33+
AutoStandby: hypeman.AutoStandbyPolicyParam{
34+
Enabled: hypeman.Bool(true),
35+
IdleTimeout: hypeman.String("5m"),
36+
IgnoreDestinationPorts: []int64{22, 9000},
37+
IgnoreSourceCidrs: []string{"10.0.0.0/8", "192.168.0.0/16"},
38+
},
39+
Cmd: []string{"echo", "hello"},
3440
Credentials: map[string]hypeman.InstanceNewParamsCredential{
3541
"OUTBOUND_OPENAI_KEY": {
3642
Inject: []hypeman.InstanceNewParamsCredentialInject{{
@@ -118,6 +124,12 @@ func TestInstanceUpdateWithOptionalParams(t *testing.T) {
118124
context.TODO(),
119125
"id",
120126
hypeman.InstanceUpdateParams{
127+
AutoStandby: hypeman.AutoStandbyPolicyParam{
128+
Enabled: hypeman.Bool(true),
129+
IdleTimeout: hypeman.String("5m"),
130+
IgnoreDestinationPorts: []int64{22, 9000},
131+
IgnoreSourceCidrs: []string{"10.0.0.0/8", "192.168.0.0/16"},
132+
},
121133
Env: map[string]string{
122134
"OUTBOUND_OPENAI_KEY": "new-rotated-key-456",
123135
},

instanceautostandby.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2+
3+
package hypeman
4+
5+
import (
6+
"context"
7+
"errors"
8+
"fmt"
9+
"net/http"
10+
"slices"
11+
12+
"github.com/kernel/hypeman-go/internal/requestconfig"
13+
"github.com/kernel/hypeman-go/option"
14+
)
15+
16+
// InstanceAutoStandbyService contains methods and other services that help with
17+
// interacting with the hypeman API.
18+
//
19+
// Note, unlike clients, this service does not read variables from the environment
20+
// automatically. You should not instantiate this service directly, and instead use
21+
// the [NewInstanceAutoStandbyService] method instead.
22+
type InstanceAutoStandbyService struct {
23+
Options []option.RequestOption
24+
}
25+
26+
// NewInstanceAutoStandbyService generates a new service that applies the given
27+
// options to each request. These options are applied after the parent client's
28+
// options (if there is one), and before any request-specific options.
29+
func NewInstanceAutoStandbyService(opts ...option.RequestOption) (r InstanceAutoStandbyService) {
30+
r = InstanceAutoStandbyService{}
31+
r.Options = opts
32+
return
33+
}
34+
35+
// Get auto-standby diagnostic status
36+
func (r *InstanceAutoStandbyService) Status(ctx context.Context, id string, opts ...option.RequestOption) (res *AutoStandbyStatus, err error) {
37+
opts = slices.Concat(r.Options, opts)
38+
if id == "" {
39+
err = errors.New("missing required id parameter")
40+
return nil, err
41+
}
42+
path := fmt.Sprintf("instances/%s/auto-standby/status", id)
43+
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
44+
return res, err
45+
}

instanceautostandby_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2+
3+
package hypeman_test
4+
5+
import (
6+
"context"
7+
"errors"
8+
"os"
9+
"testing"
10+
11+
"github.com/kernel/hypeman-go"
12+
"github.com/kernel/hypeman-go/internal/testutil"
13+
"github.com/kernel/hypeman-go/option"
14+
)
15+
16+
func TestInstanceAutoStandbyStatus(t *testing.T) {
17+
t.Skip("Mock server tests are disabled")
18+
baseURL := "http://localhost:4010"
19+
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
20+
baseURL = envURL
21+
}
22+
if !testutil.CheckTestServer(t, baseURL) {
23+
return
24+
}
25+
client := hypeman.NewClient(
26+
option.WithBaseURL(baseURL),
27+
option.WithAPIKey("My API Key"),
28+
)
29+
_, err := client.Instances.AutoStandby.Status(context.TODO(), "id")
30+
if err != nil {
31+
var apierr *hypeman.Error
32+
if errors.As(err, &apierr) {
33+
t.Log(string(apierr.DumpRequest(true)))
34+
}
35+
t.Fatalf("err should be nil: %s", err.Error())
36+
}
37+
}

0 commit comments

Comments
 (0)