From af678e8c794307a6bd47476acff3ca42a7a52546 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:23:18 +0000 Subject: [PATCH 1/2] feat: Use resources module for input validation --- .stats.yml | 8 +++---- api.md | 1 + resource.go | 5 +++++ volume.go | 59 +++++++++++++++++++++++++++++++++++++++++++++----- volume_test.go | 33 ++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 9 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5e112da..54f52d3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 36 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-fbe706932aedef16a8bb04fe361b1e8659ae074ff9122f4e9e28914087a677b8.yml -openapi_spec_hash: 6ba4858b44cb317676c58f33bfa1426a -config_hash: 542144891263a3626eabcb2fd4f4f3e5 +configured_endpoints: 37 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-cf9805637aaf9bf590b5ebc1e7430414850aff0c8ad727a017df290ea8df9913.yml +openapi_spec_hash: e27144cf0b24dc74fbdb77d8e24d818f +config_hash: d67a314ba1fda8242041000fc9160e92 diff --git a/api.md b/api.md index 4255a7d..c73bfa1 100644 --- a/api.md +++ b/api.md @@ -65,6 +65,7 @@ Methods: - client.Volumes.New(ctx context.Context, body hypeman.VolumeNewParams) (\*hypeman.Volume, error) - client.Volumes.List(ctx context.Context) (\*[]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) # Devices diff --git a/resource.go b/resource.go index 7806701..ef6dd47 100644 --- a/resource.go +++ b/resource.go @@ -158,6 +158,8 @@ type ResourceAllocation struct { CPU int64 `json:"cpu"` // Disk allocated in bytes (overlay + volumes) DiskBytes int64 `json:"disk_bytes"` + // Disk I/O bandwidth limit in bytes/sec + DiskIoBps int64 `json:"disk_io_bps"` // Instance identifier InstanceID string `json:"instance_id"` // Instance name @@ -172,6 +174,7 @@ type ResourceAllocation struct { JSON struct { CPU respjson.Field DiskBytes respjson.Field + DiskIoBps respjson.Field InstanceID respjson.Field InstanceName respjson.Field MemoryBytes respjson.Field @@ -230,6 +233,7 @@ type Resources struct { Memory ResourceStatus `json:"memory,required"` Network ResourceStatus `json:"network,required"` DiskBreakdown DiskBreakdown `json:"disk_breakdown"` + DiskIo ResourceStatus `json:"disk_io"` // GPU resource status. Null if no GPUs available. GPU GPUResourceStatus `json:"gpu,nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. @@ -240,6 +244,7 @@ type Resources struct { Memory respjson.Field Network respjson.Field DiskBreakdown respjson.Field + DiskIo respjson.Field GPU respjson.Field ExtraFields map[string]respjson.Field raw string diff --git a/volume.go b/volume.go index 75992b3..1951b76 100644 --- a/volume.go +++ b/volume.go @@ -3,14 +3,20 @@ package hypeman import ( + "bytes" "context" "errors" "fmt" + "io" + "mime/multipart" "net/http" + "net/url" "slices" "time" + "github.com/kernel/hypeman-go/internal/apiform" "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" @@ -36,11 +42,7 @@ func NewVolumeService(opts ...option.RequestOption) (r VolumeService) { return } -// Creates a new volume. Supports two modes: -// -// - JSON body: Creates an empty volume of the specified size -// - Multipart form: Creates a volume pre-populated with content from a tar.gz -// archive +// Creates a new empty volume of the specified size. func (r *VolumeService) New(ctx context.Context, body VolumeNewParams, opts ...option.RequestOption) (res *Volume, err error) { opts = slices.Concat(r.Options, opts) path := "volumes" @@ -69,6 +71,16 @@ func (r *VolumeService) Delete(ctx context.Context, id string, opts ...option.Re return } +// Creates a new volume pre-populated with content from a tar.gz archive. The +// archive is streamed directly into the volume's root directory. +func (r *VolumeService) NewFromArchive(ctx context.Context, body io.Reader, params VolumeNewFromArchiveParams, opts ...option.RequestOption) (res *Volume, err error) { + opts = slices.Concat(r.Options, opts) + opts = append([]option.RequestOption{option.WithRequestBody("application/gzip", body)}, opts...) + path := "volumes/from-archive" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) + return +} + // Get volume details func (r *VolumeService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Volume, err error) { opts = slices.Concat(r.Options, opts) @@ -150,3 +162,40 @@ func (r VolumeNewParams) MarshalJSON() (data []byte, err error) { func (r *VolumeNewParams) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } + +type VolumeNewFromArchiveParams struct { + // Volume name + Name string `query:"name,required" json:"-"` + // Maximum size in GB (extraction fails if content exceeds this) + SizeGB int64 `query:"size_gb,required" json:"-"` + // Optional custom volume ID (auto-generated if not provided) + ID param.Opt[string] `query:"id,omitzero" json:"-"` + paramObj +} + +func (r VolumeNewFromArchiveParams) MarshalMultipart() (data []byte, contentType string, err error) { + buf := bytes.NewBuffer(nil) + writer := multipart.NewWriter(buf) + err = apiform.MarshalRoot(r, writer) + if err == nil { + err = apiform.WriteExtras(writer, r.ExtraFields()) + } + if err != nil { + writer.Close() + return nil, "", err + } + err = writer.Close() + if err != nil { + return nil, "", err + } + return buf.Bytes(), writer.FormDataContentType(), nil +} + +// URLQuery serializes [VolumeNewFromArchiveParams]'s query parameters as +// `url.Values`. +func (r VolumeNewFromArchiveParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/volume_test.go b/volume_test.go index 09e1759..05dbc71 100644 --- a/volume_test.go +++ b/volume_test.go @@ -3,8 +3,10 @@ package hypeman_test import ( + "bytes" "context" "errors" + "io" "os" "testing" @@ -86,6 +88,37 @@ func TestVolumeDelete(t *testing.T) { } } +func TestVolumeNewFromArchiveWithOptionalParams(t *testing.T) { + t.Skip("Prism 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.Volumes.NewFromArchive( + context.TODO(), + io.Reader(bytes.NewBuffer([]byte("some file contents"))), + hypeman.VolumeNewFromArchiveParams{ + Name: "name", + SizeGB: 0, + ID: hypeman.String("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 TestVolumeGet(t *testing.T) { t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" From 30c85af355239d1a23d4d960c1f5c1bc2f2f50ea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:23:33 +0000 Subject: [PATCH 2/2] release: 0.9.6 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 13 +++++++++++++ README.md | 2 +- internal/version.go | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6d78745..8e9551f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.9.0" + ".": "0.9.6" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4405152..c46e448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.9.6 (2026-01-30) + +Full Changelog: [v0.9.0...v0.9.6](https://github.com/kernel/hypeman-go/compare/v0.9.0...v0.9.6) + +### Features + +* add boot time optimizations for faster VM startup ([3992761](https://github.com/kernel/hypeman-go/commit/3992761e3ad8ebb0cc22fb7408199b068e9d8013)) +* Add to stainless config new API endpoints ([de008e8](https://github.com/kernel/hypeman-go/commit/de008e89fadbaedde6554181618fb03c71b49465)) +* **api:** manual updates ([f60e600](https://github.com/kernel/hypeman-go/commit/f60e60015bb9ce18c7083963d9ecd11c980de495)) +* **builds:** implement two-tier build cache with per-repo token scopes ([0e29d03](https://github.com/kernel/hypeman-go/commit/0e29d03d94cf50a0d0e83c323f7ed9f2e15f3e61)) +* **client:** add a convenient param.SetJSON helper ([7fea166](https://github.com/kernel/hypeman-go/commit/7fea1660f3d17d8a35f5d2f6aa352b553785624b)) +* Use resources module for input validation ([af678e8](https://github.com/kernel/hypeman-go/commit/af678e8c794307a6bd47476acff3ca42a7a52546)) + ## 0.9.0 (2026-01-05) Full Changelog: [v0.8.0...v0.9.0](https://github.com/kernel/hypeman-go/compare/v0.8.0...v0.9.0) diff --git a/README.md b/README.md index 0b76851..4a876db 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.8.0' +go get -u 'github.com/kernel/hypeman-go@v0.9.6' ``` diff --git a/internal/version.go b/internal/version.go index 0e818c5..d7771e2 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.9.0" // x-release-please-version +const PackageVersion = "0.9.6" // x-release-please-version