From e6a6702ba1fcf355cf64a60fd016393e5474e9bc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:59:49 +0000 Subject: [PATCH 01/16] chore(internal): codegen related update --- internal/requestconfig/requestconfig.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/internal/requestconfig/requestconfig.go b/internal/requestconfig/requestconfig.go index 595c464..7080257 100644 --- a/internal/requestconfig/requestconfig.go +++ b/internal/requestconfig/requestconfig.go @@ -355,11 +355,9 @@ func (b *bodyWithTimeout) Close() error { } func retryDelay(res *http.Response, retryCount int) time.Duration { - // If the API asks us to wait a certain amount of time (and it's a reasonable amount), - // just do what it says. - - if retryAfterDelay, ok := parseRetryAfterHeader(res); ok && 0 <= retryAfterDelay && retryAfterDelay < time.Minute { - return retryAfterDelay + // If the backend tells us to wait a certain amount of time, use that value + if retryAfterDelay, ok := parseRetryAfterHeader(res); ok { + return max(0, retryAfterDelay) } maxDelay := 8 * time.Second From c4a0fbb34ee2b325229f6a02ae9cc3c57346788d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 03:27:35 +0000 Subject: [PATCH 02/16] feat: Snapshot --- .stats.yml | 8 +- api.md | 25 +++++ client.go | 2 + instance.go | 6 +- instancesnapshot.go | 130 ++++++++++++++++++++++++ instancesnapshot_test.go | 75 ++++++++++++++ snapshot.go | 214 +++++++++++++++++++++++++++++++++++++++ snapshot_test.go | 118 +++++++++++++++++++++ 8 files changed, 572 insertions(+), 6 deletions(-) create mode 100644 instancesnapshot.go create mode 100644 instancesnapshot_test.go create mode 100644 snapshot.go create mode 100644 snapshot_test.go diff --git a/.stats.yml b/.stats.yml index 511176d..e1a8f80 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 39 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-68bd472fc1704fc7ff7ed01b4213dda068a0865d42693d47ecef90651526febb.yml -openapi_spec_hash: 18ec995954b05d8dfb1e9e3254cf579a -config_hash: 368f8c9248e41f12124ab83b6f5b2eec +configured_endpoints: 45 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-ef3a9825726e97be4c61ca3886bc1ddc04088aa179c1fbf25c73a515df604aa5.yml +openapi_spec_hash: 349c7b6c17298c8dbdfd0750badf5608 +config_hash: 1fbc2955725b2b29a941f009ff2a2ce9 diff --git a/api.md b/api.md index 3fa23c1..38fff3e 100644 --- a/api.md +++ b/api.md @@ -56,6 +56,31 @@ Methods: - client.Instances.Volumes.Attach(ctx context.Context, volumeID string, params hypeman.InstanceVolumeAttachParams) (\*hypeman.Instance, error) - client.Instances.Volumes.Detach(ctx context.Context, volumeID string, body hypeman.InstanceVolumeDetachParams) (\*hypeman.Instance, error) +## Snapshots + +Methods: + +- client.Instances.Snapshots.New(ctx context.Context, id string, body hypeman.InstanceSnapshotNewParams) (\*hypeman.Snapshot, error) +- client.Instances.Snapshots.Restore(ctx context.Context, snapshotID string, params hypeman.InstanceSnapshotRestoreParams) (\*hypeman.Instance, error) + +# Snapshots + +Params Types: + +- hypeman.SnapshotKind + +Response Types: + +- hypeman.Snapshot +- hypeman.SnapshotKind + +Methods: + +- client.Snapshots.List(ctx context.Context, query hypeman.SnapshotListParams) (\*[]hypeman.Snapshot, error) +- client.Snapshots.Delete(ctx context.Context, snapshotID string) error +- client.Snapshots.Fork(ctx context.Context, snapshotID string, body hypeman.SnapshotForkParams) (\*hypeman.Instance, error) +- client.Snapshots.Get(ctx context.Context, snapshotID string) (\*hypeman.Snapshot, error) + # Volumes Response Types: diff --git a/client.go b/client.go index eeef91e..6105b35 100644 --- a/client.go +++ b/client.go @@ -20,6 +20,7 @@ type Client struct { Health HealthService Images ImageService Instances InstanceService + Snapshots SnapshotService Volumes VolumeService Devices DeviceService Ingresses IngressService @@ -52,6 +53,7 @@ func NewClient(opts ...option.RequestOption) (r Client) { r.Health = NewHealthService(opts...) r.Images = NewImageService(opts...) r.Instances = NewInstanceService(opts...) + r.Snapshots = NewSnapshotService(opts...) r.Volumes = NewVolumeService(opts...) r.Devices = NewDeviceService(opts...) r.Ingresses = NewIngressService(opts...) diff --git a/instance.go b/instance.go index cfe3b54..c41408b 100644 --- a/instance.go +++ b/instance.go @@ -28,8 +28,9 @@ import ( // automatically. You should not instantiate this service directly, and instead use // the [NewInstanceService] method instead. type InstanceService struct { - Options []option.RequestOption - Volumes InstanceVolumeService + Options []option.RequestOption + Volumes InstanceVolumeService + Snapshots InstanceSnapshotService } // NewInstanceService generates a new service that applies the given options to @@ -39,6 +40,7 @@ func NewInstanceService(opts ...option.RequestOption) (r InstanceService) { r = InstanceService{} r.Options = opts r.Volumes = NewInstanceVolumeService(opts...) + r.Snapshots = NewInstanceSnapshotService(opts...) return } diff --git a/instancesnapshot.go b/instancesnapshot.go new file mode 100644 index 0000000..51a8648 --- /dev/null +++ b/instancesnapshot.go @@ -0,0 +1,130 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package hypeman + +import ( + "context" + "errors" + "fmt" + "net/http" + "slices" + + "github.com/kernel/hypeman-go/internal/apijson" + "github.com/kernel/hypeman-go/internal/requestconfig" + "github.com/kernel/hypeman-go/option" + "github.com/kernel/hypeman-go/packages/param" +) + +// InstanceSnapshotService contains methods and other services that help with +// interacting with the hypeman API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewInstanceSnapshotService] method instead. +type InstanceSnapshotService struct { + Options []option.RequestOption +} + +// NewInstanceSnapshotService generates a new service that applies the given +// options to each request. These options are applied after the parent client's +// options (if there is one), and before any request-specific options. +func NewInstanceSnapshotService(opts ...option.RequestOption) (r InstanceSnapshotService) { + r = InstanceSnapshotService{} + r.Options = opts + return +} + +// Create a snapshot for an instance +func (r *InstanceSnapshotService) New(ctx context.Context, id string, body InstanceSnapshotNewParams, opts ...option.RequestOption) (res *Snapshot, err error) { + opts = slices.Concat(r.Options, opts) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("instances/%s/snapshots", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// Restore an instance from a snapshot in-place +func (r *InstanceSnapshotService) Restore(ctx context.Context, snapshotID string, params InstanceSnapshotRestoreParams, opts ...option.RequestOption) (res *Instance, err error) { + opts = slices.Concat(r.Options, opts) + if params.ID == "" { + err = errors.New("missing required id parameter") + return + } + if snapshotID == "" { + err = errors.New("missing required snapshotId parameter") + return + } + path := fmt.Sprintf("instances/%s/snapshots/%s/restore", params.ID, snapshotID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) + return +} + +type InstanceSnapshotNewParams struct { + // Snapshot capture kind + // + // Any of "Standby", "Stopped". + Kind SnapshotKind `json:"kind,omitzero" api:"required"` + // Optional snapshot name (lowercase letters, digits, and dashes only; cannot start + // or end with a dash) + Name param.Opt[string] `json:"name,omitzero"` + paramObj +} + +func (r InstanceSnapshotNewParams) MarshalJSON() (data []byte, err error) { + type shadow InstanceSnapshotNewParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *InstanceSnapshotNewParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type InstanceSnapshotRestoreParams struct { + ID string `path:"id" api:"required" json:"-"` + // Optional hypervisor override. Allowed only when restoring from a Stopped + // snapshot. Standby snapshots must restore with their original hypervisor. + // + // Any of "cloud-hypervisor", "firecracker", "qemu", "vz". + TargetHypervisor InstanceSnapshotRestoreParamsTargetHypervisor `json:"target_hypervisor,omitzero"` + // Optional final state after restore. Defaults by snapshot kind: + // + // - Standby snapshot defaults to Running + // - Stopped snapshot defaults to Stopped + // + // Any of "Stopped", "Standby", "Running". + TargetState InstanceSnapshotRestoreParamsTargetState `json:"target_state,omitzero"` + paramObj +} + +func (r InstanceSnapshotRestoreParams) MarshalJSON() (data []byte, err error) { + type shadow InstanceSnapshotRestoreParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *InstanceSnapshotRestoreParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Optional hypervisor override. Allowed only when restoring from a Stopped +// snapshot. Standby snapshots must restore with their original hypervisor. +type InstanceSnapshotRestoreParamsTargetHypervisor string + +const ( + InstanceSnapshotRestoreParamsTargetHypervisorCloudHypervisor InstanceSnapshotRestoreParamsTargetHypervisor = "cloud-hypervisor" + InstanceSnapshotRestoreParamsTargetHypervisorFirecracker InstanceSnapshotRestoreParamsTargetHypervisor = "firecracker" + InstanceSnapshotRestoreParamsTargetHypervisorQemu InstanceSnapshotRestoreParamsTargetHypervisor = "qemu" + InstanceSnapshotRestoreParamsTargetHypervisorVz InstanceSnapshotRestoreParamsTargetHypervisor = "vz" +) + +// Optional final state after restore. Defaults by snapshot kind: +// +// - Standby snapshot defaults to Running +// - Stopped snapshot defaults to Stopped +type InstanceSnapshotRestoreParamsTargetState string + +const ( + InstanceSnapshotRestoreParamsTargetStateStopped InstanceSnapshotRestoreParamsTargetState = "Stopped" + InstanceSnapshotRestoreParamsTargetStateStandby InstanceSnapshotRestoreParamsTargetState = "Standby" + InstanceSnapshotRestoreParamsTargetStateRunning InstanceSnapshotRestoreParamsTargetState = "Running" +) diff --git a/instancesnapshot_test.go b/instancesnapshot_test.go new file mode 100644 index 0000000..f41e0d5 --- /dev/null +++ b/instancesnapshot_test.go @@ -0,0 +1,75 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package hypeman_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/kernel/hypeman-go" + "github.com/kernel/hypeman-go/internal/testutil" + "github.com/kernel/hypeman-go/option" +) + +func TestInstanceSnapshotNewWithOptionalParams(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := hypeman.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Instances.Snapshots.New( + context.TODO(), + "id", + hypeman.InstanceSnapshotNewParams{ + Kind: hypeman.SnapshotKindStandby, + Name: hypeman.String("pre-upgrade"), + }, + ) + if err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestInstanceSnapshotRestoreWithOptionalParams(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := hypeman.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Instances.Snapshots.Restore( + context.TODO(), + "snapshotId", + hypeman.InstanceSnapshotRestoreParams{ + ID: "id", + TargetHypervisor: hypeman.InstanceSnapshotRestoreParamsTargetHypervisorQemu, + TargetState: hypeman.InstanceSnapshotRestoreParamsTargetStateRunning, + }, + ) + if err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/snapshot.go b/snapshot.go new file mode 100644 index 0000000..42e06ea --- /dev/null +++ b/snapshot.go @@ -0,0 +1,214 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package hypeman + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "slices" + "time" + + "github.com/kernel/hypeman-go/internal/apijson" + "github.com/kernel/hypeman-go/internal/apiquery" + "github.com/kernel/hypeman-go/internal/requestconfig" + "github.com/kernel/hypeman-go/option" + "github.com/kernel/hypeman-go/packages/param" + "github.com/kernel/hypeman-go/packages/respjson" +) + +// SnapshotService contains methods and other services that help with interacting +// with the hypeman API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewSnapshotService] method instead. +type SnapshotService struct { + Options []option.RequestOption +} + +// NewSnapshotService generates a new service that applies the given options to +// each request. These options are applied after the parent client's options (if +// there is one), and before any request-specific options. +func NewSnapshotService(opts ...option.RequestOption) (r SnapshotService) { + r = SnapshotService{} + r.Options = opts + return +} + +// List snapshots +func (r *SnapshotService) List(ctx context.Context, query SnapshotListParams, opts ...option.RequestOption) (res *[]Snapshot, err error) { + opts = slices.Concat(r.Options, opts) + path := "snapshots" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return +} + +// Delete a snapshot +func (r *SnapshotService) Delete(ctx context.Context, snapshotID string, opts ...option.RequestOption) (err error) { + opts = slices.Concat(r.Options, opts) + opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) + if snapshotID == "" { + err = errors.New("missing required snapshotId parameter") + return + } + path := fmt.Sprintf("snapshots/%s", snapshotID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) + return +} + +// Fork a new instance from a snapshot +func (r *SnapshotService) Fork(ctx context.Context, snapshotID string, body SnapshotForkParams, opts ...option.RequestOption) (res *Instance, err error) { + opts = slices.Concat(r.Options, opts) + if snapshotID == "" { + err = errors.New("missing required snapshotId parameter") + return + } + path := fmt.Sprintf("snapshots/%s/fork", snapshotID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// Get snapshot details +func (r *SnapshotService) Get(ctx context.Context, snapshotID string, opts ...option.RequestOption) (res *Snapshot, err error) { + opts = slices.Concat(r.Options, opts) + if snapshotID == "" { + err = errors.New("missing required snapshotId parameter") + return + } + path := fmt.Sprintf("snapshots/%s", snapshotID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +type Snapshot struct { + // Auto-generated unique snapshot identifier + ID string `json:"id" api:"required"` + // Snapshot creation timestamp + CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"` + // Snapshot capture kind + // + // Any of "Standby", "Stopped". + Kind SnapshotKind `json:"kind" api:"required"` + // Total payload size in bytes + SizeBytes int64 `json:"size_bytes" api:"required"` + // Source instance hypervisor at snapshot creation time + // + // Any of "cloud-hypervisor", "firecracker", "qemu", "vz". + SourceHypervisor SnapshotSourceHypervisor `json:"source_hypervisor" api:"required"` + // Source instance ID at snapshot creation time + SourceInstanceID string `json:"source_instance_id" api:"required"` + // Source instance name at snapshot creation time + SourceInstanceName string `json:"source_instance_name" api:"required"` + // Optional human-readable snapshot name (unique per source instance) + Name string `json:"name" api:"nullable"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + CreatedAt respjson.Field + Kind respjson.Field + SizeBytes respjson.Field + SourceHypervisor respjson.Field + SourceInstanceID respjson.Field + SourceInstanceName respjson.Field + Name respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r Snapshot) RawJSON() string { return r.JSON.raw } +func (r *Snapshot) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Source instance hypervisor at snapshot creation time +type SnapshotSourceHypervisor string + +const ( + SnapshotSourceHypervisorCloudHypervisor SnapshotSourceHypervisor = "cloud-hypervisor" + SnapshotSourceHypervisorFirecracker SnapshotSourceHypervisor = "firecracker" + SnapshotSourceHypervisorQemu SnapshotSourceHypervisor = "qemu" + SnapshotSourceHypervisorVz SnapshotSourceHypervisor = "vz" +) + +// Snapshot capture kind +type SnapshotKind string + +const ( + SnapshotKindStandby SnapshotKind = "Standby" + SnapshotKindStopped SnapshotKind = "Stopped" +) + +type SnapshotListParams struct { + // Filter snapshots by snapshot name + Name param.Opt[string] `query:"name,omitzero" json:"-"` + // Filter snapshots by source instance ID + SourceInstanceID param.Opt[string] `query:"source_instance_id,omitzero" json:"-"` + // Filter snapshots by kind + // + // Any of "Standby", "Stopped". + Kind SnapshotKind `query:"kind,omitzero" json:"-"` + paramObj +} + +// URLQuery serializes [SnapshotListParams]'s query parameters as `url.Values`. +func (r SnapshotListParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type SnapshotForkParams struct { + // Name for the new instance (lowercase letters, digits, and dashes only; cannot + // start or end with a dash) + Name string `json:"name" api:"required"` + // Optional hypervisor override. Allowed only when forking from a Stopped snapshot. + // Standby snapshots must fork with their original hypervisor. + // + // Any of "cloud-hypervisor", "firecracker", "qemu", "vz". + TargetHypervisor SnapshotForkParamsTargetHypervisor `json:"target_hypervisor,omitzero"` + // Optional final state for the forked instance. Defaults by snapshot kind: + // + // - Standby snapshot defaults to Running + // - Stopped snapshot defaults to Stopped + // + // Any of "Stopped", "Standby", "Running". + TargetState SnapshotForkParamsTargetState `json:"target_state,omitzero"` + paramObj +} + +func (r SnapshotForkParams) MarshalJSON() (data []byte, err error) { + type shadow SnapshotForkParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *SnapshotForkParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Optional hypervisor override. Allowed only when forking from a Stopped snapshot. +// Standby snapshots must fork with their original hypervisor. +type SnapshotForkParamsTargetHypervisor string + +const ( + SnapshotForkParamsTargetHypervisorCloudHypervisor SnapshotForkParamsTargetHypervisor = "cloud-hypervisor" + SnapshotForkParamsTargetHypervisorFirecracker SnapshotForkParamsTargetHypervisor = "firecracker" + SnapshotForkParamsTargetHypervisorQemu SnapshotForkParamsTargetHypervisor = "qemu" + SnapshotForkParamsTargetHypervisorVz SnapshotForkParamsTargetHypervisor = "vz" +) + +// Optional final state for the forked instance. Defaults by snapshot kind: +// +// - Standby snapshot defaults to Running +// - Stopped snapshot defaults to Stopped +type SnapshotForkParamsTargetState string + +const ( + SnapshotForkParamsTargetStateStopped SnapshotForkParamsTargetState = "Stopped" + SnapshotForkParamsTargetStateStandby SnapshotForkParamsTargetState = "Standby" + SnapshotForkParamsTargetStateRunning SnapshotForkParamsTargetState = "Running" +) diff --git a/snapshot_test.go b/snapshot_test.go new file mode 100644 index 0000000..f60da96 --- /dev/null +++ b/snapshot_test.go @@ -0,0 +1,118 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package hypeman_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/kernel/hypeman-go" + "github.com/kernel/hypeman-go/internal/testutil" + "github.com/kernel/hypeman-go/option" +) + +func TestSnapshotListWithOptionalParams(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := hypeman.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Snapshots.List(context.TODO(), hypeman.SnapshotListParams{ + Kind: hypeman.SnapshotKindStandby, + Name: hypeman.String("name"), + SourceInstanceID: hypeman.String("source_instance_id"), + }) + if err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestSnapshotDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := hypeman.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + err := client.Snapshots.Delete(context.TODO(), "snapshotId") + if err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestSnapshotForkWithOptionalParams(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := hypeman.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Snapshots.Fork( + context.TODO(), + "snapshotId", + hypeman.SnapshotForkParams{ + Name: "nginx-from-snap", + TargetHypervisor: hypeman.SnapshotForkParamsTargetHypervisorCloudHypervisor, + TargetState: hypeman.SnapshotForkParamsTargetStateRunning, + }, + ) + if err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestSnapshotGet(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := hypeman.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Snapshots.Get(context.TODO(), "snapshotId") + if err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} From 8b5543e6f8fed7062d28c37fa35ba031e14c6f79 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 18:30:20 +0000 Subject: [PATCH 03/16] feat: Add strict metadata tags across mutable resources --- .stats.yml | 4 ++-- api.md | 10 +++++----- build.go | 27 +++++++++++++++++++++++---- build_test.go | 10 ++++++++-- device.go | 25 +++++++++++++++++++++++-- device_test.go | 15 ++++++++++++--- image.go | 25 +++++++++++++++++++++++-- image_test.go | 15 ++++++++++++--- ingress.go | 25 +++++++++++++++++++++++-- ingress_test.go | 15 ++++++++++++--- instance.go | 4 ++-- instance_test.go | 7 ++++--- instancesnapshot.go | 2 ++ instancesnapshot_test.go | 4 ++++ snapshot.go | 5 +++++ snapshot_test.go | 6 +++++- volume.go | 25 +++++++++++++++++++++++-- volume_test.go | 17 +++++++++++++++-- 18 files changed, 203 insertions(+), 38 deletions(-) diff --git a/.stats.yml b/.stats.yml index e1a8f80..4f0d272 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 45 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-ef3a9825726e97be4c61ca3886bc1ddc04088aa179c1fbf25c73a515df604aa5.yml -openapi_spec_hash: 349c7b6c17298c8dbdfd0750badf5608 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-48572ed071402410249daca70dfcdd151ce9e0bf3674ccce98107ac4e0f9a800.yml +openapi_spec_hash: 15415b3d8b119a94b7d7a71673af0237 config_hash: 1fbc2955725b2b29a941f009ff2a2ce9 diff --git a/api.md b/api.md index 38fff3e..55b2c39 100644 --- a/api.md +++ b/api.md @@ -17,7 +17,7 @@ Response Types: Methods: - client.Images.New(ctx context.Context, body hypeman.ImageNewParams) (\*hypeman.Image, error) -- client.Images.List(ctx context.Context) (\*[]hypeman.Image, error) +- client.Images.List(ctx context.Context, query hypeman.ImageListParams) (\*[]hypeman.Image, error) - client.Images.Delete(ctx context.Context, name string) error - client.Images.Get(ctx context.Context, name string) (\*hypeman.Image, error) @@ -91,7 +91,7 @@ Response Types: Methods: - client.Volumes.New(ctx context.Context, body hypeman.VolumeNewParams) (\*hypeman.Volume, error) -- client.Volumes.List(ctx context.Context) (\*[]hypeman.Volume, error) +- client.Volumes.List(ctx context.Context, query hypeman.VolumeListParams) (\*[]hypeman.Volume, error) - client.Volumes.Delete(ctx context.Context, id string) error - client.Volumes.NewFromArchive(ctx context.Context, body io.Reader, params hypeman.VolumeNewFromArchiveParams) (\*hypeman.Volume, error) - client.Volumes.Get(ctx context.Context, id string) (\*hypeman.Volume, error) @@ -108,7 +108,7 @@ Methods: - client.Devices.New(ctx context.Context, body hypeman.DeviceNewParams) (\*hypeman.Device, error) - client.Devices.Get(ctx context.Context, id string) (\*hypeman.Device, error) -- client.Devices.List(ctx context.Context) (\*[]hypeman.Device, error) +- client.Devices.List(ctx context.Context, query hypeman.DeviceListParams) (\*[]hypeman.Device, error) - client.Devices.Delete(ctx context.Context, id string) error - client.Devices.ListAvailable(ctx context.Context) (\*[]hypeman.AvailableDevice, error) @@ -130,7 +130,7 @@ Response Types: Methods: - client.Ingresses.New(ctx context.Context, body hypeman.IngressNewParams) (\*hypeman.Ingress, error) -- client.Ingresses.List(ctx context.Context) (\*[]hypeman.Ingress, error) +- client.Ingresses.List(ctx context.Context, query hypeman.IngressListParams) (\*[]hypeman.Ingress, error) - client.Ingresses.Delete(ctx context.Context, id string) error - client.Ingresses.Get(ctx context.Context, id string) (\*hypeman.Ingress, error) @@ -162,7 +162,7 @@ Response Types: Methods: - client.Builds.New(ctx context.Context, body hypeman.BuildNewParams) (\*hypeman.Build, error) -- client.Builds.List(ctx context.Context) (\*[]hypeman.Build, error) +- client.Builds.List(ctx context.Context, query hypeman.BuildListParams) (\*[]hypeman.Build, error) - client.Builds.Cancel(ctx context.Context, id string) error - client.Builds.Events(ctx context.Context, id string, query hypeman.BuildEventsParams) (\*hypeman.BuildEvent, error) - client.Builds.Get(ctx context.Context, id string) (\*hypeman.Build, error) diff --git a/build.go b/build.go index 48de63b..26d6f24 100644 --- a/build.go +++ b/build.go @@ -53,10 +53,10 @@ func (r *BuildService) New(ctx context.Context, body BuildNewParams, opts ...opt } // List builds -func (r *BuildService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Build, err error) { +func (r *BuildService) List(ctx context.Context, query BuildListParams, opts ...option.RequestOption) (res *[]Build, err error) { opts = slices.Concat(r.Options, opts) path := "builds" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) return } @@ -128,8 +128,10 @@ type Build struct { // Digest of built image (only when status is ready) ImageDigest string `json:"image_digest" api:"nullable"` // Full image reference (only when status is ready) - ImageRef string `json:"image_ref" api:"nullable"` - Provenance BuildProvenance `json:"provenance"` + ImageRef string `json:"image_ref" api:"nullable"` + // User-defined key-value metadata tags. + Metadata map[string]string `json:"metadata"` + Provenance BuildProvenance `json:"provenance"` // Position in build queue (only when status is queued) QueuePosition int64 `json:"queue_position" api:"nullable"` // Build start timestamp @@ -145,6 +147,7 @@ type Build struct { Error respjson.Field ImageDigest respjson.Field ImageRef respjson.Field + Metadata respjson.Field Provenance respjson.Field QueuePosition respjson.Field StartedAt respjson.Field @@ -262,6 +265,8 @@ type BuildNewParams struct { IsAdminBuild param.Opt[string] `json:"is_admin_build,omitzero"` // Memory limit for builder VM in MB (default 2048) MemoryMB param.Opt[int64] `json:"memory_mb,omitzero"` + // JSON object of metadata tags. Example: {"team":"backend","env":"staging"} + Metadata param.Opt[string] `json:"metadata,omitzero"` // JSON array of secret references to inject during build. Each object has "id" // (required) for use with --mount=type=secret,id=... Example: [{"id": // "npm_token"}, {"id": "github_token"}] @@ -289,6 +294,20 @@ func (r BuildNewParams) MarshalMultipart() (data []byte, contentType string, err return buf.Bytes(), writer.FormDataContentType(), nil } +type BuildListParams struct { + // Filter builds by metadata key-value pairs. + Metadata map[string]string `query:"metadata,omitzero" json:"-"` + paramObj +} + +// URLQuery serializes [BuildListParams]'s query parameters as `url.Values`. +func (r BuildListParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + type BuildEventsParams struct { // Continue streaming new events after initial output Follow param.Opt[bool] `query:"follow,omitzero" json:"-"` diff --git a/build_test.go b/build_test.go index 250c1e3..ea025d3 100644 --- a/build_test.go +++ b/build_test.go @@ -38,6 +38,7 @@ func TestBuildNewWithOptionalParams(t *testing.T) { ImageName: hypeman.String("image_name"), IsAdminBuild: hypeman.String("is_admin_build"), MemoryMB: hypeman.Int(0), + Metadata: hypeman.String("metadata"), Secrets: hypeman.String("secrets"), TimeoutSeconds: hypeman.Int(0), }) @@ -50,7 +51,7 @@ func TestBuildNewWithOptionalParams(t *testing.T) { } } -func TestBuildList(t *testing.T) { +func TestBuildListWithOptionalParams(t *testing.T) { t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { @@ -63,7 +64,12 @@ func TestBuildList(t *testing.T) { option.WithBaseURL(baseURL), option.WithAPIKey("My API Key"), ) - _, err := client.Builds.List(context.TODO()) + _, err := client.Builds.List(context.TODO(), hypeman.BuildListParams{ + Metadata: map[string]string{ + "team": "backend", + "env": "staging", + }, + }) if err != nil { var apierr *hypeman.Error if errors.As(err, &apierr) { diff --git a/device.go b/device.go index c754587..387478d 100644 --- a/device.go +++ b/device.go @@ -7,10 +7,12 @@ import ( "errors" "fmt" "net/http" + "net/url" "slices" "time" "github.com/kernel/hypeman-go/internal/apijson" + "github.com/kernel/hypeman-go/internal/apiquery" "github.com/kernel/hypeman-go/internal/requestconfig" "github.com/kernel/hypeman-go/option" "github.com/kernel/hypeman-go/packages/param" @@ -57,10 +59,10 @@ func (r *DeviceService) Get(ctx context.Context, id string, opts ...option.Reque } // List registered devices -func (r *DeviceService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Device, err error) { +func (r *DeviceService) List(ctx context.Context, query DeviceListParams, opts ...option.RequestOption) (res *[]Device, err error) { opts = slices.Concat(r.Options, opts) path := "devices" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) return } @@ -147,6 +149,8 @@ type Device struct { VendorID string `json:"vendor_id" api:"required"` // Instance ID if attached AttachedTo string `json:"attached_to" api:"nullable"` + // User-defined key-value metadata tags. + Metadata map[string]string `json:"metadata"` // Device name (user-provided or auto-generated from PCI address) Name string `json:"name"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. @@ -160,6 +164,7 @@ type Device struct { Type respjson.Field VendorID respjson.Field AttachedTo respjson.Field + Metadata respjson.Field Name respjson.Field ExtraFields map[string]respjson.Field raw string @@ -186,6 +191,8 @@ type DeviceNewParams struct { // Optional globally unique device name. If not provided, a name is auto-generated // from the PCI address (e.g., "pci-0000-a2-00-0") Name param.Opt[string] `json:"name,omitzero"` + // User-defined key-value metadata tags. + Metadata map[string]string `json:"metadata,omitzero"` paramObj } @@ -196,3 +203,17 @@ func (r DeviceNewParams) MarshalJSON() (data []byte, err error) { func (r *DeviceNewParams) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } + +type DeviceListParams struct { + // Filter devices by metadata key-value pairs. + Metadata map[string]string `query:"metadata,omitzero" json:"-"` + paramObj +} + +// URLQuery serializes [DeviceListParams]'s query parameters as `url.Values`. +func (r DeviceListParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/device_test.go b/device_test.go index 1c09257..c4f4853 100644 --- a/device_test.go +++ b/device_test.go @@ -28,7 +28,11 @@ func TestDeviceNewWithOptionalParams(t *testing.T) { ) _, err := client.Devices.New(context.TODO(), hypeman.DeviceNewParams{ PciAddress: "0000:a2:00.0", - Name: hypeman.String("l4-gpu"), + Metadata: map[string]string{ + "team": "backend", + "env": "staging", + }, + Name: hypeman.String("l4-gpu"), }) if err != nil { var apierr *hypeman.Error @@ -62,7 +66,7 @@ func TestDeviceGet(t *testing.T) { } } -func TestDeviceList(t *testing.T) { +func TestDeviceListWithOptionalParams(t *testing.T) { t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { @@ -75,7 +79,12 @@ func TestDeviceList(t *testing.T) { option.WithBaseURL(baseURL), option.WithAPIKey("My API Key"), ) - _, err := client.Devices.List(context.TODO()) + _, err := client.Devices.List(context.TODO(), hypeman.DeviceListParams{ + Metadata: map[string]string{ + "team": "backend", + "env": "staging", + }, + }) if err != nil { var apierr *hypeman.Error if errors.As(err, &apierr) { diff --git a/image.go b/image.go index 5c11648..5c78fd4 100644 --- a/image.go +++ b/image.go @@ -7,10 +7,12 @@ import ( "errors" "fmt" "net/http" + "net/url" "slices" "time" "github.com/kernel/hypeman-go/internal/apijson" + "github.com/kernel/hypeman-go/internal/apiquery" "github.com/kernel/hypeman-go/internal/requestconfig" "github.com/kernel/hypeman-go/option" "github.com/kernel/hypeman-go/packages/param" @@ -45,10 +47,10 @@ func (r *ImageService) New(ctx context.Context, body ImageNewParams, opts ...opt } // List images -func (r *ImageService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Image, err error) { +func (r *ImageService) List(ctx context.Context, query ImageListParams, opts ...option.RequestOption) (res *[]Image, err error) { opts = slices.Concat(r.Options, opts) path := "images" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) return } @@ -96,6 +98,8 @@ type Image struct { Env map[string]string `json:"env"` // Error message if status is failed Error string `json:"error" api:"nullable"` + // User-defined key-value metadata tags. + Metadata map[string]string `json:"metadata"` // Position in build queue (null if not queued) QueuePosition int64 `json:"queue_position" api:"nullable"` // Disk size in bytes (null until ready) @@ -112,6 +116,7 @@ type Image struct { Entrypoint respjson.Field Env respjson.Field Error respjson.Field + Metadata respjson.Field QueuePosition respjson.Field SizeBytes respjson.Field WorkingDir respjson.Field @@ -140,6 +145,8 @@ const ( type ImageNewParams struct { // OCI image reference (e.g., docker.io/library/nginx:latest) Name string `json:"name" api:"required"` + // User-defined key-value metadata tags. + Metadata map[string]string `json:"metadata,omitzero"` paramObj } @@ -150,3 +157,17 @@ func (r ImageNewParams) MarshalJSON() (data []byte, err error) { func (r *ImageNewParams) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } + +type ImageListParams struct { + // Filter images by metadata key-value pairs. + Metadata map[string]string `query:"metadata,omitzero" json:"-"` + paramObj +} + +// URLQuery serializes [ImageListParams]'s query parameters as `url.Values`. +func (r ImageListParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/image_test.go b/image_test.go index e799946..e3b04fb 100644 --- a/image_test.go +++ b/image_test.go @@ -13,7 +13,7 @@ import ( "github.com/kernel/hypeman-go/option" ) -func TestImageNew(t *testing.T) { +func TestImageNewWithOptionalParams(t *testing.T) { t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { @@ -28,6 +28,10 @@ func TestImageNew(t *testing.T) { ) _, err := client.Images.New(context.TODO(), hypeman.ImageNewParams{ Name: "docker.io/library/nginx:latest", + Metadata: map[string]string{ + "team": "backend", + "env": "staging", + }, }) if err != nil { var apierr *hypeman.Error @@ -38,7 +42,7 @@ func TestImageNew(t *testing.T) { } } -func TestImageList(t *testing.T) { +func TestImageListWithOptionalParams(t *testing.T) { t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { @@ -51,7 +55,12 @@ func TestImageList(t *testing.T) { option.WithBaseURL(baseURL), option.WithAPIKey("My API Key"), ) - _, err := client.Images.List(context.TODO()) + _, err := client.Images.List(context.TODO(), hypeman.ImageListParams{ + Metadata: map[string]string{ + "team": "backend", + "env": "staging", + }, + }) if err != nil { var apierr *hypeman.Error if errors.As(err, &apierr) { diff --git a/ingress.go b/ingress.go index 860f31c..1dd6fed 100644 --- a/ingress.go +++ b/ingress.go @@ -8,10 +8,12 @@ import ( "errors" "fmt" "net/http" + "net/url" "slices" "time" "github.com/kernel/hypeman-go/internal/apijson" + "github.com/kernel/hypeman-go/internal/apiquery" "github.com/kernel/hypeman-go/internal/requestconfig" "github.com/kernel/hypeman-go/option" "github.com/kernel/hypeman-go/packages/param" @@ -46,10 +48,10 @@ func (r *IngressService) New(ctx context.Context, body IngressNewParams, opts .. } // List ingresses -func (r *IngressService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Ingress, err error) { +func (r *IngressService) List(ctx context.Context, query IngressListParams, opts ...option.RequestOption) (res *[]Ingress, err error) { opts = slices.Concat(r.Options, opts) path := "ingresses" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) return } @@ -87,12 +89,15 @@ type Ingress struct { Name string `json:"name" api:"required"` // Routing rules for this ingress Rules []IngressRule `json:"rules" api:"required"` + // User-defined key-value metadata tags. + Metadata map[string]string `json:"metadata"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { ID respjson.Field CreatedAt respjson.Field Name respjson.Field Rules respjson.Field + Metadata respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -284,6 +289,8 @@ type IngressNewParams struct { Name string `json:"name" api:"required"` // Routing rules for this ingress Rules []IngressRuleParam `json:"rules,omitzero" api:"required"` + // User-defined key-value metadata tags. + Metadata map[string]string `json:"metadata,omitzero"` paramObj } @@ -294,3 +301,17 @@ func (r IngressNewParams) MarshalJSON() (data []byte, err error) { func (r *IngressNewParams) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } + +type IngressListParams struct { + // Filter ingresses by metadata key-value pairs. + Metadata map[string]string `query:"metadata,omitzero" json:"-"` + paramObj +} + +// URLQuery serializes [IngressListParams]'s query parameters as `url.Values`. +func (r IngressListParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/ingress_test.go b/ingress_test.go index 28bbab5..d03f8dc 100644 --- a/ingress_test.go +++ b/ingress_test.go @@ -13,7 +13,7 @@ import ( "github.com/kernel/hypeman-go/option" ) -func TestIngressNew(t *testing.T) { +func TestIngressNewWithOptionalParams(t *testing.T) { t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { @@ -40,6 +40,10 @@ func TestIngressNew(t *testing.T) { RedirectHTTP: hypeman.Bool(true), Tls: hypeman.Bool(true), }}, + Metadata: map[string]string{ + "team": "backend", + "env": "staging", + }, }) if err != nil { var apierr *hypeman.Error @@ -50,7 +54,7 @@ func TestIngressNew(t *testing.T) { } } -func TestIngressList(t *testing.T) { +func TestIngressListWithOptionalParams(t *testing.T) { t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { @@ -63,7 +67,12 @@ func TestIngressList(t *testing.T) { option.WithBaseURL(baseURL), option.WithAPIKey("My API Key"), ) - _, err := client.Ingresses.List(context.TODO()) + _, err := client.Ingresses.List(context.TODO(), hypeman.IngressListParams{ + Metadata: map[string]string{ + "team": "backend", + "env": "staging", + }, + }) if err != nil { var apierr *hypeman.Error if errors.As(err, &apierr) { diff --git a/instance.go b/instance.go index c41408b..2edc640 100644 --- a/instance.go +++ b/instance.go @@ -238,7 +238,7 @@ type Instance struct { // // Any of "cloud-hypervisor", "firecracker", "qemu", "vz". Hypervisor InstanceHypervisor `json:"hypervisor"` - // User-defined key-value metadata + // User-defined key-value metadata tags. Metadata map[string]string `json:"metadata"` // Network configuration of the instance Network InstanceNetwork `json:"network"` @@ -567,7 +567,7 @@ type InstanceNewParams struct { // // Any of "cloud-hypervisor", "firecracker", "qemu", "vz". Hypervisor InstanceNewParamsHypervisor `json:"hypervisor,omitzero"` - // User-defined key-value metadata for the instance + // User-defined key-value metadata tags. Metadata map[string]string `json:"metadata,omitzero"` // Network configuration for the instance Network InstanceNewParamsNetwork `json:"network,omitzero"` diff --git a/instance_test.go b/instance_test.go index 92f99ae..de544c4 100644 --- a/instance_test.go +++ b/instance_test.go @@ -43,8 +43,8 @@ func TestInstanceNewWithOptionalParams(t *testing.T) { HotplugSize: hypeman.String("2GB"), Hypervisor: hypeman.InstanceNewParamsHypervisorCloudHypervisor, Metadata: map[string]string{ - "team": "backend", - "purpose": "staging", + "team": "backend", + "env": "staging", }, Network: hypeman.InstanceNewParamsNetwork{ BandwidthDownload: hypeman.String("1Gbps"), @@ -88,7 +88,8 @@ func TestInstanceListWithOptionalParams(t *testing.T) { ) _, err := client.Instances.List(context.TODO(), hypeman.InstanceListParams{ Metadata: map[string]string{ - "foo": "string", + "team": "backend", + "env": "staging", }, State: hypeman.InstanceListParamsStateCreated, }) diff --git a/instancesnapshot.go b/instancesnapshot.go index 51a8648..9d219e7 100644 --- a/instancesnapshot.go +++ b/instancesnapshot.go @@ -70,6 +70,8 @@ type InstanceSnapshotNewParams struct { // Optional snapshot name (lowercase letters, digits, and dashes only; cannot start // or end with a dash) Name param.Opt[string] `json:"name,omitzero"` + // User-defined key-value metadata tags. + Metadata map[string]string `json:"metadata,omitzero"` paramObj } diff --git a/instancesnapshot_test.go b/instancesnapshot_test.go index f41e0d5..d613da5 100644 --- a/instancesnapshot_test.go +++ b/instancesnapshot_test.go @@ -31,6 +31,10 @@ func TestInstanceSnapshotNewWithOptionalParams(t *testing.T) { "id", hypeman.InstanceSnapshotNewParams{ Kind: hypeman.SnapshotKindStandby, + Metadata: map[string]string{ + "team": "backend", + "env": "staging", + }, Name: hypeman.String("pre-upgrade"), }, ) diff --git a/snapshot.go b/snapshot.go index 42e06ea..9895db7 100644 --- a/snapshot.go +++ b/snapshot.go @@ -102,6 +102,8 @@ type Snapshot struct { SourceInstanceID string `json:"source_instance_id" api:"required"` // Source instance name at snapshot creation time SourceInstanceName string `json:"source_instance_name" api:"required"` + // User-defined key-value metadata tags. + Metadata map[string]string `json:"metadata"` // Optional human-readable snapshot name (unique per source instance) Name string `json:"name" api:"nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. @@ -113,6 +115,7 @@ type Snapshot struct { SourceHypervisor respjson.Field SourceInstanceID respjson.Field SourceInstanceName respjson.Field + Metadata respjson.Field Name respjson.Field ExtraFields map[string]respjson.Field raw string @@ -152,6 +155,8 @@ type SnapshotListParams struct { // // Any of "Standby", "Stopped". Kind SnapshotKind `query:"kind,omitzero" json:"-"` + // Filter snapshots by metadata key-value pairs. + Metadata map[string]string `query:"metadata,omitzero" json:"-"` paramObj } diff --git a/snapshot_test.go b/snapshot_test.go index f60da96..f1092fb 100644 --- a/snapshot_test.go +++ b/snapshot_test.go @@ -27,7 +27,11 @@ func TestSnapshotListWithOptionalParams(t *testing.T) { option.WithAPIKey("My API Key"), ) _, err := client.Snapshots.List(context.TODO(), hypeman.SnapshotListParams{ - Kind: hypeman.SnapshotKindStandby, + Kind: hypeman.SnapshotKindStandby, + Metadata: map[string]string{ + "team": "backend", + "env": "staging", + }, Name: hypeman.String("name"), SourceInstanceID: hypeman.String("source_instance_id"), }) diff --git a/volume.go b/volume.go index 4cbf2a0..991e35b 100644 --- a/volume.go +++ b/volume.go @@ -51,10 +51,10 @@ func (r *VolumeService) New(ctx context.Context, body VolumeNewParams, opts ...o } // List volumes -func (r *VolumeService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Volume, err error) { +func (r *VolumeService) List(ctx context.Context, query VolumeListParams, opts ...option.RequestOption) (res *[]Volume, err error) { opts = slices.Concat(r.Options, opts) path := "volumes" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) return } @@ -104,6 +104,8 @@ type Volume struct { SizeGB int64 `json:"size_gb" api:"required"` // List of current attachments (empty if not attached) Attachments []VolumeAttachment `json:"attachments"` + // User-defined key-value metadata tags. + Metadata map[string]string `json:"metadata"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { ID respjson.Field @@ -111,6 +113,7 @@ type Volume struct { Name respjson.Field SizeGB respjson.Field Attachments respjson.Field + Metadata respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -152,6 +155,8 @@ type VolumeNewParams struct { SizeGB int64 `json:"size_gb" api:"required"` // Optional custom identifier (auto-generated if not provided) ID param.Opt[string] `json:"id,omitzero"` + // User-defined key-value metadata tags. + Metadata map[string]string `json:"metadata,omitzero"` paramObj } @@ -163,6 +168,20 @@ func (r *VolumeNewParams) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +type VolumeListParams struct { + // Filter volumes by metadata key-value pairs. + Metadata map[string]string `query:"metadata,omitzero" json:"-"` + paramObj +} + +// URLQuery serializes [VolumeListParams]'s query parameters as `url.Values`. +func (r VolumeListParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + type VolumeNewFromArchiveParams struct { // Volume name Name string `query:"name" api:"required" json:"-"` @@ -170,6 +189,8 @@ type VolumeNewFromArchiveParams struct { SizeGB int64 `query:"size_gb" api:"required" json:"-"` // Optional custom volume ID (auto-generated if not provided) ID param.Opt[string] `query:"id,omitzero" json:"-"` + // Metadata tags for the created volume. + Metadata map[string]string `query:"metadata,omitzero" json:"-"` paramObj } diff --git a/volume_test.go b/volume_test.go index 2f90508..7b18156 100644 --- a/volume_test.go +++ b/volume_test.go @@ -32,6 +32,10 @@ func TestVolumeNewWithOptionalParams(t *testing.T) { Name: "my-data-volume", SizeGB: 10, ID: hypeman.String("vol-data-1"), + Metadata: map[string]string{ + "team": "backend", + "env": "staging", + }, }) if err != nil { var apierr *hypeman.Error @@ -42,7 +46,7 @@ func TestVolumeNewWithOptionalParams(t *testing.T) { } } -func TestVolumeList(t *testing.T) { +func TestVolumeListWithOptionalParams(t *testing.T) { t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { @@ -55,7 +59,12 @@ func TestVolumeList(t *testing.T) { option.WithBaseURL(baseURL), option.WithAPIKey("My API Key"), ) - _, err := client.Volumes.List(context.TODO()) + _, err := client.Volumes.List(context.TODO(), hypeman.VolumeListParams{ + Metadata: map[string]string{ + "team": "backend", + "env": "staging", + }, + }) if err != nil { var apierr *hypeman.Error if errors.As(err, &apierr) { @@ -108,6 +117,10 @@ func TestVolumeNewFromArchiveWithOptionalParams(t *testing.T) { Name: "name", SizeGB: 0, ID: hypeman.String("id"), + Metadata: map[string]string{ + "team": "backend", + "env": "staging", + }, }, ) if err != nil { From 0d9d654f8074387de55f5befe709477c6905deaa Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 19:51:07 +0000 Subject: [PATCH 04/16] chore(ci): skip uploading artifacts on stainless-internal branches --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab1f59c..7f1d37d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,14 +27,18 @@ jobs: - uses: actions/checkout@v6 - name: Get GitHub OIDC Token - if: github.repository == 'stainless-sdks/hypeman-go' + if: |- + github.repository == 'stainless-sdks/hypeman-go' && + !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); - name: Upload tarball - if: github.repository == 'stainless-sdks/hypeman-go' + if: |- + github.repository == 'stainless-sdks/hypeman-go' && + !startsWith(github.ref, 'refs/heads/stl/') env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} From bea84ac19f1178f378cc6ba8bad07a26848b5492 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 19:53:05 +0000 Subject: [PATCH 05/16] chore: update placeholder string --- build_test.go | 2 +- volume_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build_test.go b/build_test.go index ea025d3..292220f 100644 --- a/build_test.go +++ b/build_test.go @@ -29,7 +29,7 @@ func TestBuildNewWithOptionalParams(t *testing.T) { option.WithAPIKey("My API Key"), ) _, err := client.Builds.New(context.TODO(), hypeman.BuildNewParams{ - Source: io.Reader(bytes.NewBuffer([]byte("some file contents"))), + Source: io.Reader(bytes.NewBuffer([]byte("Example data"))), BaseImageDigest: hypeman.String("base_image_digest"), CacheScope: hypeman.String("cache_scope"), CPUs: hypeman.Int(0), diff --git a/volume_test.go b/volume_test.go index 7b18156..b10053b 100644 --- a/volume_test.go +++ b/volume_test.go @@ -112,7 +112,7 @@ func TestVolumeNewFromArchiveWithOptionalParams(t *testing.T) { ) _, err := client.Volumes.NewFromArchive( context.TODO(), - io.Reader(bytes.NewBuffer([]byte("some file contents"))), + io.Reader(bytes.NewBuffer([]byte("Example data"))), hypeman.VolumeNewFromArchiveParams{ Name: "name", SizeGB: 0, From 2f8e29e033b44a8a492ec9a3bdcbc44cabf2eb11 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:16:11 +0000 Subject: [PATCH 06/16] feat: Rename tag fields from metadata to tags --- .stats.yml | 4 ++-- build.go | 18 +++++++++--------- build_test.go | 4 ++-- device.go | 14 +++++++------- device_test.go | 6 +++--- image.go | 14 +++++++------- image_test.go | 4 ++-- ingress.go | 14 +++++++------- ingress_test.go | 4 ++-- instance.go | 18 +++++++++--------- instance_test.go | 14 +++++++------- instancesnapshot.go | 4 ++-- instancesnapshot_test.go | 4 ++-- snapshot.go | 10 +++++----- snapshot_test.go | 8 ++++---- volume.go | 18 +++++++++--------- volume_test.go | 6 +++--- 17 files changed, 82 insertions(+), 82 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4f0d272..7b0bdb7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 45 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-48572ed071402410249daca70dfcdd151ce9e0bf3674ccce98107ac4e0f9a800.yml -openapi_spec_hash: 15415b3d8b119a94b7d7a71673af0237 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-d3b8f05f7a0fb4f832292528da15cf65caabc14b38ac9fbc9263f8496b0ec346.yml +openapi_spec_hash: 30f28d648747a2c4fe2231d7a08fe735 config_hash: 1fbc2955725b2b29a941f009ff2a2ce9 diff --git a/build.go b/build.go index 26d6f24..10294ae 100644 --- a/build.go +++ b/build.go @@ -128,14 +128,14 @@ type Build struct { // Digest of built image (only when status is ready) ImageDigest string `json:"image_digest" api:"nullable"` // Full image reference (only when status is ready) - ImageRef string `json:"image_ref" api:"nullable"` - // User-defined key-value metadata tags. - Metadata map[string]string `json:"metadata"` - Provenance BuildProvenance `json:"provenance"` + ImageRef string `json:"image_ref" api:"nullable"` + Provenance BuildProvenance `json:"provenance"` // Position in build queue (only when status is queued) QueuePosition int64 `json:"queue_position" api:"nullable"` // Build start timestamp StartedAt time.Time `json:"started_at" api:"nullable" format:"date-time"` + // User-defined key-value tags. + Tags map[string]string `json:"tags"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { ID respjson.Field @@ -147,10 +147,10 @@ type Build struct { Error respjson.Field ImageDigest respjson.Field ImageRef respjson.Field - Metadata respjson.Field Provenance respjson.Field QueuePosition respjson.Field StartedAt respjson.Field + Tags respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -265,12 +265,12 @@ type BuildNewParams struct { IsAdminBuild param.Opt[string] `json:"is_admin_build,omitzero"` // Memory limit for builder VM in MB (default 2048) MemoryMB param.Opt[int64] `json:"memory_mb,omitzero"` - // JSON object of metadata tags. Example: {"team":"backend","env":"staging"} - Metadata param.Opt[string] `json:"metadata,omitzero"` // JSON array of secret references to inject during build. Each object has "id" // (required) for use with --mount=type=secret,id=... Example: [{"id": // "npm_token"}, {"id": "github_token"}] Secrets param.Opt[string] `json:"secrets,omitzero"` + // JSON object of tags. Example: {"team":"backend","env":"staging"} + Tags param.Opt[string] `json:"tags,omitzero"` // Build timeout (default 600) TimeoutSeconds param.Opt[int64] `json:"timeout_seconds,omitzero"` paramObj @@ -295,8 +295,8 @@ func (r BuildNewParams) MarshalMultipart() (data []byte, contentType string, err } type BuildListParams struct { - // Filter builds by metadata key-value pairs. - Metadata map[string]string `query:"metadata,omitzero" json:"-"` + // Filter builds by tag key-value pairs. + Tags map[string]string `query:"tags,omitzero" json:"-"` paramObj } diff --git a/build_test.go b/build_test.go index 292220f..c7b7139 100644 --- a/build_test.go +++ b/build_test.go @@ -38,8 +38,8 @@ func TestBuildNewWithOptionalParams(t *testing.T) { ImageName: hypeman.String("image_name"), IsAdminBuild: hypeman.String("is_admin_build"), MemoryMB: hypeman.Int(0), - Metadata: hypeman.String("metadata"), Secrets: hypeman.String("secrets"), + Tags: hypeman.String("tags"), TimeoutSeconds: hypeman.Int(0), }) if err != nil { @@ -65,7 +65,7 @@ func TestBuildListWithOptionalParams(t *testing.T) { option.WithAPIKey("My API Key"), ) _, err := client.Builds.List(context.TODO(), hypeman.BuildListParams{ - Metadata: map[string]string{ + Tags: map[string]string{ "team": "backend", "env": "staging", }, diff --git a/device.go b/device.go index 387478d..5f920b1 100644 --- a/device.go +++ b/device.go @@ -149,10 +149,10 @@ type Device struct { VendorID string `json:"vendor_id" api:"required"` // Instance ID if attached AttachedTo string `json:"attached_to" api:"nullable"` - // User-defined key-value metadata tags. - Metadata map[string]string `json:"metadata"` // Device name (user-provided or auto-generated from PCI address) Name string `json:"name"` + // User-defined key-value tags. + Tags map[string]string `json:"tags"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { ID respjson.Field @@ -164,8 +164,8 @@ type Device struct { Type respjson.Field VendorID respjson.Field AttachedTo respjson.Field - Metadata respjson.Field Name respjson.Field + Tags respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -191,8 +191,8 @@ type DeviceNewParams struct { // Optional globally unique device name. If not provided, a name is auto-generated // from the PCI address (e.g., "pci-0000-a2-00-0") Name param.Opt[string] `json:"name,omitzero"` - // User-defined key-value metadata tags. - Metadata map[string]string `json:"metadata,omitzero"` + // User-defined key-value tags. + Tags map[string]string `json:"tags,omitzero"` paramObj } @@ -205,8 +205,8 @@ func (r *DeviceNewParams) UnmarshalJSON(data []byte) error { } type DeviceListParams struct { - // Filter devices by metadata key-value pairs. - Metadata map[string]string `query:"metadata,omitzero" json:"-"` + // Filter devices by tag key-value pairs. + Tags map[string]string `query:"tags,omitzero" json:"-"` paramObj } diff --git a/device_test.go b/device_test.go index c4f4853..48d0d79 100644 --- a/device_test.go +++ b/device_test.go @@ -28,11 +28,11 @@ func TestDeviceNewWithOptionalParams(t *testing.T) { ) _, err := client.Devices.New(context.TODO(), hypeman.DeviceNewParams{ PciAddress: "0000:a2:00.0", - Metadata: map[string]string{ + Name: hypeman.String("l4-gpu"), + Tags: map[string]string{ "team": "backend", "env": "staging", }, - Name: hypeman.String("l4-gpu"), }) if err != nil { var apierr *hypeman.Error @@ -80,7 +80,7 @@ func TestDeviceListWithOptionalParams(t *testing.T) { option.WithAPIKey("My API Key"), ) _, err := client.Devices.List(context.TODO(), hypeman.DeviceListParams{ - Metadata: map[string]string{ + Tags: map[string]string{ "team": "backend", "env": "staging", }, diff --git a/image.go b/image.go index 5c78fd4..1cb9e5b 100644 --- a/image.go +++ b/image.go @@ -98,12 +98,12 @@ type Image struct { Env map[string]string `json:"env"` // Error message if status is failed Error string `json:"error" api:"nullable"` - // User-defined key-value metadata tags. - Metadata map[string]string `json:"metadata"` // Position in build queue (null if not queued) QueuePosition int64 `json:"queue_position" api:"nullable"` // Disk size in bytes (null until ready) SizeBytes int64 `json:"size_bytes" api:"nullable"` + // User-defined key-value tags. + Tags map[string]string `json:"tags"` // Working directory from container metadata WorkingDir string `json:"working_dir" api:"nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. @@ -116,9 +116,9 @@ type Image struct { Entrypoint respjson.Field Env respjson.Field Error respjson.Field - Metadata respjson.Field QueuePosition respjson.Field SizeBytes respjson.Field + Tags respjson.Field WorkingDir respjson.Field ExtraFields map[string]respjson.Field raw string @@ -145,8 +145,8 @@ const ( type ImageNewParams struct { // OCI image reference (e.g., docker.io/library/nginx:latest) Name string `json:"name" api:"required"` - // User-defined key-value metadata tags. - Metadata map[string]string `json:"metadata,omitzero"` + // User-defined key-value tags. + Tags map[string]string `json:"tags,omitzero"` paramObj } @@ -159,8 +159,8 @@ func (r *ImageNewParams) UnmarshalJSON(data []byte) error { } type ImageListParams struct { - // Filter images by metadata key-value pairs. - Metadata map[string]string `query:"metadata,omitzero" json:"-"` + // Filter images by tag key-value pairs. + Tags map[string]string `query:"tags,omitzero" json:"-"` paramObj } diff --git a/image_test.go b/image_test.go index e3b04fb..9631e75 100644 --- a/image_test.go +++ b/image_test.go @@ -28,7 +28,7 @@ func TestImageNewWithOptionalParams(t *testing.T) { ) _, err := client.Images.New(context.TODO(), hypeman.ImageNewParams{ Name: "docker.io/library/nginx:latest", - Metadata: map[string]string{ + Tags: map[string]string{ "team": "backend", "env": "staging", }, @@ -56,7 +56,7 @@ func TestImageListWithOptionalParams(t *testing.T) { option.WithAPIKey("My API Key"), ) _, err := client.Images.List(context.TODO(), hypeman.ImageListParams{ - Metadata: map[string]string{ + Tags: map[string]string{ "team": "backend", "env": "staging", }, diff --git a/ingress.go b/ingress.go index 1dd6fed..fd5e121 100644 --- a/ingress.go +++ b/ingress.go @@ -89,15 +89,15 @@ type Ingress struct { Name string `json:"name" api:"required"` // Routing rules for this ingress Rules []IngressRule `json:"rules" api:"required"` - // User-defined key-value metadata tags. - Metadata map[string]string `json:"metadata"` + // User-defined key-value tags. + Tags map[string]string `json:"tags"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { ID respjson.Field CreatedAt respjson.Field Name respjson.Field Rules respjson.Field - Metadata respjson.Field + Tags respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -289,8 +289,8 @@ type IngressNewParams struct { Name string `json:"name" api:"required"` // Routing rules for this ingress Rules []IngressRuleParam `json:"rules,omitzero" api:"required"` - // User-defined key-value metadata tags. - Metadata map[string]string `json:"metadata,omitzero"` + // User-defined key-value tags. + Tags map[string]string `json:"tags,omitzero"` paramObj } @@ -303,8 +303,8 @@ func (r *IngressNewParams) UnmarshalJSON(data []byte) error { } type IngressListParams struct { - // Filter ingresses by metadata key-value pairs. - Metadata map[string]string `query:"metadata,omitzero" json:"-"` + // Filter ingresses by tag key-value pairs. + Tags map[string]string `query:"tags,omitzero" json:"-"` paramObj } diff --git a/ingress_test.go b/ingress_test.go index d03f8dc..68693c9 100644 --- a/ingress_test.go +++ b/ingress_test.go @@ -40,7 +40,7 @@ func TestIngressNewWithOptionalParams(t *testing.T) { RedirectHTTP: hypeman.Bool(true), Tls: hypeman.Bool(true), }}, - Metadata: map[string]string{ + Tags: map[string]string{ "team": "backend", "env": "staging", }, @@ -68,7 +68,7 @@ func TestIngressListWithOptionalParams(t *testing.T) { option.WithAPIKey("My API Key"), ) _, err := client.Ingresses.List(context.TODO(), hypeman.IngressListParams{ - Metadata: map[string]string{ + Tags: map[string]string{ "team": "backend", "env": "staging", }, diff --git a/instance.go b/instance.go index 2edc640..d7b6d0e 100644 --- a/instance.go +++ b/instance.go @@ -238,8 +238,6 @@ type Instance struct { // // Any of "cloud-hypervisor", "firecracker", "qemu", "vz". Hypervisor InstanceHypervisor `json:"hypervisor"` - // User-defined key-value metadata tags. - Metadata map[string]string `json:"metadata"` // Network configuration of the instance Network InstanceNetwork `json:"network"` // Writable overlay disk size (human-readable) @@ -252,6 +250,8 @@ type Instance struct { StateError string `json:"state_error" api:"nullable"` // Stop timestamp (RFC3339) StoppedAt time.Time `json:"stopped_at" api:"nullable" format:"date-time"` + // User-defined key-value tags. + Tags map[string]string `json:"tags"` // Number of virtual CPUs Vcpus int64 `json:"vcpus"` // Volumes attached to the instance @@ -271,13 +271,13 @@ type Instance struct { HasSnapshot respjson.Field HotplugSize respjson.Field Hypervisor respjson.Field - Metadata respjson.Field Network respjson.Field OverlaySize respjson.Field Size respjson.Field StartedAt respjson.Field StateError respjson.Field StoppedAt respjson.Field + Tags respjson.Field Vcpus respjson.Field Volumes respjson.Field ExtraFields map[string]respjson.Field @@ -567,10 +567,10 @@ type InstanceNewParams struct { // // Any of "cloud-hypervisor", "firecracker", "qemu", "vz". Hypervisor InstanceNewParamsHypervisor `json:"hypervisor,omitzero"` - // User-defined key-value metadata tags. - Metadata map[string]string `json:"metadata,omitzero"` // Network configuration for the instance Network InstanceNewParamsNetwork `json:"network,omitzero"` + // User-defined key-value tags. + Tags map[string]string `json:"tags,omitzero"` // Volumes to attach to the instance at creation time Volumes []VolumeMountParam `json:"volumes,omitzero"` paramObj @@ -631,15 +631,15 @@ func (r *InstanceNewParamsNetwork) UnmarshalJSON(data []byte) error { } type InstanceListParams struct { - // Filter instances by metadata key-value pairs. Uses deepObject style: - // ?metadata[team]=backend&metadata[env]=staging Multiple entries are ANDed - // together. All specified key-value pairs must match. - Metadata map[string]string `query:"metadata,omitzero" json:"-"` // Filter instances by state (e.g., Running, Stopped) // // Any of "Created", "Running", "Paused", "Shutdown", "Stopped", "Standby", // "Unknown". State InstanceListParamsState `query:"state,omitzero" json:"-"` + // Filter instances by tag key-value pairs. Uses deepObject style: + // ?tags[team]=backend&tags[env]=staging Multiple entries are ANDed together. All + // specified key-value pairs must match. + Tags map[string]string `query:"tags,omitzero" json:"-"` paramObj } diff --git a/instance_test.go b/instance_test.go index de544c4..deb108e 100644 --- a/instance_test.go +++ b/instance_test.go @@ -42,10 +42,6 @@ func TestInstanceNewWithOptionalParams(t *testing.T) { }, HotplugSize: hypeman.String("2GB"), Hypervisor: hypeman.InstanceNewParamsHypervisorCloudHypervisor, - Metadata: map[string]string{ - "team": "backend", - "env": "staging", - }, Network: hypeman.InstanceNewParamsNetwork{ BandwidthDownload: hypeman.String("1Gbps"), BandwidthUpload: hypeman.String("1Gbps"), @@ -55,7 +51,11 @@ func TestInstanceNewWithOptionalParams(t *testing.T) { Size: hypeman.String("2GB"), SkipGuestAgent: hypeman.Bool(false), SkipKernelHeaders: hypeman.Bool(true), - Vcpus: hypeman.Int(2), + Tags: map[string]string{ + "team": "backend", + "env": "staging", + }, + Vcpus: hypeman.Int(2), Volumes: []hypeman.VolumeMountParam{{ MountPath: "/mnt/data", VolumeID: "vol-abc123", @@ -87,11 +87,11 @@ func TestInstanceListWithOptionalParams(t *testing.T) { option.WithAPIKey("My API Key"), ) _, err := client.Instances.List(context.TODO(), hypeman.InstanceListParams{ - Metadata: map[string]string{ + State: hypeman.InstanceListParamsStateCreated, + Tags: map[string]string{ "team": "backend", "env": "staging", }, - State: hypeman.InstanceListParamsStateCreated, }) if err != nil { var apierr *hypeman.Error diff --git a/instancesnapshot.go b/instancesnapshot.go index 9d219e7..9175e90 100644 --- a/instancesnapshot.go +++ b/instancesnapshot.go @@ -70,8 +70,8 @@ type InstanceSnapshotNewParams struct { // Optional snapshot name (lowercase letters, digits, and dashes only; cannot start // or end with a dash) Name param.Opt[string] `json:"name,omitzero"` - // User-defined key-value metadata tags. - Metadata map[string]string `json:"metadata,omitzero"` + // User-defined key-value tags. + Tags map[string]string `json:"tags,omitzero"` paramObj } diff --git a/instancesnapshot_test.go b/instancesnapshot_test.go index d613da5..755f0e4 100644 --- a/instancesnapshot_test.go +++ b/instancesnapshot_test.go @@ -31,11 +31,11 @@ func TestInstanceSnapshotNewWithOptionalParams(t *testing.T) { "id", hypeman.InstanceSnapshotNewParams{ Kind: hypeman.SnapshotKindStandby, - Metadata: map[string]string{ + Name: hypeman.String("pre-upgrade"), + Tags: map[string]string{ "team": "backend", "env": "staging", }, - Name: hypeman.String("pre-upgrade"), }, ) if err != nil { diff --git a/snapshot.go b/snapshot.go index 9895db7..08b5c5a 100644 --- a/snapshot.go +++ b/snapshot.go @@ -102,10 +102,10 @@ type Snapshot struct { SourceInstanceID string `json:"source_instance_id" api:"required"` // Source instance name at snapshot creation time SourceInstanceName string `json:"source_instance_name" api:"required"` - // User-defined key-value metadata tags. - Metadata map[string]string `json:"metadata"` // Optional human-readable snapshot name (unique per source instance) Name string `json:"name" api:"nullable"` + // User-defined key-value tags. + Tags map[string]string `json:"tags"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { ID respjson.Field @@ -115,8 +115,8 @@ type Snapshot struct { SourceHypervisor respjson.Field SourceInstanceID respjson.Field SourceInstanceName respjson.Field - Metadata respjson.Field Name respjson.Field + Tags respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -155,8 +155,8 @@ type SnapshotListParams struct { // // Any of "Standby", "Stopped". Kind SnapshotKind `query:"kind,omitzero" json:"-"` - // Filter snapshots by metadata key-value pairs. - Metadata map[string]string `query:"metadata,omitzero" json:"-"` + // Filter snapshots by tag key-value pairs. + Tags map[string]string `query:"tags,omitzero" json:"-"` paramObj } diff --git a/snapshot_test.go b/snapshot_test.go index f1092fb..dfbb194 100644 --- a/snapshot_test.go +++ b/snapshot_test.go @@ -27,13 +27,13 @@ func TestSnapshotListWithOptionalParams(t *testing.T) { option.WithAPIKey("My API Key"), ) _, err := client.Snapshots.List(context.TODO(), hypeman.SnapshotListParams{ - Kind: hypeman.SnapshotKindStandby, - Metadata: map[string]string{ + Kind: hypeman.SnapshotKindStandby, + Name: hypeman.String("name"), + SourceInstanceID: hypeman.String("source_instance_id"), + Tags: map[string]string{ "team": "backend", "env": "staging", }, - Name: hypeman.String("name"), - SourceInstanceID: hypeman.String("source_instance_id"), }) if err != nil { var apierr *hypeman.Error diff --git a/volume.go b/volume.go index 991e35b..d41a112 100644 --- a/volume.go +++ b/volume.go @@ -104,8 +104,8 @@ type Volume struct { SizeGB int64 `json:"size_gb" api:"required"` // List of current attachments (empty if not attached) Attachments []VolumeAttachment `json:"attachments"` - // User-defined key-value metadata tags. - Metadata map[string]string `json:"metadata"` + // User-defined key-value tags. + Tags map[string]string `json:"tags"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { ID respjson.Field @@ -113,7 +113,7 @@ type Volume struct { Name respjson.Field SizeGB respjson.Field Attachments respjson.Field - Metadata respjson.Field + Tags respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -155,8 +155,8 @@ type VolumeNewParams struct { SizeGB int64 `json:"size_gb" api:"required"` // Optional custom identifier (auto-generated if not provided) ID param.Opt[string] `json:"id,omitzero"` - // User-defined key-value metadata tags. - Metadata map[string]string `json:"metadata,omitzero"` + // User-defined key-value tags. + Tags map[string]string `json:"tags,omitzero"` paramObj } @@ -169,8 +169,8 @@ func (r *VolumeNewParams) UnmarshalJSON(data []byte) error { } type VolumeListParams struct { - // Filter volumes by metadata key-value pairs. - Metadata map[string]string `query:"metadata,omitzero" json:"-"` + // Filter volumes by tag key-value pairs. + Tags map[string]string `query:"tags,omitzero" json:"-"` paramObj } @@ -189,8 +189,8 @@ type VolumeNewFromArchiveParams struct { SizeGB int64 `query:"size_gb" api:"required" json:"-"` // Optional custom volume ID (auto-generated if not provided) ID param.Opt[string] `query:"id,omitzero" json:"-"` - // Metadata tags for the created volume. - Metadata map[string]string `query:"metadata,omitzero" json:"-"` + // Tags for the created volume. + Tags map[string]string `query:"tags,omitzero" json:"-"` paramObj } diff --git a/volume_test.go b/volume_test.go index b10053b..aca90e7 100644 --- a/volume_test.go +++ b/volume_test.go @@ -32,7 +32,7 @@ func TestVolumeNewWithOptionalParams(t *testing.T) { Name: "my-data-volume", SizeGB: 10, ID: hypeman.String("vol-data-1"), - Metadata: map[string]string{ + Tags: map[string]string{ "team": "backend", "env": "staging", }, @@ -60,7 +60,7 @@ func TestVolumeListWithOptionalParams(t *testing.T) { option.WithAPIKey("My API Key"), ) _, err := client.Volumes.List(context.TODO(), hypeman.VolumeListParams{ - Metadata: map[string]string{ + Tags: map[string]string{ "team": "backend", "env": "staging", }, @@ -117,7 +117,7 @@ func TestVolumeNewFromArchiveWithOptionalParams(t *testing.T) { Name: "name", SizeGB: 0, ID: hypeman.String("id"), - Metadata: map[string]string{ + Tags: map[string]string{ "team": "backend", "env": "staging", }, From 0b3751a37068dca3771ad04fc1af876c5e52384f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:35:48 +0000 Subject: [PATCH 07/16] feat: Add always-on /metrics endpoint with dual pull/push telemetry --- .stats.yml | 4 ++-- instance.go | 44 ++++++++++++++++++++++++-------------------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/.stats.yml b/.stats.yml index 7b0bdb7..fd7192c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 45 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-d3b8f05f7a0fb4f832292528da15cf65caabc14b38ac9fbc9263f8496b0ec346.yml -openapi_spec_hash: 30f28d648747a2c4fe2231d7a08fe735 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-12f78ec90ae60eea3df85d7dbe7b6840e728d4782a565762adcb35b6f8a0fab4.yml +openapi_spec_hash: fedfe1621e41fe217b0192423eb9bb80 config_hash: 1fbc2955725b2b29a941f009ff2a2ce9 diff --git a/instance.go b/instance.go index d7b6d0e..eed7af1 100644 --- a/instance.go +++ b/instance.go @@ -209,15 +209,16 @@ type Instance struct { // Instance state: // // - Created: VMM created but not started (Cloud Hypervisor native) - // - Running: VM is actively running (Cloud Hypervisor native) + // - Initializing: VM is running while guest init is still in progress + // - Running: Guest program has started and instance is ready // - Paused: VM is paused (Cloud Hypervisor native) // - Shutdown: VM shut down but VMM exists (Cloud Hypervisor native) // - Stopped: No VMM running, no snapshot exists // - Standby: No VMM running, snapshot exists (can be restored) // - Unknown: Failed to determine state (see state_error for details) // - // Any of "Created", "Running", "Paused", "Shutdown", "Stopped", "Standby", - // "Unknown". + // Any of "Created", "Initializing", "Running", "Paused", "Shutdown", "Stopped", + // "Standby", "Unknown". State InstanceState `json:"state" api:"required"` // Disk I/O rate limit (human-readable, e.g., "100MB/s") DiskIoBps string `json:"disk_io_bps"` @@ -294,7 +295,8 @@ func (r *Instance) UnmarshalJSON(data []byte) error { // Instance state: // // - Created: VMM created but not started (Cloud Hypervisor native) -// - Running: VM is actively running (Cloud Hypervisor native) +// - Initializing: VM is running while guest init is still in progress +// - Running: Guest program has started and instance is ready // - Paused: VM is paused (Cloud Hypervisor native) // - Shutdown: VM shut down but VMM exists (Cloud Hypervisor native) // - Stopped: No VMM running, no snapshot exists @@ -303,13 +305,14 @@ func (r *Instance) UnmarshalJSON(data []byte) error { type InstanceState string const ( - InstanceStateCreated InstanceState = "Created" - InstanceStateRunning InstanceState = "Running" - InstanceStatePaused InstanceState = "Paused" - InstanceStateShutdown InstanceState = "Shutdown" - InstanceStateStopped InstanceState = "Stopped" - InstanceStateStandby InstanceState = "Standby" - InstanceStateUnknown InstanceState = "Unknown" + InstanceStateCreated InstanceState = "Created" + InstanceStateInitializing InstanceState = "Initializing" + InstanceStateRunning InstanceState = "Running" + InstanceStatePaused InstanceState = "Paused" + InstanceStateShutdown InstanceState = "Shutdown" + InstanceStateStopped InstanceState = "Stopped" + InstanceStateStandby InstanceState = "Standby" + InstanceStateUnknown InstanceState = "Unknown" ) // GPU information attached to the instance @@ -633,8 +636,8 @@ func (r *InstanceNewParamsNetwork) UnmarshalJSON(data []byte) error { type InstanceListParams struct { // Filter instances by state (e.g., Running, Stopped) // - // Any of "Created", "Running", "Paused", "Shutdown", "Stopped", "Standby", - // "Unknown". + // Any of "Created", "Initializing", "Running", "Paused", "Shutdown", "Stopped", + // "Standby", "Unknown". State InstanceListParamsState `query:"state,omitzero" json:"-"` // Filter instances by tag key-value pairs. Uses deepObject style: // ?tags[team]=backend&tags[env]=staging Multiple entries are ANDed together. All @@ -655,13 +658,14 @@ func (r InstanceListParams) URLQuery() (v url.Values, err error) { type InstanceListParamsState string const ( - InstanceListParamsStateCreated InstanceListParamsState = "Created" - InstanceListParamsStateRunning InstanceListParamsState = "Running" - InstanceListParamsStatePaused InstanceListParamsState = "Paused" - InstanceListParamsStateShutdown InstanceListParamsState = "Shutdown" - InstanceListParamsStateStopped InstanceListParamsState = "Stopped" - InstanceListParamsStateStandby InstanceListParamsState = "Standby" - InstanceListParamsStateUnknown InstanceListParamsState = "Unknown" + InstanceListParamsStateCreated InstanceListParamsState = "Created" + InstanceListParamsStateInitializing InstanceListParamsState = "Initializing" + InstanceListParamsStateRunning InstanceListParamsState = "Running" + InstanceListParamsStatePaused InstanceListParamsState = "Paused" + InstanceListParamsStateShutdown InstanceListParamsState = "Shutdown" + InstanceListParamsStateStopped InstanceListParamsState = "Stopped" + InstanceListParamsStateStandby InstanceListParamsState = "Standby" + InstanceListParamsStateUnknown InstanceListParamsState = "Unknown" ) type InstanceForkParams struct { From 1917d6de179a6a36358598cf3b1f6d4ebbe7f034 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:37:12 +0000 Subject: [PATCH 08/16] chore(internal): minor cleanup --- client_test.go | 2 +- internal/apiform/form_test.go | 9 ++++++--- internal/apiform/tag.go | 2 ++ internal/apijson/decoder.go | 9 +++------ internal/apijson/encoder.go | 21 --------------------- internal/apijson/enum.go | 9 ++------- internal/apijson/json_test.go | 6 +++--- internal/apijson/tag.go | 2 ++ internal/apiquery/encoder.go | 23 +---------------------- internal/requestconfig/requestconfig.go | 6 +++--- usage_test.go | 2 +- 11 files changed, 24 insertions(+), 67 deletions(-) diff --git a/client_test.go b/client_test.go index cd17cef..997045e 100644 --- a/client_test.go +++ b/client_test.go @@ -39,7 +39,7 @@ func TestUserAgentHeader(t *testing.T) { }, }), ) - client.Health.Check(context.Background()) + _, _ = client.Health.Check(context.Background()) if userAgent != fmt.Sprintf("Hypeman/Go %s", internal.PackageVersion) { t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent) } diff --git a/internal/apiform/form_test.go b/internal/apiform/form_test.go index 2884602..737af2c 100644 --- a/internal/apiform/form_test.go +++ b/internal/apiform/form_test.go @@ -585,14 +585,17 @@ func TestEncode(t *testing.T) { t.Run(name, func(t *testing.T) { buf := bytes.NewBuffer(nil) writer := multipart.NewWriter(buf) - writer.SetBoundary("xxx") + err := writer.SetBoundary("xxx") + if err != nil { + t.Errorf("setting boundary for %v failed with error %v", test.val, err) + } - var arrayFmt string = "indices:dots" + arrayFmt := "indices:dots" if tags := strings.Split(name, ","); len(tags) > 1 { arrayFmt = tags[1] } - err := MarshalWithSettings(test.val, writer, arrayFmt) + err = MarshalWithSettings(test.val, writer, arrayFmt) if err != nil { t.Errorf("serialization of %v failed with error %v", test.val, err) } diff --git a/internal/apiform/tag.go b/internal/apiform/tag.go index b353617..5dd0b45 100644 --- a/internal/apiform/tag.go +++ b/internal/apiform/tag.go @@ -60,6 +60,8 @@ func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { tag.extras = true case "required": tag.required = true + case "metadata": + tag.metadata = true } } } diff --git a/internal/apijson/decoder.go b/internal/apijson/decoder.go index 95c9c28..0225b9f 100644 --- a/internal/apijson/decoder.go +++ b/internal/apijson/decoder.go @@ -393,7 +393,7 @@ func (d *decoderBuilder) newStructTypeDecoder(t reflect.Type) decoderFunc { for _, decoder := range anonymousDecoders { // ignore errors - decoder.fn(node, value.FieldByIndex(decoder.idx), state) + _ = decoder.fn(node, value.FieldByIndex(decoder.idx), state) } for _, inlineDecoder := range inlineDecoders { @@ -462,7 +462,7 @@ func (d *decoderBuilder) newStructTypeDecoder(t reflect.Type) decoderFunc { // Handle null [param.Opt] if itemNode.Type == gjson.Null && dest.IsValid() && dest.Type().Implements(reflect.TypeOf((*param.Optional)(nil)).Elem()) { - dest.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(itemNode.Raw)) + _ = dest.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(itemNode.Raw)) continue } @@ -684,8 +684,5 @@ func guardUnknown(state *decoderState, v reflect.Value) bool { constantString, ok := v.Interface().(interface{ Default() string }) named := v.Type() != stringType - if guardStrict(state, ok && named && v.Equal(reflect.ValueOf(constantString.Default()))) { - return true - } - return false + return guardStrict(state, ok && named && v.Equal(reflect.ValueOf(constantString.Default()))) } diff --git a/internal/apijson/encoder.go b/internal/apijson/encoder.go index ab7a3c1..f4e3a5c 100644 --- a/internal/apijson/encoder.go +++ b/internal/apijson/encoder.go @@ -290,27 +290,6 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { } } -func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc { - f, _ := t.FieldByName("Value") - enc := e.typeEncoder(f.Type) - - return func(value reflect.Value) (json []byte, err error) { - present := value.FieldByName("Present") - if !present.Bool() { - return nil, nil - } - null := value.FieldByName("Null") - if null.Bool() { - return []byte("null"), nil - } - raw := value.FieldByName("Raw") - if !raw.IsNil() { - return e.typeEncoder(raw.Type())(raw) - } - return enc(value.FieldByName("Value")) - } -} - func (e *encoder) newTimeTypeEncoder() encoderFunc { format := e.dateFormat return func(value reflect.Value) (json []byte, err error) { diff --git a/internal/apijson/enum.go b/internal/apijson/enum.go index 5bef11c..a1626a5 100644 --- a/internal/apijson/enum.go +++ b/internal/apijson/enum.go @@ -4,7 +4,6 @@ import ( "fmt" "reflect" "slices" - "sync" "github.com/tidwall/gjson" ) @@ -15,7 +14,6 @@ import ( type validationEntry struct { field reflect.StructField - required bool legalValues struct { strings []string // 1 represents true, 0 represents false, -1 represents either @@ -24,9 +22,6 @@ type validationEntry struct { } } -type validatorFunc func(reflect.Value) exactness - -var validators sync.Map var validationRegistry = map[reflect.Type][]validationEntry{} func RegisterFieldValidator[T any, V string | bool | int | float64](fieldName string, values ...V) { @@ -111,9 +106,9 @@ func (state *decoderState) validateBool(v reflect.Value) { return } b := v.Bool() - if state.validator.legalValues.bools == 1 && b == false { + if state.validator.legalValues.bools == 1 && !b { state.exactness = loose - } else if state.validator.legalValues.bools == 0 && b == true { + } else if state.validator.legalValues.bools == 0 && b { state.exactness = loose } } diff --git a/internal/apijson/json_test.go b/internal/apijson/json_test.go index fac9fcc..6932a7b 100644 --- a/internal/apijson/json_test.go +++ b/internal/apijson/json_test.go @@ -87,7 +87,7 @@ type JSONFieldStruct struct { C string `json:"c"` D string `json:"d"` ExtraFields map[string]int64 `json:"" api:"extrafields"` - JSON JSONFieldStructJSON `json:",metadata"` + JSON JSONFieldStructJSON `json:"-" api:"metadata"` } type JSONFieldStructJSON struct { @@ -113,12 +113,12 @@ type Union interface { type Inline struct { InlineField Primitives `json:",inline"` - JSON InlineJSON `json:",metadata"` + JSON InlineJSON `json:"-" api:"metadata"` } type InlineArray struct { InlineField []string `json:",inline"` - JSON InlineJSON `json:",metadata"` + JSON InlineJSON `json:"-" api:"metadata"` } type InlineJSON struct { diff --git a/internal/apijson/tag.go b/internal/apijson/tag.go index 49731b8..0511d69 100644 --- a/internal/apijson/tag.go +++ b/internal/apijson/tag.go @@ -57,6 +57,8 @@ func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { tag.extras = true case "required": tag.required = true + case "metadata": + tag.metadata = true } } } diff --git a/internal/apiquery/encoder.go b/internal/apiquery/encoder.go index a98c29c..4221840 100644 --- a/internal/apiquery/encoder.go +++ b/internal/apiquery/encoder.go @@ -193,7 +193,7 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { return func(key string, value reflect.Value) (pairs []Pair, err error) { for _, ef := range encoderFields { - var subkey string = e.renderKeyPath(key, ef.tag.name) + subkey := e.renderKeyPath(key, ef.tag.name) if ef.tag.inline { subkey = key } @@ -372,27 +372,6 @@ func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc { } } -func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc { - f, _ := t.FieldByName("Value") - enc := e.typeEncoder(f.Type) - - return func(key string, value reflect.Value) ([]Pair, error) { - present := value.FieldByName("Present") - if !present.Bool() { - return nil, nil - } - null := value.FieldByName("Null") - if null.Bool() { - return nil, fmt.Errorf("apiquery: field cannot be null") - } - raw := value.FieldByName("Raw") - if !raw.IsNil() { - return e.typeEncoder(raw.Type())(key, raw) - } - return enc(key, value.FieldByName("Value")) - } -} - func (e *encoder) newTimeTypeEncoder(_ reflect.Type) encoderFunc { format := e.dateFormat return func(key string, value reflect.Value) ([]Pair, error) { diff --git a/internal/requestconfig/requestconfig.go b/internal/requestconfig/requestconfig.go index 7080257..b578df8 100644 --- a/internal/requestconfig/requestconfig.go +++ b/internal/requestconfig/requestconfig.go @@ -461,7 +461,7 @@ func (cfg *RequestConfig) Execute() (err error) { // Close the response body before retrying to prevent connection leaks if res != nil && res.Body != nil { - res.Body.Close() + _ = res.Body.Close() } select { @@ -489,7 +489,7 @@ func (cfg *RequestConfig) Execute() (err error) { if res.StatusCode >= 400 { contents, err := io.ReadAll(res.Body) - res.Body.Close() + _ = res.Body.Close() if err != nil { return err } @@ -520,7 +520,7 @@ func (cfg *RequestConfig) Execute() (err error) { } contents, err := io.ReadAll(res.Body) - res.Body.Close() + _ = res.Body.Close() if err != nil { return fmt.Errorf("error reading response body: %w", err) } diff --git a/usage_test.go b/usage_test.go index 436721b..195a051 100644 --- a/usage_test.go +++ b/usage_test.go @@ -13,6 +13,7 @@ import ( ) func TestUsage(t *testing.T) { + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -24,7 +25,6 @@ func TestUsage(t *testing.T) { option.WithBaseURL(baseURL), option.WithAPIKey("My API Key"), ) - t.Skip("Mock server tests are disabled") response, err := client.Health.Check(context.TODO()) if err != nil { t.Fatalf("err should be nil: %s", err.Error()) From 923db74ba5750233d429a5e98dc6cb5fb0e806ba Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:55:35 +0000 Subject: [PATCH 09/16] chore(internal): use explicit returns --- build.go | 14 +++++++------- device.go | 14 +++++++------- health.go | 2 +- image.go | 12 ++++++------ ingress.go | 12 ++++++------ instance.go | 42 +++++++++++++++++++++--------------------- instancesnapshot.go | 10 +++++----- instancevolume.go | 12 ++++++------ resource.go | 2 +- snapshot.go | 14 +++++++------- volume.go | 14 +++++++------- 11 files changed, 74 insertions(+), 74 deletions(-) diff --git a/build.go b/build.go index 10294ae..ad536ea 100644 --- a/build.go +++ b/build.go @@ -49,7 +49,7 @@ func (r *BuildService) New(ctx context.Context, body BuildNewParams, opts ...opt opts = slices.Concat(r.Options, opts) path := "builds" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } // List builds @@ -57,7 +57,7 @@ func (r *BuildService) List(ctx context.Context, query BuildListParams, opts ... opts = slices.Concat(r.Options, opts) path := "builds" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) - return + return res, err } // Cancel build @@ -66,11 +66,11 @@ func (r *BuildService) Cancel(ctx context.Context, id string, opts ...option.Req opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) if id == "" { err = errors.New("missing required id parameter") - return + return err } path := fmt.Sprintf("builds/%s", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) - return + return err } // Streams build events as Server-Sent Events. Events include: @@ -89,7 +89,7 @@ func (r *BuildService) EventsStreaming(ctx context.Context, id string, query Bui opts = append([]option.RequestOption{option.WithHeader("Accept", "text/event-stream")}, opts...) if id == "" { err = errors.New("missing required id parameter") - return + return ssestream.NewStream[BuildEvent](nil, err) } path := fmt.Sprintf("builds/%s/events", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &raw, opts...) @@ -101,11 +101,11 @@ func (r *BuildService) Get(ctx context.Context, id string, opts ...option.Reques opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("builds/%s", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } type Build struct { diff --git a/device.go b/device.go index 5f920b1..7b69256 100644 --- a/device.go +++ b/device.go @@ -43,7 +43,7 @@ func (r *DeviceService) New(ctx context.Context, body DeviceNewParams, opts ...o opts = slices.Concat(r.Options, opts) path := "devices" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } // Get device details @@ -51,11 +51,11 @@ func (r *DeviceService) Get(ctx context.Context, id string, opts ...option.Reque opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("devices/%s", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } // List registered devices @@ -63,7 +63,7 @@ func (r *DeviceService) List(ctx context.Context, query DeviceListParams, opts . opts = slices.Concat(r.Options, opts) path := "devices" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) - return + return res, err } // Unregister device @@ -72,11 +72,11 @@ func (r *DeviceService) Delete(ctx context.Context, id string, opts ...option.Re opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) if id == "" { err = errors.New("missing required id parameter") - return + return err } path := fmt.Sprintf("devices/%s", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) - return + return err } // Discover passthrough-capable devices on host @@ -84,7 +84,7 @@ func (r *DeviceService) ListAvailable(ctx context.Context, opts ...option.Reques opts = slices.Concat(r.Options, opts) path := "devices/available" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } type AvailableDevice struct { diff --git a/health.go b/health.go index 9e6cc0f..5532bf4 100644 --- a/health.go +++ b/health.go @@ -37,7 +37,7 @@ func (r *HealthService) Check(ctx context.Context, opts ...option.RequestOption) opts = slices.Concat(r.Options, opts) path := "health" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } type HealthCheckResponse struct { diff --git a/image.go b/image.go index 1cb9e5b..babf0db 100644 --- a/image.go +++ b/image.go @@ -43,7 +43,7 @@ func (r *ImageService) New(ctx context.Context, body ImageNewParams, opts ...opt opts = slices.Concat(r.Options, opts) path := "images" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } // List images @@ -51,7 +51,7 @@ func (r *ImageService) List(ctx context.Context, query ImageListParams, opts ... opts = slices.Concat(r.Options, opts) path := "images" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) - return + return res, err } // Delete image @@ -60,11 +60,11 @@ func (r *ImageService) Delete(ctx context.Context, name string, opts ...option.R opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) if name == "" { err = errors.New("missing required name parameter") - return + return err } path := fmt.Sprintf("images/%s", name) err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) - return + return err } // Get image details @@ -72,11 +72,11 @@ func (r *ImageService) Get(ctx context.Context, name string, opts ...option.Requ opts = slices.Concat(r.Options, opts) if name == "" { err = errors.New("missing required name parameter") - return + return nil, err } path := fmt.Sprintf("images/%s", name) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } type Image struct { diff --git a/ingress.go b/ingress.go index fd5e121..8d4393d 100644 --- a/ingress.go +++ b/ingress.go @@ -44,7 +44,7 @@ func (r *IngressService) New(ctx context.Context, body IngressNewParams, opts .. opts = slices.Concat(r.Options, opts) path := "ingresses" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } // List ingresses @@ -52,7 +52,7 @@ func (r *IngressService) List(ctx context.Context, query IngressListParams, opts opts = slices.Concat(r.Options, opts) path := "ingresses" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) - return + return res, err } // Delete ingress @@ -61,11 +61,11 @@ func (r *IngressService) Delete(ctx context.Context, id string, opts ...option.R opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) if id == "" { err = errors.New("missing required id parameter") - return + return err } path := fmt.Sprintf("ingresses/%s", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) - return + return err } // Get ingress details @@ -73,11 +73,11 @@ func (r *IngressService) Get(ctx context.Context, id string, opts ...option.Requ opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("ingresses/%s", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } type Ingress struct { diff --git a/instance.go b/instance.go index eed7af1..349506f 100644 --- a/instance.go +++ b/instance.go @@ -49,7 +49,7 @@ func (r *InstanceService) New(ctx context.Context, body InstanceNewParams, opts opts = slices.Concat(r.Options, opts) path := "instances" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } // List instances @@ -57,7 +57,7 @@ func (r *InstanceService) List(ctx context.Context, query InstanceListParams, op opts = slices.Concat(r.Options, opts) path := "instances" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) - return + return res, err } // Stop and delete instance @@ -66,11 +66,11 @@ func (r *InstanceService) Delete(ctx context.Context, id string, opts ...option. opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) if id == "" { err = errors.New("missing required id parameter") - return + return err } path := fmt.Sprintf("instances/%s", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) - return + return err } // Fork an instance from stopped, standby, or running (with from_running=true) @@ -78,11 +78,11 @@ func (r *InstanceService) Fork(ctx context.Context, id string, body InstanceFork opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("instances/%s/fork", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } // Get instance details @@ -90,11 +90,11 @@ func (r *InstanceService) Get(ctx context.Context, id string, opts ...option.Req opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("instances/%s", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } // Streams instance logs as Server-Sent Events. Use the `source` parameter to @@ -115,7 +115,7 @@ func (r *InstanceService) LogsStreaming(ctx context.Context, id string, query In opts = append([]option.RequestOption{option.WithHeader("Accept", "text/event-stream")}, opts...) if id == "" { err = errors.New("missing required id parameter") - return + return ssestream.NewStream[string](nil, err) } path := fmt.Sprintf("instances/%s/logs", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &raw, opts...) @@ -127,11 +127,11 @@ func (r *InstanceService) Restore(ctx context.Context, id string, opts ...option opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("instances/%s/restore", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) - return + return res, err } // Put instance in standby (pause, snapshot, delete VMM) @@ -139,11 +139,11 @@ func (r *InstanceService) Standby(ctx context.Context, id string, opts ...option opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("instances/%s/standby", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) - return + return res, err } // Start a stopped instance @@ -151,11 +151,11 @@ func (r *InstanceService) Start(ctx context.Context, id string, body InstanceSta opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("instances/%s/start", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } // Returns information about a path in the guest filesystem. Useful for checking if @@ -164,11 +164,11 @@ func (r *InstanceService) Stat(ctx context.Context, id string, query InstanceSta opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("instances/%s/stat", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) - return + return res, err } // Returns real-time resource utilization statistics for a running VM instance. @@ -178,11 +178,11 @@ func (r *InstanceService) Stats(ctx context.Context, id string, opts ...option.R opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("instances/%s/stats", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } // Stop instance (graceful shutdown) @@ -190,11 +190,11 @@ func (r *InstanceService) Stop(ctx context.Context, id string, opts ...option.Re opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("instances/%s/stop", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) - return + return res, err } type Instance struct { diff --git a/instancesnapshot.go b/instancesnapshot.go index 9175e90..889f833 100644 --- a/instancesnapshot.go +++ b/instancesnapshot.go @@ -39,11 +39,11 @@ func (r *InstanceSnapshotService) New(ctx context.Context, id string, body Insta opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("instances/%s/snapshots", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } // Restore an instance from a snapshot in-place @@ -51,15 +51,15 @@ func (r *InstanceSnapshotService) Restore(ctx context.Context, snapshotID string opts = slices.Concat(r.Options, opts) if params.ID == "" { err = errors.New("missing required id parameter") - return + return nil, err } if snapshotID == "" { err = errors.New("missing required snapshotId parameter") - return + return nil, err } path := fmt.Sprintf("instances/%s/snapshots/%s/restore", params.ID, snapshotID) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) - return + return res, err } type InstanceSnapshotNewParams struct { diff --git a/instancevolume.go b/instancevolume.go index 2bc78d5..5ebb8e0 100644 --- a/instancevolume.go +++ b/instancevolume.go @@ -39,15 +39,15 @@ func (r *InstanceVolumeService) Attach(ctx context.Context, volumeID string, par opts = slices.Concat(r.Options, opts) if params.ID == "" { err = errors.New("missing required id parameter") - return + return nil, err } if volumeID == "" { err = errors.New("missing required volumeId parameter") - return + return nil, err } path := fmt.Sprintf("instances/%s/volumes/%s", params.ID, volumeID) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) - return + return res, err } // Detach volume from instance @@ -55,15 +55,15 @@ func (r *InstanceVolumeService) Detach(ctx context.Context, volumeID string, bod opts = slices.Concat(r.Options, opts) if body.ID == "" { err = errors.New("missing required id parameter") - return + return nil, err } if volumeID == "" { err = errors.New("missing required volumeId parameter") - return + return nil, err } path := fmt.Sprintf("instances/%s/volumes/%s", body.ID, volumeID) err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, &res, opts...) - return + return res, err } type InstanceVolumeAttachParams struct { diff --git a/resource.go b/resource.go index 6ee51a1..f8ba67d 100644 --- a/resource.go +++ b/resource.go @@ -39,7 +39,7 @@ func (r *ResourceService) Get(ctx context.Context, opts ...option.RequestOption) opts = slices.Concat(r.Options, opts) path := "resources" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } type DiskBreakdown struct { diff --git a/snapshot.go b/snapshot.go index 08b5c5a..99fe8a7 100644 --- a/snapshot.go +++ b/snapshot.go @@ -43,7 +43,7 @@ func (r *SnapshotService) List(ctx context.Context, query SnapshotListParams, op opts = slices.Concat(r.Options, opts) path := "snapshots" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) - return + return res, err } // Delete a snapshot @@ -52,11 +52,11 @@ func (r *SnapshotService) Delete(ctx context.Context, snapshotID string, opts .. opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) if snapshotID == "" { err = errors.New("missing required snapshotId parameter") - return + return err } path := fmt.Sprintf("snapshots/%s", snapshotID) err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) - return + return err } // Fork a new instance from a snapshot @@ -64,11 +64,11 @@ func (r *SnapshotService) Fork(ctx context.Context, snapshotID string, body Snap opts = slices.Concat(r.Options, opts) if snapshotID == "" { err = errors.New("missing required snapshotId parameter") - return + return nil, err } path := fmt.Sprintf("snapshots/%s/fork", snapshotID) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } // Get snapshot details @@ -76,11 +76,11 @@ func (r *SnapshotService) Get(ctx context.Context, snapshotID string, opts ...op opts = slices.Concat(r.Options, opts) if snapshotID == "" { err = errors.New("missing required snapshotId parameter") - return + return nil, err } path := fmt.Sprintf("snapshots/%s", snapshotID) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } type Snapshot struct { diff --git a/volume.go b/volume.go index d41a112..127ccf0 100644 --- a/volume.go +++ b/volume.go @@ -47,7 +47,7 @@ func (r *VolumeService) New(ctx context.Context, body VolumeNewParams, opts ...o opts = slices.Concat(r.Options, opts) path := "volumes" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } // List volumes @@ -55,7 +55,7 @@ func (r *VolumeService) List(ctx context.Context, query VolumeListParams, opts . opts = slices.Concat(r.Options, opts) path := "volumes" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) - return + return res, err } // Delete volume @@ -64,11 +64,11 @@ func (r *VolumeService) Delete(ctx context.Context, id string, opts ...option.Re opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) if id == "" { err = errors.New("missing required id parameter") - return + return err } path := fmt.Sprintf("volumes/%s", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) - return + return err } // Creates a new volume pre-populated with content from a tar.gz archive. The @@ -78,7 +78,7 @@ func (r *VolumeService) NewFromArchive(ctx context.Context, body io.Reader, para opts = append([]option.RequestOption{option.WithRequestBody("application/gzip", body)}, opts...) path := "volumes/from-archive" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) - return + return res, err } // Get volume details @@ -86,11 +86,11 @@ func (r *VolumeService) Get(ctx context.Context, id string, opts ...option.Reque opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("volumes/%s", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } type Volume struct { From 16131b802aa3f5a2b32d298d179966a440f53af5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:01:40 +0000 Subject: [PATCH 10/16] chore(internal): use explicit returns in more places --- internal/apiform/encoder.go | 2 +- internal/apiform/tag.go | 6 +++--- internal/apijson/encoder.go | 2 +- internal/apijson/json_test.go | 2 +- internal/apijson/tag.go | 6 +++--- internal/apiquery/encoder.go | 10 +++++----- internal/apiquery/tag.go | 6 +++--- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index a2f1516..61effd1 100644 --- a/internal/apiform/encoder.go +++ b/internal/apiform/encoder.go @@ -469,5 +469,5 @@ func WriteExtras(writer *multipart.Writer, extras map[string]any) (err error) { break } } - return + return err } diff --git a/internal/apiform/tag.go b/internal/apiform/tag.go index 5dd0b45..d9915d4 100644 --- a/internal/apiform/tag.go +++ b/internal/apiform/tag.go @@ -24,7 +24,7 @@ func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool raw, ok = field.Tag.Lookup(jsonStructTag) } if !ok { - return + return tag, ok } parts := strings.Split(raw, ",") if len(parts) == 0 { @@ -45,7 +45,7 @@ func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool } parseApiStructTag(field, &tag) - return + return tag, ok } func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { @@ -68,5 +68,5 @@ func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { format, ok = field.Tag.Lookup(formatStructTag) - return + return format, ok } diff --git a/internal/apijson/encoder.go b/internal/apijson/encoder.go index f4e3a5c..0decb73 100644 --- a/internal/apijson/encoder.go +++ b/internal/apijson/encoder.go @@ -286,7 +286,7 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { return nil, err } } - return + return json, err } } diff --git a/internal/apijson/json_test.go b/internal/apijson/json_test.go index 6932a7b..19b3614 100644 --- a/internal/apijson/json_test.go +++ b/internal/apijson/json_test.go @@ -268,7 +268,7 @@ type MarshallingUnionStruct struct { func (r *MarshallingUnionStruct) UnmarshalJSON(data []byte) (err error) { *r = MarshallingUnionStruct{} err = UnmarshalRoot(data, &r.Union) - return + return err } func (r MarshallingUnionStruct) MarshalJSON() (data []byte, err error) { diff --git a/internal/apijson/tag.go b/internal/apijson/tag.go index 0511d69..17b2130 100644 --- a/internal/apijson/tag.go +++ b/internal/apijson/tag.go @@ -20,7 +20,7 @@ type parsedStructTag struct { func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { raw, ok := field.Tag.Lookup(jsonStructTag) if !ok { - return + return tag, ok } parts := strings.Split(raw, ",") if len(parts) == 0 { @@ -42,7 +42,7 @@ func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool // the `api` struct tag is only used alongside `json` for custom behaviour parseApiStructTag(field, &tag) - return + return tag, ok } func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { @@ -65,5 +65,5 @@ func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { format, ok = field.Tag.Lookup(formatStructTag) - return + return format, ok } diff --git a/internal/apiquery/encoder.go b/internal/apiquery/encoder.go index 4221840..ac31823 100644 --- a/internal/apiquery/encoder.go +++ b/internal/apiquery/encoder.go @@ -103,7 +103,7 @@ func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc { encoder := e.typeEncoder(t.Elem()) return func(key string, value reflect.Value) (pairs []Pair, err error) { if !value.IsValid() || value.IsNil() { - return + return pairs, err } return encoder(key, value.Elem()) } @@ -205,7 +205,7 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { } pairs = append(pairs, subpairs...) } - return + return pairs, err } } @@ -256,7 +256,7 @@ func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc { } pairs = append(pairs, subpairs...) } - return + return pairs, err } } @@ -300,7 +300,7 @@ func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { } pairs = append(pairs, subpairs...) } - return + return pairs, err } case ArrayQueryFormatIndices: panic("The array indices format is not supported yet") @@ -315,7 +315,7 @@ func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { } pairs = append(pairs, subpairs...) } - return + return pairs, err } default: panic(fmt.Sprintf("Unknown ArrayFormat value: %d", e.settings.ArrayFormat)) diff --git a/internal/apiquery/tag.go b/internal/apiquery/tag.go index 772c40e..9e413ad 100644 --- a/internal/apiquery/tag.go +++ b/internal/apiquery/tag.go @@ -18,7 +18,7 @@ type parsedStructTag struct { func parseQueryStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { raw, ok := field.Tag.Lookup(queryStructTag) if !ok { - return + return tag, ok } parts := strings.Split(raw, ",") if len(parts) == 0 { @@ -35,10 +35,10 @@ func parseQueryStructTag(field reflect.StructField) (tag parsedStructTag, ok boo tag.inline = true } } - return + return tag, ok } func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { format, ok = field.Tag.Lookup(formatStructTag) - return + return format, ok } From 6dc7e68725e65b75bd7c2aa07d4427909ad2365c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:37:35 +0000 Subject: [PATCH 11/16] chore(internal): tweak CI branches --- .github/workflows/ci.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f1d37d..5fb9003 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' From e8b721cbc709af739827929839e443981d636233 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:55:13 +0000 Subject: [PATCH 12/16] feat: add optional VM egress MITM proxy with mock-secret header rewriting --- .stats.yml | 4 +- instance.go | 125 +++++++++++++++++++++++++++++++++++++++++++++++ instance_test.go | 28 +++++++++-- 3 files changed, 151 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index fd7192c..f269291 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 45 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-12f78ec90ae60eea3df85d7dbe7b6840e728d4782a565762adcb35b6f8a0fab4.yml -openapi_spec_hash: fedfe1621e41fe217b0192423eb9bb80 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-84cb170b2dc7bee2d94ec86519a050da932bd0d49cbc5ebded23d463adeafbb8.yml +openapi_spec_hash: dcd84edb5d18fb62b3d35b2a3b10ac05 config_hash: 1fbc2955725b2b29a941f009ff2a2ce9 diff --git a/instance.go b/instance.go index 349506f..9a82de3 100644 --- a/instance.go +++ b/instance.go @@ -557,6 +557,11 @@ type InstanceNewParams struct { // Override image CMD (like docker run ). Omit to use image // default. Cmd []string `json:"cmd,omitzero"` + // Host-managed credential brokering policies keyed by guest-visible env var name. + // Those guest env vars receive mock placeholder values, while the real values + // remain host-scoped in the request `env` map and are only materialized on the + // mediated egress path according to each credential's `source` and `inject` rules. + Credentials map[string]InstanceNewParamsCredential `json:"credentials,omitzero"` // Device IDs or names to attach for GPU/PCI passthrough Devices []string `json:"devices,omitzero"` // Override image entrypoint (like docker run --entrypoint). Omit to use image @@ -587,6 +592,77 @@ func (r *InstanceNewParams) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// The properties Inject, Source are required. +type InstanceNewParamsCredential struct { + Inject []InstanceNewParamsCredentialInject `json:"inject,omitzero" api:"required"` + Source InstanceNewParamsCredentialSource `json:"source,omitzero" api:"required"` + paramObj +} + +func (r InstanceNewParamsCredential) MarshalJSON() (data []byte, err error) { + type shadow InstanceNewParamsCredential + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *InstanceNewParamsCredential) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The property As is required. +type InstanceNewParamsCredentialInject struct { + // Current v1 transform shape. Header templating is supported now; other transform + // types (for example request signing) can be added in future revisions. + As InstanceNewParamsCredentialInjectAs `json:"as,omitzero" api:"required"` + // Optional destination host patterns (`api.example.com`, `*.example.com`). Omit to + // allow injection on all destinations. + Hosts []string `json:"hosts,omitzero"` + paramObj +} + +func (r InstanceNewParamsCredentialInject) MarshalJSON() (data []byte, err error) { + type shadow InstanceNewParamsCredentialInject + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *InstanceNewParamsCredentialInject) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Current v1 transform shape. Header templating is supported now; other transform +// types (for example request signing) can be added in future revisions. +// +// The properties Format, Header are required. +type InstanceNewParamsCredentialInjectAs struct { + // Template that must include `${value}`. + Format string `json:"format" api:"required"` + // Header name to set/mutate for matching outbound requests. + Header string `json:"header" api:"required"` + paramObj +} + +func (r InstanceNewParamsCredentialInjectAs) MarshalJSON() (data []byte, err error) { + type shadow InstanceNewParamsCredentialInjectAs + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *InstanceNewParamsCredentialInjectAs) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The property Env is required. +type InstanceNewParamsCredentialSource struct { + // Name of the real credential in the request `env` map. The guest-visible env var + // key can receive a mock placeholder, while the mediated egress path resolves that + // placeholder back to this real value only on the host. + Env string `json:"env" api:"required"` + paramObj +} + +func (r InstanceNewParamsCredentialSource) MarshalJSON() (data []byte, err error) { + type shadow InstanceNewParamsCredentialSource + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *InstanceNewParamsCredentialSource) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + // GPU configuration for the instance type InstanceNewParamsGPU struct { // vGPU profile name (e.g., "L40S-1Q"). Only used in vGPU mode. @@ -622,6 +698,10 @@ type InstanceNewParamsNetwork struct { BandwidthUpload param.Opt[string] `json:"bandwidth_upload,omitzero"` // Whether to attach instance to the default network Enabled param.Opt[bool] `json:"enabled,omitzero"` + // Host-mediated outbound network policy. Omit this object, or set + // `enabled: false`, to preserve normal direct outbound networking when + // `network.enabled` is true. + Egress InstanceNewParamsNetworkEgress `json:"egress,omitzero"` paramObj } @@ -633,6 +713,51 @@ func (r *InstanceNewParamsNetwork) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// Host-mediated outbound network policy. Omit this object, or set +// `enabled: false`, to preserve normal direct outbound networking when +// `network.enabled` is true. +type InstanceNewParamsNetworkEgress struct { + // Whether to enable the mediated egress path. When false or omitted, the instance + // keeps normal direct outbound networking and host-managed credential rewriting is + // disabled. + Enabled param.Opt[bool] `json:"enabled,omitzero"` + // Egress enforcement policy applied when mediation is enabled. + Enforcement InstanceNewParamsNetworkEgressEnforcement `json:"enforcement,omitzero"` + paramObj +} + +func (r InstanceNewParamsNetworkEgress) MarshalJSON() (data []byte, err error) { + type shadow InstanceNewParamsNetworkEgress + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *InstanceNewParamsNetworkEgress) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Egress enforcement policy applied when mediation is enabled. +type InstanceNewParamsNetworkEgressEnforcement struct { + // `all` (default) rejects direct non-mediated TCP egress from the VM, while + // `http_https_only` rejects direct egress only on TCP ports 80 and 443. + // + // Any of "all", "http_https_only". + Mode string `json:"mode,omitzero"` + paramObj +} + +func (r InstanceNewParamsNetworkEgressEnforcement) MarshalJSON() (data []byte, err error) { + type shadow InstanceNewParamsNetworkEgressEnforcement + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *InstanceNewParamsNetworkEgressEnforcement) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +func init() { + apijson.RegisterFieldValidator[InstanceNewParamsNetworkEgressEnforcement]( + "mode", "all", "http_https_only", + ) +} + type InstanceListParams struct { // Filter instances by state (e.g., Running, Stopped) // diff --git a/instance_test.go b/instance_test.go index deb108e..69489f0 100644 --- a/instance_test.go +++ b/instance_test.go @@ -27,9 +27,23 @@ func TestInstanceNewWithOptionalParams(t *testing.T) { option.WithAPIKey("My API Key"), ) _, err := client.Instances.New(context.TODO(), hypeman.InstanceNewParams{ - Image: "docker.io/library/alpine:latest", - Name: "my-workload-1", - Cmd: []string{"echo", "hello"}, + Image: "docker.io/library/alpine:latest", + Name: "my-workload-1", + Cmd: []string{"echo", "hello"}, + Credentials: map[string]hypeman.InstanceNewParamsCredential{ + "OUTBOUND_OPENAI_KEY": { + Inject: []hypeman.InstanceNewParamsCredentialInject{{ + As: hypeman.InstanceNewParamsCredentialInjectAs{ + Format: "Bearer ${value}", + Header: "Authorization", + }, + Hosts: []string{"api.openai.com", "*.openai.com"}, + }}, + Source: hypeman.InstanceNewParamsCredentialSource{ + Env: "OUTBOUND_OPENAI_KEY", + }, + }, + }, Devices: []string{"l4-gpu"}, DiskIoBps: hypeman.String("100MB/s"), Entrypoint: []string{"/bin/sh", "-c"}, @@ -45,7 +59,13 @@ func TestInstanceNewWithOptionalParams(t *testing.T) { Network: hypeman.InstanceNewParamsNetwork{ BandwidthDownload: hypeman.String("1Gbps"), BandwidthUpload: hypeman.String("1Gbps"), - Enabled: hypeman.Bool(true), + Egress: hypeman.InstanceNewParamsNetworkEgress{ + Enabled: hypeman.Bool(true), + Enforcement: hypeman.InstanceNewParamsNetworkEgressEnforcement{ + Mode: "all", + }, + }, + Enabled: hypeman.Bool(true), }, OverlaySize: hypeman.String("20GB"), Size: hypeman.String("2GB"), From 96b32092d707499c6a962d89a3530b413be4ccab Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:27:28 +0000 Subject: [PATCH 13/16] feat: support updating egress proxy secret envs for key rotation --- .stats.yml | 8 ++++---- api.md | 1 + instance.go | 30 ++++++++++++++++++++++++++++++ instance_test.go | 31 +++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index f269291..3c93d33 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 45 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-84cb170b2dc7bee2d94ec86519a050da932bd0d49cbc5ebded23d463adeafbb8.yml -openapi_spec_hash: dcd84edb5d18fb62b3d35b2a3b10ac05 -config_hash: 1fbc2955725b2b29a941f009ff2a2ce9 +configured_endpoints: 46 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-c5a7803a10e64d5a80acfadd94238643632466951c8fce9e5d2e390f3516606d.yml +openapi_spec_hash: 9537a4ce57ba747d1eab58fadc22abe6 +config_hash: 3921a3bc5d9ec653d98c2e50a16985f6 diff --git a/api.md b/api.md index 55b2c39..ccf178e 100644 --- a/api.md +++ b/api.md @@ -37,6 +37,7 @@ Response Types: Methods: - client.Instances.New(ctx context.Context, body hypeman.InstanceNewParams) (\*hypeman.Instance, error) +- client.Instances.Update(ctx context.Context, id string, body hypeman.InstanceUpdateParams) (\*hypeman.Instance, error) - client.Instances.List(ctx context.Context, query hypeman.InstanceListParams) (\*[]hypeman.Instance, error) - client.Instances.Delete(ctx context.Context, id string) error - client.Instances.Fork(ctx context.Context, id string, body hypeman.InstanceForkParams) (\*hypeman.Instance, error) diff --git a/instance.go b/instance.go index 9a82de3..ac23d3d 100644 --- a/instance.go +++ b/instance.go @@ -52,6 +52,20 @@ func (r *InstanceService) New(ctx context.Context, body InstanceNewParams, opts return res, err } +// Update mutable properties of a running instance. Currently supports updating +// only the environment variables referenced by existing credential policies, +// enabling secret/key rotation without instance restart. +func (r *InstanceService) Update(ctx context.Context, id string, body InstanceUpdateParams, opts ...option.RequestOption) (res *Instance, err error) { + opts = slices.Concat(r.Options, opts) + if id == "" { + err = errors.New("missing required id parameter") + return nil, err + } + path := fmt.Sprintf("instances/%s", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPatch, path, body, &res, opts...) + return res, err +} + // List instances func (r *InstanceService) List(ctx context.Context, query InstanceListParams, opts ...option.RequestOption) (res *[]Instance, err error) { opts = slices.Concat(r.Options, opts) @@ -758,6 +772,22 @@ func init() { ) } +type InstanceUpdateParams struct { + // Environment variables to update (merged with existing). Only keys referenced by + // the instance's existing credential `source.env` bindings are accepted. Use this + // to rotate real credential values without restarting the VM. + Env map[string]string `json:"env,omitzero"` + paramObj +} + +func (r InstanceUpdateParams) MarshalJSON() (data []byte, err error) { + type shadow InstanceUpdateParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *InstanceUpdateParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type InstanceListParams struct { // Filter instances by state (e.g., Running, Stopped) // diff --git a/instance_test.go b/instance_test.go index 69489f0..47fcdef 100644 --- a/instance_test.go +++ b/instance_test.go @@ -93,6 +93,37 @@ func TestInstanceNewWithOptionalParams(t *testing.T) { } } +func TestInstanceUpdateWithOptionalParams(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := hypeman.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Instances.Update( + context.TODO(), + "id", + hypeman.InstanceUpdateParams{ + Env: map[string]string{ + "OUTBOUND_OPENAI_KEY": "new-rotated-key-456", + }, + }, + ) + if err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + func TestInstanceListWithOptionalParams(t *testing.T) { t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" From b8ecb541b8237d1283a05422eed6e30ff2b0536f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:46:43 +0000 Subject: [PATCH 14/16] feat: add active ballooning reclaim controller --- .stats.yml | 8 +-- api.md | 7 +++ resource.go | 133 +++++++++++++++++++++++++++++++++++++++++++++++ resource_test.go | 30 +++++++++++ 4 files changed, 174 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3c93d33..fefa738 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 46 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-c5a7803a10e64d5a80acfadd94238643632466951c8fce9e5d2e390f3516606d.yml -openapi_spec_hash: 9537a4ce57ba747d1eab58fadc22abe6 -config_hash: 3921a3bc5d9ec653d98c2e50a16985f6 +configured_endpoints: 47 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-00d03cbc91225f6b74d241a6c75f647d48ff53a04c1feaa76b9566708c7d189a.yml +openapi_spec_hash: 499b4986f927b5d65563f13c351faaa6 +config_hash: 2d3d0f2a9622044a1321bb92ef474188 diff --git a/api.md b/api.md index ccf178e..99b2fc8 100644 --- a/api.md +++ b/api.md @@ -137,11 +137,17 @@ Methods: # Resources +Params Types: + +- hypeman.MemoryReclaimRequestParam + Response Types: - hypeman.DiskBreakdown - hypeman.GPUProfile - hypeman.GPUResourceStatus +- hypeman.MemoryReclaimAction +- hypeman.MemoryReclaimResponse - hypeman.PassthroughDevice - hypeman.ResourceAllocation - hypeman.ResourceStatus @@ -150,6 +156,7 @@ Response Types: Methods: - client.Resources.Get(ctx context.Context) (\*hypeman.Resources, error) +- client.Resources.ReclaimMemory(ctx context.Context, body hypeman.ResourceReclaimMemoryParams) (\*hypeman.MemoryReclaimResponse, error) # Builds diff --git a/resource.go b/resource.go index f8ba67d..661648a 100644 --- a/resource.go +++ b/resource.go @@ -4,12 +4,16 @@ package hypeman import ( "context" + "encoding/json" "net/http" "slices" + "time" "github.com/kernel/hypeman-go/internal/apijson" + shimjson "github.com/kernel/hypeman-go/internal/encoding/json" "github.com/kernel/hypeman-go/internal/requestconfig" "github.com/kernel/hypeman-go/option" + "github.com/kernel/hypeman-go/packages/param" "github.com/kernel/hypeman-go/packages/respjson" ) @@ -42,6 +46,16 @@ func (r *ResourceService) Get(ctx context.Context, opts ...option.RequestOption) return res, err } +// Requests runtime balloon inflation across reclaim-eligible guests. The same +// planner used by host-pressure reclaim is applied, including protected floors and +// per-VM step limits. +func (r *ResourceService) ReclaimMemory(ctx context.Context, body ResourceReclaimMemoryParams, opts ...option.RequestOption) (res *MemoryReclaimResponse, err error) { + opts = slices.Concat(r.Options, opts) + path := "resources/memory/reclaim" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return res, err +} + type DiskBreakdown struct { // Disk used by exported rootfs images ImagesBytes int64 `json:"images_bytes"` @@ -132,6 +146,113 @@ const ( GPUResourceStatusModePassthrough GPUResourceStatusMode = "passthrough" ) +type MemoryReclaimAction struct { + AppliedReclaimBytes int64 `json:"applied_reclaim_bytes" api:"required"` + AssignedMemoryBytes int64 `json:"assigned_memory_bytes" api:"required"` + // Any of "cloud-hypervisor", "firecracker", "qemu", "vz". + Hypervisor MemoryReclaimActionHypervisor `json:"hypervisor" api:"required"` + InstanceID string `json:"instance_id" api:"required"` + InstanceName string `json:"instance_name" api:"required"` + PlannedTargetGuestMemoryBytes int64 `json:"planned_target_guest_memory_bytes" api:"required"` + PreviousTargetGuestMemoryBytes int64 `json:"previous_target_guest_memory_bytes" api:"required"` + ProtectedFloorBytes int64 `json:"protected_floor_bytes" api:"required"` + // Result of this VM's reclaim step. + Status string `json:"status" api:"required"` + TargetGuestMemoryBytes int64 `json:"target_guest_memory_bytes" api:"required"` + // Error message when status is error or unsupported. + Error string `json:"error"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + AppliedReclaimBytes respjson.Field + AssignedMemoryBytes respjson.Field + Hypervisor respjson.Field + InstanceID respjson.Field + InstanceName respjson.Field + PlannedTargetGuestMemoryBytes respjson.Field + PreviousTargetGuestMemoryBytes respjson.Field + ProtectedFloorBytes respjson.Field + Status respjson.Field + TargetGuestMemoryBytes respjson.Field + Error respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r MemoryReclaimAction) RawJSON() string { return r.JSON.raw } +func (r *MemoryReclaimAction) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type MemoryReclaimActionHypervisor string + +const ( + MemoryReclaimActionHypervisorCloudHypervisor MemoryReclaimActionHypervisor = "cloud-hypervisor" + MemoryReclaimActionHypervisorFirecracker MemoryReclaimActionHypervisor = "firecracker" + MemoryReclaimActionHypervisorQemu MemoryReclaimActionHypervisor = "qemu" + MemoryReclaimActionHypervisorVz MemoryReclaimActionHypervisor = "vz" +) + +// The property ReclaimBytes is required. +type MemoryReclaimRequestParam struct { + // Total bytes of guest memory to reclaim across eligible VMs. + ReclaimBytes int64 `json:"reclaim_bytes" api:"required"` + // Calculate a reclaim plan without applying balloon changes or creating a hold. + DryRun param.Opt[bool] `json:"dry_run,omitzero"` + // How long to keep the reclaim hold active (Go duration string). Defaults to 5m + // when omitted. + HoldFor param.Opt[string] `json:"hold_for,omitzero"` + // Optional operator-provided reason attached to logs and traces. + Reason param.Opt[string] `json:"reason,omitzero"` + paramObj +} + +func (r MemoryReclaimRequestParam) MarshalJSON() (data []byte, err error) { + type shadow MemoryReclaimRequestParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *MemoryReclaimRequestParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type MemoryReclaimResponse struct { + Actions []MemoryReclaimAction `json:"actions" api:"required"` + AppliedReclaimBytes int64 `json:"applied_reclaim_bytes" api:"required"` + HostAvailableBytes int64 `json:"host_available_bytes" api:"required"` + // Any of "healthy", "pressure". + HostPressureState MemoryReclaimResponseHostPressureState `json:"host_pressure_state" api:"required"` + PlannedReclaimBytes int64 `json:"planned_reclaim_bytes" api:"required"` + RequestedReclaimBytes int64 `json:"requested_reclaim_bytes" api:"required"` + // When the current manual reclaim hold expires. + HoldUntil time.Time `json:"hold_until" format:"date-time"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Actions respjson.Field + AppliedReclaimBytes respjson.Field + HostAvailableBytes respjson.Field + HostPressureState respjson.Field + PlannedReclaimBytes respjson.Field + RequestedReclaimBytes respjson.Field + HoldUntil respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r MemoryReclaimResponse) RawJSON() string { return r.JSON.raw } +func (r *MemoryReclaimResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type MemoryReclaimResponseHostPressureState string + +const ( + MemoryReclaimResponseHostPressureStateHealthy MemoryReclaimResponseHostPressureState = "healthy" + MemoryReclaimResponseHostPressureStatePressure MemoryReclaimResponseHostPressureState = "pressure" +) + // Physical GPU available for passthrough type PassthroughDevice struct { // Whether this GPU is available (not attached to an instance) @@ -256,3 +377,15 @@ func (r Resources) RawJSON() string { return r.JSON.raw } func (r *Resources) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } + +type ResourceReclaimMemoryParams struct { + MemoryReclaimRequest MemoryReclaimRequestParam + paramObj +} + +func (r ResourceReclaimMemoryParams) MarshalJSON() (data []byte, err error) { + return shimjson.Marshal(r.MemoryReclaimRequest) +} +func (r *ResourceReclaimMemoryParams) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &r.MemoryReclaimRequest) +} diff --git a/resource_test.go b/resource_test.go index 47a0603..97d280f 100644 --- a/resource_test.go +++ b/resource_test.go @@ -35,3 +35,33 @@ func TestResourceGet(t *testing.T) { t.Fatalf("err should be nil: %s", err.Error()) } } + +func TestResourceReclaimMemoryWithOptionalParams(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := hypeman.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Resources.ReclaimMemory(context.TODO(), hypeman.ResourceReclaimMemoryParams{ + MemoryReclaimRequest: hypeman.MemoryReclaimRequestParam{ + ReclaimBytes: 536870912, + DryRun: hypeman.Bool(true), + HoldFor: hypeman.String("5m"), + Reason: hypeman.String("prepare for another vm start"), + }, + }) + if err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} From b6d9ab3d0b4ffdd7d56153464387ed9e603d8d0a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:10:56 +0000 Subject: [PATCH 15/16] feat: Add optional snapshot compression defaults and standby integration --- .stats.yml | 6 +-- aliases.go | 19 +++++++ api.md | 12 ++++- instance.go | 111 +++++++++++++++++++++++++++++---------- instance_test.go | 22 +++++++- instancesnapshot.go | 4 ++ instancesnapshot_test.go | 6 +++ shared/shared.go | 86 ++++++++++++++++++++++++++++++ snapshot.go | 49 +++++++++++++---- 9 files changed, 271 insertions(+), 44 deletions(-) create mode 100644 shared/shared.go diff --git a/.stats.yml b/.stats.yml index fefa738..4d77a66 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 47 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-00d03cbc91225f6b74d241a6c75f647d48ff53a04c1feaa76b9566708c7d189a.yml -openapi_spec_hash: 499b4986f927b5d65563f13c351faaa6 -config_hash: 2d3d0f2a9622044a1321bb92ef474188 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-8251e13b6a8ec8965b5cb0b5ab33d4a16e274a4be7cd6d9fa36642878108797c.yml +openapi_spec_hash: 5c1c0d21d430074ffa76ae62ea137f0b +config_hash: 47cce606a7f8af4dac9c2a8dbc822484 diff --git a/aliases.go b/aliases.go index 1eefe6d..1f5b004 100644 --- a/aliases.go +++ b/aliases.go @@ -5,6 +5,7 @@ package hypeman import ( "github.com/kernel/hypeman-go/internal/apierror" "github.com/kernel/hypeman-go/packages/param" + "github.com/kernel/hypeman-go/shared" ) // aliased to make [param.APIUnion] private when embedding @@ -14,3 +15,21 @@ type paramUnion = param.APIUnion type paramObj = param.APIObject type Error = apierror.Error + +// This is an alias to an internal type. +type SnapshotCompressionConfig = shared.SnapshotCompressionConfig + +// Compression algorithm (defaults to zstd when enabled). Ignored when enabled is +// false. +// +// This is an alias to an internal type. +type SnapshotCompressionConfigAlgorithm = shared.SnapshotCompressionConfigAlgorithm + +// Equals "zstd" +const SnapshotCompressionConfigAlgorithmZstd = shared.SnapshotCompressionConfigAlgorithmZstd + +// Equals "lz4" +const SnapshotCompressionConfigAlgorithmLz4 = shared.SnapshotCompressionConfigAlgorithmLz4 + +// This is an alias to an internal type. +type SnapshotCompressionConfigParam = shared.SnapshotCompressionConfigParam diff --git a/api.md b/api.md index 99b2fc8..01ab836 100644 --- a/api.md +++ b/api.md @@ -1,3 +1,11 @@ +# Shared Params Types + +- shared.SnapshotCompressionConfigParam + +# Shared Response Types + +- shared.SnapshotCompressionConfig + # Health Response Types: @@ -25,6 +33,7 @@ Methods: Params Types: +- hypeman.SnapshotPolicyParam - hypeman.VolumeMountParam Response Types: @@ -32,6 +41,7 @@ Response Types: - hypeman.Instance - hypeman.InstanceStats - hypeman.PathInfo +- hypeman.SnapshotPolicy - hypeman.VolumeMount Methods: @@ -44,7 +54,7 @@ Methods: - client.Instances.Get(ctx context.Context, id string) (\*hypeman.Instance, error) - client.Instances.Logs(ctx context.Context, id string, query hypeman.InstanceLogsParams) (\*string, error) - client.Instances.Restore(ctx context.Context, id string) (\*hypeman.Instance, error) -- client.Instances.Standby(ctx context.Context, id string) (\*hypeman.Instance, error) +- client.Instances.Standby(ctx context.Context, id string, body hypeman.InstanceStandbyParams) (\*hypeman.Instance, error) - client.Instances.Start(ctx context.Context, id string, body hypeman.InstanceStartParams) (\*hypeman.Instance, error) - client.Instances.Stat(ctx context.Context, id string, query hypeman.InstanceStatParams) (\*hypeman.PathInfo, error) - client.Instances.Stats(ctx context.Context, id string) (\*hypeman.InstanceStats, error) diff --git a/instance.go b/instance.go index ac23d3d..a2c146f 100644 --- a/instance.go +++ b/instance.go @@ -19,6 +19,7 @@ import ( "github.com/kernel/hypeman-go/packages/param" "github.com/kernel/hypeman-go/packages/respjson" "github.com/kernel/hypeman-go/packages/ssestream" + "github.com/kernel/hypeman-go/shared" ) // InstanceService contains methods and other services that help with interacting @@ -149,14 +150,14 @@ func (r *InstanceService) Restore(ctx context.Context, id string, opts ...option } // Put instance in standby (pause, snapshot, delete VMM) -func (r *InstanceService) Standby(ctx context.Context, id string, opts ...option.RequestOption) (res *Instance, err error) { +func (r *InstanceService) Standby(ctx context.Context, id string, body InstanceStandbyParams, opts ...option.RequestOption) (res *Instance, err error) { opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") return nil, err } path := fmt.Sprintf("instances/%s/standby", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) return res, err } @@ -258,7 +259,8 @@ type Instance struct { // Writable overlay disk size (human-readable) OverlaySize string `json:"overlay_size"` // Base memory size (human-readable) - Size string `json:"size"` + Size string `json:"size"` + SnapshotPolicy SnapshotPolicy `json:"snapshot_policy"` // Start timestamp (RFC3339) StartedAt time.Time `json:"started_at" api:"nullable" format:"date-time"` // Error message if state couldn't be determined (only set when state is Unknown) @@ -273,30 +275,31 @@ type Instance struct { Volumes []VolumeMount `json:"volumes"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - ID respjson.Field - CreatedAt respjson.Field - Image respjson.Field - Name respjson.Field - State respjson.Field - DiskIoBps respjson.Field - Env respjson.Field - ExitCode respjson.Field - ExitMessage respjson.Field - GPU respjson.Field - HasSnapshot respjson.Field - HotplugSize respjson.Field - Hypervisor respjson.Field - Network respjson.Field - OverlaySize respjson.Field - Size respjson.Field - StartedAt respjson.Field - StateError respjson.Field - StoppedAt respjson.Field - Tags respjson.Field - Vcpus respjson.Field - Volumes respjson.Field - ExtraFields map[string]respjson.Field - raw string + ID respjson.Field + CreatedAt respjson.Field + Image respjson.Field + Name respjson.Field + State respjson.Field + DiskIoBps respjson.Field + Env respjson.Field + ExitCode respjson.Field + ExitMessage respjson.Field + GPU respjson.Field + HasSnapshot respjson.Field + HotplugSize respjson.Field + Hypervisor respjson.Field + Network respjson.Field + OverlaySize respjson.Field + Size respjson.Field + SnapshotPolicy respjson.Field + StartedAt respjson.Field + StateError respjson.Field + StoppedAt respjson.Field + Tags respjson.Field + Vcpus respjson.Field + Volumes respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } @@ -478,6 +481,44 @@ func (r *PathInfo) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +type SnapshotPolicy struct { + Compression shared.SnapshotCompressionConfig `json:"compression"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Compression respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r SnapshotPolicy) RawJSON() string { return r.JSON.raw } +func (r *SnapshotPolicy) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// ToParam converts this SnapshotPolicy to a SnapshotPolicyParam. +// +// Warning: the fields of the param type will not be present. ToParam should only +// be used at the last possible moment before sending a request. Test for this with +// SnapshotPolicyParam.Overrides() +func (r SnapshotPolicy) ToParam() SnapshotPolicyParam { + return param.Override[SnapshotPolicyParam](json.RawMessage(r.RawJSON())) +} + +type SnapshotPolicyParam struct { + Compression shared.SnapshotCompressionConfigParam `json:"compression,omitzero"` + paramObj +} + +func (r SnapshotPolicyParam) MarshalJSON() (data []byte, err error) { + type shadow SnapshotPolicyParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *SnapshotPolicyParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type VolumeMount struct { // Path where volume is mounted in the guest MountPath string `json:"mount_path" api:"required"` @@ -591,6 +632,9 @@ type InstanceNewParams struct { Hypervisor InstanceNewParamsHypervisor `json:"hypervisor,omitzero"` // Network configuration for the instance Network InstanceNewParamsNetwork `json:"network,omitzero"` + // Snapshot compression policy for this instance. Controls compression settings + // applied when creating snapshots or entering standby. + SnapshotPolicy SnapshotPolicyParam `json:"snapshot_policy,omitzero"` // User-defined key-value tags. Tags map[string]string `json:"tags,omitzero"` // Volumes to attach to the instance at creation time @@ -895,6 +939,19 @@ const ( InstanceLogsParamsSourceHypeman InstanceLogsParamsSource = "hypeman" ) +type InstanceStandbyParams struct { + Compression shared.SnapshotCompressionConfigParam `json:"compression,omitzero"` + paramObj +} + +func (r InstanceStandbyParams) MarshalJSON() (data []byte, err error) { + type shadow InstanceStandbyParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *InstanceStandbyParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type InstanceStartParams struct { // Override image CMD for this run. Omit to keep previous value. Cmd []string `json:"cmd,omitzero"` diff --git a/instance_test.go b/instance_test.go index 47fcdef..be89cd7 100644 --- a/instance_test.go +++ b/instance_test.go @@ -11,6 +11,7 @@ import ( "github.com/kernel/hypeman-go" "github.com/kernel/hypeman-go/internal/testutil" "github.com/kernel/hypeman-go/option" + "github.com/kernel/hypeman-go/shared" ) func TestInstanceNewWithOptionalParams(t *testing.T) { @@ -71,6 +72,13 @@ func TestInstanceNewWithOptionalParams(t *testing.T) { Size: hypeman.String("2GB"), SkipGuestAgent: hypeman.Bool(false), SkipKernelHeaders: hypeman.Bool(true), + SnapshotPolicy: hypeman.SnapshotPolicyParam{ + Compression: shared.SnapshotCompressionConfigParam{ + Enabled: true, + Algorithm: shared.SnapshotCompressionConfigAlgorithmZstd, + Level: hypeman.Int(1), + }, + }, Tags: map[string]string{ "team": "backend", "env": "staging", @@ -253,7 +261,7 @@ func TestInstanceRestore(t *testing.T) { } } -func TestInstanceStandby(t *testing.T) { +func TestInstanceStandbyWithOptionalParams(t *testing.T) { t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { @@ -266,7 +274,17 @@ func TestInstanceStandby(t *testing.T) { option.WithBaseURL(baseURL), option.WithAPIKey("My API Key"), ) - _, err := client.Instances.Standby(context.TODO(), "id") + _, err := client.Instances.Standby( + context.TODO(), + "id", + hypeman.InstanceStandbyParams{ + Compression: shared.SnapshotCompressionConfigParam{ + Enabled: true, + Algorithm: shared.SnapshotCompressionConfigAlgorithmZstd, + Level: hypeman.Int(1), + }, + }, + ) if err != nil { var apierr *hypeman.Error if errors.As(err, &apierr) { diff --git a/instancesnapshot.go b/instancesnapshot.go index 889f833..d0aed99 100644 --- a/instancesnapshot.go +++ b/instancesnapshot.go @@ -13,6 +13,7 @@ import ( "github.com/kernel/hypeman-go/internal/requestconfig" "github.com/kernel/hypeman-go/option" "github.com/kernel/hypeman-go/packages/param" + "github.com/kernel/hypeman-go/shared" ) // InstanceSnapshotService contains methods and other services that help with @@ -70,6 +71,9 @@ type InstanceSnapshotNewParams struct { // Optional snapshot name (lowercase letters, digits, and dashes only; cannot start // or end with a dash) Name param.Opt[string] `json:"name,omitzero"` + // Compression settings to use for this snapshot. Overrides instance and server + // defaults. + Compression shared.SnapshotCompressionConfigParam `json:"compression,omitzero"` // User-defined key-value tags. Tags map[string]string `json:"tags,omitzero"` paramObj diff --git a/instancesnapshot_test.go b/instancesnapshot_test.go index 755f0e4..8424616 100644 --- a/instancesnapshot_test.go +++ b/instancesnapshot_test.go @@ -11,6 +11,7 @@ import ( "github.com/kernel/hypeman-go" "github.com/kernel/hypeman-go/internal/testutil" "github.com/kernel/hypeman-go/option" + "github.com/kernel/hypeman-go/shared" ) func TestInstanceSnapshotNewWithOptionalParams(t *testing.T) { @@ -31,6 +32,11 @@ func TestInstanceSnapshotNewWithOptionalParams(t *testing.T) { "id", hypeman.InstanceSnapshotNewParams{ Kind: hypeman.SnapshotKindStandby, + Compression: shared.SnapshotCompressionConfigParam{ + Enabled: true, + Algorithm: shared.SnapshotCompressionConfigAlgorithmZstd, + Level: hypeman.Int(1), + }, Name: hypeman.String("pre-upgrade"), Tags: map[string]string{ "team": "backend", diff --git a/shared/shared.go b/shared/shared.go new file mode 100644 index 0000000..4b9e9eb --- /dev/null +++ b/shared/shared.go @@ -0,0 +1,86 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package shared + +import ( + "encoding/json" + + "github.com/kernel/hypeman-go/internal/apijson" + "github.com/kernel/hypeman-go/packages/param" + "github.com/kernel/hypeman-go/packages/respjson" +) + +// aliased to make [param.APIUnion] private when embedding +type paramUnion = param.APIUnion + +// aliased to make [param.APIObject] private when embedding +type paramObj = param.APIObject + +type SnapshotCompressionConfig struct { + // Enable snapshot memory compression + Enabled bool `json:"enabled" api:"required"` + // Compression algorithm (defaults to zstd when enabled). Ignored when enabled is + // false. + // + // Any of "zstd", "lz4". + Algorithm SnapshotCompressionConfigAlgorithm `json:"algorithm"` + // Compression level. Allowed ranges are zstd=1-19 and lz4=0-9. When omitted, zstd + // defaults to 1 and lz4 defaults to 0. Ignored when enabled is false. + Level int64 `json:"level"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Enabled respjson.Field + Algorithm respjson.Field + Level respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r SnapshotCompressionConfig) RawJSON() string { return r.JSON.raw } +func (r *SnapshotCompressionConfig) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// ToParam converts this SnapshotCompressionConfig to a +// SnapshotCompressionConfigParam. +// +// Warning: the fields of the param type will not be present. ToParam should only +// be used at the last possible moment before sending a request. Test for this with +// SnapshotCompressionConfigParam.Overrides() +func (r SnapshotCompressionConfig) ToParam() SnapshotCompressionConfigParam { + return param.Override[SnapshotCompressionConfigParam](json.RawMessage(r.RawJSON())) +} + +// Compression algorithm (defaults to zstd when enabled). Ignored when enabled is +// false. +type SnapshotCompressionConfigAlgorithm string + +const ( + SnapshotCompressionConfigAlgorithmZstd SnapshotCompressionConfigAlgorithm = "zstd" + SnapshotCompressionConfigAlgorithmLz4 SnapshotCompressionConfigAlgorithm = "lz4" +) + +// The property Enabled is required. +type SnapshotCompressionConfigParam struct { + // Enable snapshot memory compression + Enabled bool `json:"enabled" api:"required"` + // Compression level. Allowed ranges are zstd=1-19 and lz4=0-9. When omitted, zstd + // defaults to 1 and lz4 defaults to 0. Ignored when enabled is false. + Level param.Opt[int64] `json:"level,omitzero"` + // Compression algorithm (defaults to zstd when enabled). Ignored when enabled is + // false. + // + // Any of "zstd", "lz4". + Algorithm SnapshotCompressionConfigAlgorithm `json:"algorithm,omitzero"` + paramObj +} + +func (r SnapshotCompressionConfigParam) MarshalJSON() (data []byte, err error) { + type shadow SnapshotCompressionConfigParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *SnapshotCompressionConfigParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} diff --git a/snapshot.go b/snapshot.go index 99fe8a7..f615439 100644 --- a/snapshot.go +++ b/snapshot.go @@ -17,6 +17,7 @@ import ( "github.com/kernel/hypeman-go/option" "github.com/kernel/hypeman-go/packages/param" "github.com/kernel/hypeman-go/packages/respjson" + "github.com/kernel/hypeman-go/shared" ) // SnapshotService contains methods and other services that help with interacting @@ -102,23 +103,39 @@ type Snapshot struct { SourceInstanceID string `json:"source_instance_id" api:"required"` // Source instance name at snapshot creation time SourceInstanceName string `json:"source_instance_name" api:"required"` + // Compressed memory payload size in bytes + CompressedSizeBytes int64 `json:"compressed_size_bytes" api:"nullable"` + Compression shared.SnapshotCompressionConfig `json:"compression"` + // Compression error message when compression_state is error + CompressionError string `json:"compression_error" api:"nullable"` + // Compression status of the snapshot payload memory file + // + // Any of "none", "compressing", "compressed", "error". + CompressionState SnapshotCompressionState `json:"compression_state"` // Optional human-readable snapshot name (unique per source instance) Name string `json:"name" api:"nullable"` // User-defined key-value tags. Tags map[string]string `json:"tags"` + // Uncompressed memory payload size in bytes + UncompressedSizeBytes int64 `json:"uncompressed_size_bytes" api:"nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - ID respjson.Field - CreatedAt respjson.Field - Kind respjson.Field - SizeBytes respjson.Field - SourceHypervisor respjson.Field - SourceInstanceID respjson.Field - SourceInstanceName respjson.Field - Name respjson.Field - Tags respjson.Field - ExtraFields map[string]respjson.Field - raw string + ID respjson.Field + CreatedAt respjson.Field + Kind respjson.Field + SizeBytes respjson.Field + SourceHypervisor respjson.Field + SourceInstanceID respjson.Field + SourceInstanceName respjson.Field + CompressedSizeBytes respjson.Field + Compression respjson.Field + CompressionError respjson.Field + CompressionState respjson.Field + Name respjson.Field + Tags respjson.Field + UncompressedSizeBytes respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } @@ -138,6 +155,16 @@ const ( SnapshotSourceHypervisorVz SnapshotSourceHypervisor = "vz" ) +// Compression status of the snapshot payload memory file +type SnapshotCompressionState string + +const ( + SnapshotCompressionStateNone SnapshotCompressionState = "none" + SnapshotCompressionStateCompressing SnapshotCompressionState = "compressing" + SnapshotCompressionStateCompressed SnapshotCompressionState = "compressed" + SnapshotCompressionStateError SnapshotCompressionState = "error" +) + // Snapshot capture kind type SnapshotKind string From 5c811cd092924097f804838cac43aa38655f3825 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:11:18 +0000 Subject: [PATCH 16/16] release: 0.16.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 26 ++++++++++++++++++++++++++ README.md | 2 +- internal/version.go | 2 +- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8f3e0a4..b4e9013 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.15.0" + ".": "0.16.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 646e8f4..4e2ab44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## 0.16.0 (2026-03-23) + +Full Changelog: [v0.15.0...v0.16.0](https://github.com/kernel/hypeman-go/compare/v0.15.0...v0.16.0) + +### Features + +* add active ballooning reclaim controller ([b8ecb54](https://github.com/kernel/hypeman-go/commit/b8ecb541b8237d1283a05422eed6e30ff2b0536f)) +* Add always-on /metrics endpoint with dual pull/push telemetry ([0b3751a](https://github.com/kernel/hypeman-go/commit/0b3751a37068dca3771ad04fc1af876c5e52384f)) +* Add optional snapshot compression defaults and standby integration ([b6d9ab3](https://github.com/kernel/hypeman-go/commit/b6d9ab3d0b4ffdd7d56153464387ed9e603d8d0a)) +* add optional VM egress MITM proxy with mock-secret header rewriting ([e8b721c](https://github.com/kernel/hypeman-go/commit/e8b721cbc709af739827929839e443981d636233)) +* Add strict metadata tags across mutable resources ([8b5543e](https://github.com/kernel/hypeman-go/commit/8b5543e6f8fed7062d28c37fa35ba031e14c6f79)) +* Rename tag fields from metadata to tags ([2f8e29e](https://github.com/kernel/hypeman-go/commit/2f8e29e033b44a8a492ec9a3bdcbc44cabf2eb11)) +* Snapshot ([c4a0fbb](https://github.com/kernel/hypeman-go/commit/c4a0fbb34ee2b325229f6a02ae9cc3c57346788d)) +* support updating egress proxy secret envs for key rotation ([96b3209](https://github.com/kernel/hypeman-go/commit/96b32092d707499c6a962d89a3530b413be4ccab)) + + +### Chores + +* **ci:** skip uploading artifacts on stainless-internal branches ([0d9d654](https://github.com/kernel/hypeman-go/commit/0d9d654f8074387de55f5befe709477c6905deaa)) +* **internal:** codegen related update ([e6a6702](https://github.com/kernel/hypeman-go/commit/e6a6702ba1fcf355cf64a60fd016393e5474e9bc)) +* **internal:** minor cleanup ([1917d6d](https://github.com/kernel/hypeman-go/commit/1917d6de179a6a36358598cf3b1f6d4ebbe7f034)) +* **internal:** tweak CI branches ([6dc7e68](https://github.com/kernel/hypeman-go/commit/6dc7e68725e65b75bd7c2aa07d4427909ad2365c)) +* **internal:** use explicit returns ([923db74](https://github.com/kernel/hypeman-go/commit/923db74ba5750233d429a5e98dc6cb5fb0e806ba)) +* **internal:** use explicit returns in more places ([16131b8](https://github.com/kernel/hypeman-go/commit/16131b802aa3f5a2b32d298d179966a440f53af5)) +* update placeholder string ([bea84ac](https://github.com/kernel/hypeman-go/commit/bea84ac19f1178f378cc6ba8bad07a26848b5492)) + ## 0.15.0 (2026-03-04) Full Changelog: [v0.14.0...v0.15.0](https://github.com/kernel/hypeman-go/compare/v0.14.0...v0.15.0) diff --git a/README.md b/README.md index 47f44f5..62dd1ed 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Or to pin the version: ```sh -go get -u 'github.com/kernel/hypeman-go@v0.15.0' +go get -u 'github.com/kernel/hypeman-go@v0.16.0' ``` diff --git a/internal/version.go b/internal/version.go index 1f338c3..16d4c3d 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.15.0" // x-release-please-version +const PackageVersion = "0.16.0" // x-release-please-version