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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,33 @@ on:
- 'stl-preview-base/**'

jobs:
build:
timeout-minutes: 10
name: build
permissions:
contents: read
id-token: write
runs-on: ${{ github.repository == 'stainless-sdks/hypeman-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: |-
github.repository == 'stainless-sdks/hypeman-go' &&
(github.event_name == 'push' || github.event.pull_request.head.repo.fork)
steps:
- uses: actions/checkout@v6

- name: Get GitHub OIDC Token
if: github.repository == 'stainless-sdks/hypeman-go'
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'
env:
URL: https://pkg.stainless.com/s
AUTH: ${{ steps.github-oidc.outputs.github_token }}
SHA: ${{ github.sha }}
run: ./scripts/utils/upload-artifact.sh
lint:
timeout-minutes: 10
name: lint
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.14.0"
".": "0.15.0"
}
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 37
configured_endpoints: 39
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-68bd472fc1704fc7ff7ed01b4213dda068a0865d42693d47ecef90651526febb.yml
openapi_spec_hash: 18ec995954b05d8dfb1e9e3254cf579a
config_hash: d452c139da1e46a44a68b91e8a40de72
config_hash: 368f8c9248e41f12124ab83b6f5b2eec
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## 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)

### Features

* Add fork operation to stainless config ([5ab52b7](https://github.com/kernel/hypeman-go/commit/5ab52b7c74031eb7e874615aa42c00090cb00b51))


### Chores

* **internal:** codegen related update ([83363a5](https://github.com/kernel/hypeman-go/commit/83363a5275d88e131d0736516fea215faaab84a7))

## 0.14.0 (2026-03-02)

Full Changelog: [v0.13.0...v0.14.0](https://github.com/kernel/hypeman-go/compare/v0.13.0...v0.14.0)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Or to pin the version:
<!-- x-release-please-start-version -->

```sh
go get -u 'github.com/kernel/hypeman-go@v0.14.0'
go get -u 'github.com/kernel/hypeman-go@v0.15.0'
```

<!-- x-release-please-end -->
Expand Down
3 changes: 3 additions & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Params Types:
Response Types:

- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Instance">Instance</a>
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceStats">InstanceStats</a>
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#PathInfo">PathInfo</a>
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#VolumeMount">VolumeMount</a>

Expand All @@ -38,12 +39,14 @@ Methods:
- <code title="post /instances">client.Instances.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceService.New">New</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceNewParams">InstanceNewParams</a>) (\*<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Instance">Instance</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /instances">client.Instances.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceListParams">InstanceListParams</a>) (\*[]<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Instance">Instance</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="delete /instances/{id}">client.Instances.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceService.Delete">Delete</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) <a href="https://pkg.go.dev/builtin#error">error</a></code>
- <code title="post /instances/{id}/fork">client.Instances.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceService.Fork">Fork</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceForkParams">InstanceForkParams</a>) (\*<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Instance">Instance</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /instances/{id}">client.Instances.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (\*<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Instance">Instance</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /instances/{id}/logs">client.Instances.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceService.Logs">Logs</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, query <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceLogsParams">InstanceLogsParams</a>) (\*<a href="https://pkg.go.dev/builtin#string">string</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /instances/{id}/restore">client.Instances.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceService.Restore">Restore</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (\*<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Instance">Instance</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /instances/{id}/standby">client.Instances.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceService.Standby">Standby</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (\*<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Instance">Instance</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /instances/{id}/start">client.Instances.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceService.Start">Start</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceStartParams">InstanceStartParams</a>) (\*<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Instance">Instance</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /instances/{id}/stat">client.Instances.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceService.Stat">Stat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, query <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceStatParams">InstanceStatParams</a>) (\*<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#PathInfo">PathInfo</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /instances/{id}/stats">client.Instances.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceService.Stats">Stats</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (\*<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceStats">InstanceStats</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /instances/{id}/stop">client.Instances.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#InstanceService.Stop">Stop</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (\*<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Instance">Instance</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>

## Volumes
Expand Down
107 changes: 107 additions & 0 deletions instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ func (r *InstanceService) Delete(ctx context.Context, id string, opts ...option.
return
}

// Fork an instance from stopped, standby, or running (with from_running=true)
func (r *InstanceService) Fork(ctx context.Context, id string, body InstanceForkParams, opts ...option.RequestOption) (res *Instance, err error) {
opts = slices.Concat(r.Options, opts)
if id == "" {
err = errors.New("missing required id parameter")
return
}
path := fmt.Sprintf("instances/%s/fork", id)
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
return
}

// Get instance details
func (r *InstanceService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Instance, err error) {
opts = slices.Concat(r.Options, opts)
Expand Down Expand Up @@ -157,6 +169,20 @@ func (r *InstanceService) Stat(ctx context.Context, id string, query InstanceSta
return
}

// Returns real-time resource utilization statistics for a running VM instance.
// Metrics are collected from /proc/<pid>/stat and /proc/<pid>/statm for CPU and
// memory, and from TAP interface statistics for network I/O.
func (r *InstanceService) Stats(ctx context.Context, id string, opts ...option.RequestOption) (res *InstanceStats, err error) {
opts = slices.Concat(r.Options, opts)
if id == "" {
err = errors.New("missing required id parameter")
return
}
path := fmt.Sprintf("instances/%s/stats", id)
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
return
}

// Stop instance (graceful shutdown)
func (r *InstanceService) Stop(ctx context.Context, id string, opts ...option.RequestOption) (res *Instance, err error) {
opts = slices.Concat(r.Options, opts)
Expand Down Expand Up @@ -348,6 +374,52 @@ func (r *InstanceNetwork) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}

// Real-time resource utilization statistics for a VM instance
type InstanceStats struct {
// Total memory allocated to the VM (Size + HotplugSize) in bytes
AllocatedMemoryBytes int64 `json:"allocated_memory_bytes" api:"required"`
// Number of vCPUs allocated to the VM
AllocatedVcpus int64 `json:"allocated_vcpus" api:"required"`
// Total CPU time consumed by the VM hypervisor process in seconds
CPUSeconds float64 `json:"cpu_seconds" api:"required"`
// Instance identifier
InstanceID string `json:"instance_id" api:"required"`
// Instance name
InstanceName string `json:"instance_name" api:"required"`
// Resident Set Size - actual physical memory used by the VM in bytes
MemoryRssBytes int64 `json:"memory_rss_bytes" api:"required"`
// Virtual Memory Size - total virtual memory allocated in bytes
MemoryVmsBytes int64 `json:"memory_vms_bytes" api:"required"`
// Total network bytes received by the VM (from TAP interface)
NetworkRxBytes int64 `json:"network_rx_bytes" api:"required"`
// Total network bytes transmitted by the VM (from TAP interface)
NetworkTxBytes int64 `json:"network_tx_bytes" api:"required"`
// Memory utilization ratio (RSS / allocated memory). Only present when
// allocated_memory_bytes > 0.
MemoryUtilizationRatio float64 `json:"memory_utilization_ratio" api:"nullable"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
AllocatedMemoryBytes respjson.Field
AllocatedVcpus respjson.Field
CPUSeconds respjson.Field
InstanceID respjson.Field
InstanceName respjson.Field
MemoryRssBytes respjson.Field
MemoryVmsBytes respjson.Field
NetworkRxBytes respjson.Field
NetworkTxBytes respjson.Field
MemoryUtilizationRatio respjson.Field
ExtraFields map[string]respjson.Field
raw string
} `json:"-"`
}

// Returns the unmodified JSON received from the API
func (r InstanceStats) RawJSON() string { return r.JSON.raw }
func (r *InstanceStats) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}

type PathInfo struct {
// Whether the path exists
Exists bool `json:"exists" api:"required"`
Expand Down Expand Up @@ -590,6 +662,41 @@ const (
InstanceListParamsStateUnknown InstanceListParamsState = "Unknown"
)

type InstanceForkParams struct {
// Name for the forked instance (lowercase letters, digits, and dashes only; cannot
// start or end with a dash)
Name string `json:"name" api:"required"`
// Allow forking from a running source instance. When true and source is Running,
// the source is put into standby, forked, then restored back to Running.
FromRunning param.Opt[bool] `json:"from_running,omitzero"`
// Optional final state for the forked instance. Default is the source instance
// state at fork time. For example, forking from Running defaults the fork result
// to Running.
//
// Any of "Stopped", "Standby", "Running".
TargetState InstanceForkParamsTargetState `json:"target_state,omitzero"`
paramObj
}

func (r InstanceForkParams) MarshalJSON() (data []byte, err error) {
type shadow InstanceForkParams
return param.MarshalObject(r, (*shadow)(&r))
}
func (r *InstanceForkParams) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}

// Optional final state for the forked instance. Default is the source instance
// state at fork time. For example, forking from Running defaults the fork result
// to Running.
type InstanceForkParamsTargetState string

const (
InstanceForkParamsTargetStateStopped InstanceForkParamsTargetState = "Stopped"
InstanceForkParamsTargetStateStandby InstanceForkParamsTargetState = "Standby"
InstanceForkParamsTargetStateRunning InstanceForkParamsTargetState = "Running"
)

type InstanceLogsParams struct {
// Continue streaming new lines after initial output
Follow param.Opt[bool] `query:"follow,omitzero" json:"-"`
Expand Down
54 changes: 54 additions & 0 deletions instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,37 @@ func TestInstanceDelete(t *testing.T) {
}
}

func TestInstanceForkWithOptionalParams(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.Fork(
context.TODO(),
"id",
hypeman.InstanceForkParams{
Name: "my-workload-1-fork",
FromRunning: hypeman.Bool(false),
TargetState: hypeman.InstanceForkParamsTargetStateRunning,
},
)
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 TestInstanceGet(t *testing.T) {
t.Skip("Mock server tests are disabled")
baseURL := "http://localhost:4010"
Expand Down Expand Up @@ -253,6 +284,29 @@ func TestInstanceStatWithOptionalParams(t *testing.T) {
}
}

func TestInstanceStats(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.Stats(context.TODO(), "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 TestInstanceStop(t *testing.T) {
t.Skip("Mock server tests are disabled")
baseURL := "http://localhost:4010"
Expand Down
2 changes: 1 addition & 1 deletion internal/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

package internal

const PackageVersion = "0.14.0" // x-release-please-version
const PackageVersion = "0.15.0" // x-release-please-version
50 changes: 50 additions & 0 deletions scripts/utils/upload-artifact.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -exuo pipefail

DIST_DIR="dist"
FILENAME="source.zip"

mapfile -d '' files < <(
find . -type f \
\( -name '*.go' -o -name 'go.mod' -o -name 'go.sum' \) \
! -path "./${DIST_DIR}/*" \
-print0
)

if [[ ${#files[@]} -eq 0 ]]; then
echo -e "\033[31mNo Go source files found for packaging.\033[0m"
exit 1
fi

mkdir -p "$DIST_DIR"
rm -f "${DIST_DIR}/${FILENAME}"

relative_files=()
for file in "${files[@]}"; do
relative_files+=("${file#./}")
done

zip "${DIST_DIR}/${FILENAME}" "${relative_files[@]}"

RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \
-H "Authorization: Bearer $AUTH" \
-H "Content-Type: application/json")
Comment thread
sjmiller609 marked this conversation as resolved.

SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url')

if [[ "$SIGNED_URL" == "null" ]]; then
echo -e "\033[31mFailed to get signed URL.\033[0m"
exit 1
fi

UPLOAD_RESPONSE=$(curl -v -X PUT \
-H "Content-Type: application/zip" \
--data-binary "@${DIST_DIR}/${FILENAME}" "$SIGNED_URL" 2>&1)

if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then
echo -e "\033[32mUploaded build to Stainless storage.\033[0m"
echo -e "\033[32mInstallation: Download and unzip: 'https://pkg.stainless.com/s/hypeman-go/$SHA'. Run 'go mod edit -replace github.com/kernel/hypeman-go=/path/to/unzipped_directory'.\033[0m"
else
echo -e "\033[31mFailed to upload artifact.\033[0m"
exit 1
fi