diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index da59f99..2aca35a 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.4.0"
+ ".": "0.5.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 2410f40..96e35f6 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 18
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-ce51f144a3d2de556750203edbaa5bfeefe874660737c35a4fc37dfb30057dd5.yml
-openapi_spec_hash: 27663b6503056317abcb578ac7b67c06
-config_hash: b4e65d240d7bca1ba6162ee2098c8ac2
+configured_endpoints: 22
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-da3f4038bb544acae375f44527f515dc58308f67822905258b155192041e65ed.yml
+openapi_spec_hash: 4c7f6f453c20eda7fd8689e8917c65f9
+config_hash: a7d0557c72de54fd6baded5b189777c3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b85f65e..2feb3ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
# Changelog
+## 0.5.0 (2025-12-05)
+
+Full Changelog: [v0.4.0...v0.5.0](https://github.com/onkernel/hypeman-go/compare/v0.4.0...v0.5.0)
+
+### Features
+
+* add Push and PushImage functions for OCI registry push ([7417cc8](https://github.com/onkernel/hypeman-go/commit/7417cc8a56c7d11c535ac7ab9a7b3d21d80bd2b4))
+* Ingress ([c751d1a](https://github.com/onkernel/hypeman-go/commit/c751d1a6bba5ca619c03f833f27251c6d3b855a7))
+* Initialize volume with data ([32d4047](https://github.com/onkernel/hypeman-go/commit/32d404746df0a3e9d83e7651105e6c6daa16476f))
+* try to fix name collision in codegen ([8173a73](https://github.com/onkernel/hypeman-go/commit/8173a73d0317d35870d5a3cec8f3fdec56fcf362))
+* Volume readonly multi-attach ([bac3fd2](https://github.com/onkernel/hypeman-go/commit/bac3fd2cee3325dc3d1b31e6077ad1f1ce13340c))
+* Volumes ([099f9b8](https://github.com/onkernel/hypeman-go/commit/099f9b8a2553087e117c8c8a9731900081d713f0))
+
## 0.4.0 (2025-11-26)
Full Changelog: [v0.3.0...v0.4.0](https://github.com/onkernel/hypeman-go/compare/v0.3.0...v0.4.0)
diff --git a/README.md b/README.md
index 7171142..a5c9ecb 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ Or to pin the version:
```sh
-go get -u 'github.com/onkernel/hypeman-go@v0.4.0'
+go get -u 'github.com/onkernel/hypeman-go@v0.5.0'
```
diff --git a/api.md b/api.md
index aa4b3c7..81727b9 100644
--- a/api.md
+++ b/api.md
@@ -17,25 +17,30 @@ Response Types:
Methods:
- client.Images.New(ctx context.Context, body hypeman.ImageNewParams) (hypeman.Image, error)
-- client.Images.Get(ctx context.Context, name string) (hypeman.Image, error)
- client.Images.List(ctx context.Context) ([]hypeman.Image, error)
- client.Images.Delete(ctx context.Context, name string) error
+- client.Images.Get(ctx context.Context, name string) (hypeman.Image, error)
# Instances
+Params Types:
+
+- hypeman.VolumeMountParam
+
Response Types:
- hypeman.Instance
+- hypeman.VolumeMount
Methods:
- client.Instances.New(ctx context.Context, body hypeman.InstanceNewParams) (hypeman.Instance, error)
-- client.Instances.Get(ctx context.Context, id string) (hypeman.Instance, error)
- client.Instances.List(ctx context.Context) ([]hypeman.Instance, error)
- client.Instances.Delete(ctx context.Context, id string) error
+- 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.PutInStandby(ctx context.Context, id string) (hypeman.Instance, error)
-- client.Instances.RestoreFromStandby(ctx context.Context, id string) (hypeman.Instance, error)
+- client.Instances.Restore(ctx context.Context, id string) (hypeman.Instance, error)
+- client.Instances.Standby(ctx context.Context, id string) (hypeman.Instance, error)
## Volumes
@@ -49,10 +54,33 @@ Methods:
Response Types:
- hypeman.Volume
+- hypeman.VolumeAttachment
Methods:
- client.Volumes.New(ctx context.Context, body hypeman.VolumeNewParams) (hypeman.Volume, error)
-- client.Volumes.Get(ctx context.Context, id string) (hypeman.Volume, error)
- client.Volumes.List(ctx context.Context) ([]hypeman.Volume, error)
- client.Volumes.Delete(ctx context.Context, id string) error
+- client.Volumes.Get(ctx context.Context, id string) (hypeman.Volume, error)
+
+# Ingresses
+
+Params Types:
+
+- hypeman.IngressMatchParam
+- hypeman.IngressRuleParam
+- hypeman.IngressTargetParam
+
+Response Types:
+
+- hypeman.Ingress
+- hypeman.IngressMatch
+- hypeman.IngressRule
+- hypeman.IngressTarget
+
+Methods:
+
+- client.Ingresses.New(ctx context.Context, body hypeman.IngressNewParams) (hypeman.Ingress, error)
+- client.Ingresses.List(ctx context.Context) ([]hypeman.Ingress, error)
+- client.Ingresses.Delete(ctx context.Context, id string) error
+- client.Ingresses.Get(ctx context.Context, id string) (hypeman.Ingress, error)
diff --git a/client.go b/client.go
index 3f05529..70aefcc 100644
--- a/client.go
+++ b/client.go
@@ -21,6 +21,7 @@ type Client struct {
Images ImageService
Instances InstanceService
Volumes VolumeService
+ Ingresses IngressService
}
// DefaultClientOptions read from the environment (HYPEMAN_API_KEY,
@@ -49,6 +50,7 @@ func NewClient(opts ...option.RequestOption) (r Client) {
r.Images = NewImageService(opts...)
r.Instances = NewInstanceService(opts...)
r.Volumes = NewVolumeService(opts...)
+ r.Ingresses = NewIngressService(opts...)
return
}
diff --git a/image.go b/image.go
index 3026a2c..410478a 100644
--- a/image.go
+++ b/image.go
@@ -44,18 +44,6 @@ func (r *ImageService) New(ctx context.Context, body ImageNewParams, opts ...opt
return
}
-// Get image details
-func (r *ImageService) Get(ctx context.Context, name string, opts ...option.RequestOption) (res *Image, err error) {
- opts = slices.Concat(r.Options, opts)
- if name == "" {
- err = errors.New("missing required name parameter")
- return
- }
- path := fmt.Sprintf("images/%s", name)
- err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
- return
-}
-
// List images
func (r *ImageService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Image, err error) {
opts = slices.Concat(r.Options, opts)
@@ -77,6 +65,18 @@ func (r *ImageService) Delete(ctx context.Context, name string, opts ...option.R
return
}
+// Get image details
+func (r *ImageService) Get(ctx context.Context, name string, opts ...option.RequestOption) (res *Image, err error) {
+ opts = slices.Concat(r.Options, opts)
+ if name == "" {
+ err = errors.New("missing required name parameter")
+ return
+ }
+ path := fmt.Sprintf("images/%s", name)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
type Image struct {
// Creation timestamp (RFC3339)
CreatedAt time.Time `json:"created_at,required" format:"date-time"`
diff --git a/image_test.go b/image_test.go
index cd7536b..facbfba 100644
--- a/image_test.go
+++ b/image_test.go
@@ -38,7 +38,7 @@ func TestImageNew(t *testing.T) {
}
}
-func TestImageGet(t *testing.T) {
+func TestImageList(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -51,7 +51,7 @@ func TestImageGet(t *testing.T) {
option.WithBaseURL(baseURL),
option.WithAPIKey("My API Key"),
)
- _, err := client.Images.Get(context.TODO(), "name")
+ _, err := client.Images.List(context.TODO())
if err != nil {
var apierr *hypeman.Error
if errors.As(err, &apierr) {
@@ -61,7 +61,7 @@ func TestImageGet(t *testing.T) {
}
}
-func TestImageList(t *testing.T) {
+func TestImageDelete(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -74,7 +74,7 @@ func TestImageList(t *testing.T) {
option.WithBaseURL(baseURL),
option.WithAPIKey("My API Key"),
)
- _, err := client.Images.List(context.TODO())
+ err := client.Images.Delete(context.TODO(), "name")
if err != nil {
var apierr *hypeman.Error
if errors.As(err, &apierr) {
@@ -84,7 +84,7 @@ func TestImageList(t *testing.T) {
}
}
-func TestImageDelete(t *testing.T) {
+func TestImageGet(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -97,7 +97,7 @@ func TestImageDelete(t *testing.T) {
option.WithBaseURL(baseURL),
option.WithAPIKey("My API Key"),
)
- err := client.Images.Delete(context.TODO(), "name")
+ _, err := client.Images.Get(context.TODO(), "name")
if err != nil {
var apierr *hypeman.Error
if errors.As(err, &apierr) {
diff --git a/ingress.go b/ingress.go
new file mode 100644
index 0000000..68a4e1d
--- /dev/null
+++ b/ingress.go
@@ -0,0 +1,256 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package hypeman
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "slices"
+ "time"
+
+ "github.com/onkernel/hypeman-go/internal/apijson"
+ "github.com/onkernel/hypeman-go/internal/requestconfig"
+ "github.com/onkernel/hypeman-go/option"
+ "github.com/onkernel/hypeman-go/packages/param"
+ "github.com/onkernel/hypeman-go/packages/respjson"
+)
+
+// IngressService 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 [NewIngressService] method instead.
+type IngressService struct {
+ Options []option.RequestOption
+}
+
+// NewIngressService 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 NewIngressService(opts ...option.RequestOption) (r IngressService) {
+ r = IngressService{}
+ r.Options = opts
+ return
+}
+
+// Create ingress
+func (r *IngressService) New(ctx context.Context, body IngressNewParams, opts ...option.RequestOption) (res *Ingress, err error) {
+ opts = slices.Concat(r.Options, opts)
+ path := "ingresses"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
+ return
+}
+
+// List ingresses
+func (r *IngressService) List(ctx context.Context, 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...)
+ return
+}
+
+// Delete ingress
+func (r *IngressService) Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) {
+ opts = slices.Concat(r.Options, opts)
+ opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("ingresses/%s", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...)
+ return
+}
+
+// Get ingress details
+func (r *IngressService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Ingress, err error) {
+ opts = slices.Concat(r.Options, opts)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("ingresses/%s", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
+type Ingress struct {
+ // Auto-generated unique identifier
+ ID string `json:"id,required"`
+ // Creation timestamp (RFC3339)
+ CreatedAt time.Time `json:"created_at,required" format:"date-time"`
+ // Human-readable name
+ Name string `json:"name,required"`
+ // Routing rules for this ingress
+ Rules []IngressRule `json:"rules,required"`
+ // 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
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r Ingress) RawJSON() string { return r.JSON.raw }
+func (r *Ingress) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type IngressMatch struct {
+ // Hostname to match (exact match on Host header)
+ Hostname string `json:"hostname,required"`
+ // Host port to listen on for this rule (default 80)
+ Port int64 `json:"port"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ Hostname respjson.Field
+ Port respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r IngressMatch) RawJSON() string { return r.JSON.raw }
+func (r *IngressMatch) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+// ToParam converts this IngressMatch to a IngressMatchParam.
+//
+// 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
+// IngressMatchParam.Overrides()
+func (r IngressMatch) ToParam() IngressMatchParam {
+ return param.Override[IngressMatchParam](json.RawMessage(r.RawJSON()))
+}
+
+// The property Hostname is required.
+type IngressMatchParam struct {
+ // Hostname to match (exact match on Host header)
+ Hostname string `json:"hostname,required"`
+ // Host port to listen on for this rule (default 80)
+ Port param.Opt[int64] `json:"port,omitzero"`
+ paramObj
+}
+
+func (r IngressMatchParam) MarshalJSON() (data []byte, err error) {
+ type shadow IngressMatchParam
+ return param.MarshalObject(r, (*shadow)(&r))
+}
+func (r *IngressMatchParam) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type IngressRule struct {
+ Match IngressMatch `json:"match,required"`
+ Target IngressTarget `json:"target,required"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ Match respjson.Field
+ Target respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r IngressRule) RawJSON() string { return r.JSON.raw }
+func (r *IngressRule) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+// ToParam converts this IngressRule to a IngressRuleParam.
+//
+// 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
+// IngressRuleParam.Overrides()
+func (r IngressRule) ToParam() IngressRuleParam {
+ return param.Override[IngressRuleParam](json.RawMessage(r.RawJSON()))
+}
+
+// The properties Match, Target are required.
+type IngressRuleParam struct {
+ Match IngressMatchParam `json:"match,omitzero,required"`
+ Target IngressTargetParam `json:"target,omitzero,required"`
+ paramObj
+}
+
+func (r IngressRuleParam) MarshalJSON() (data []byte, err error) {
+ type shadow IngressRuleParam
+ return param.MarshalObject(r, (*shadow)(&r))
+}
+func (r *IngressRuleParam) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type IngressTarget struct {
+ // Target instance name or ID
+ Instance string `json:"instance,required"`
+ // Target port on the instance
+ Port int64 `json:"port,required"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ Instance respjson.Field
+ Port respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r IngressTarget) RawJSON() string { return r.JSON.raw }
+func (r *IngressTarget) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+// ToParam converts this IngressTarget to a IngressTargetParam.
+//
+// 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
+// IngressTargetParam.Overrides()
+func (r IngressTarget) ToParam() IngressTargetParam {
+ return param.Override[IngressTargetParam](json.RawMessage(r.RawJSON()))
+}
+
+// The properties Instance, Port are required.
+type IngressTargetParam struct {
+ // Target instance name or ID
+ Instance string `json:"instance,required"`
+ // Target port on the instance
+ Port int64 `json:"port,required"`
+ paramObj
+}
+
+func (r IngressTargetParam) MarshalJSON() (data []byte, err error) {
+ type shadow IngressTargetParam
+ return param.MarshalObject(r, (*shadow)(&r))
+}
+func (r *IngressTargetParam) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type IngressNewParams struct {
+ // Human-readable name (lowercase letters, digits, and dashes only; cannot start or
+ // end with a dash)
+ Name string `json:"name,required"`
+ // Routing rules for this ingress
+ Rules []IngressRuleParam `json:"rules,omitzero,required"`
+ paramObj
+}
+
+func (r IngressNewParams) MarshalJSON() (data []byte, err error) {
+ type shadow IngressNewParams
+ return param.MarshalObject(r, (*shadow)(&r))
+}
+func (r *IngressNewParams) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
diff --git a/ingress_test.go b/ingress_test.go
new file mode 100644
index 0000000..ae44a04
--- /dev/null
+++ b/ingress_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/onkernel/hypeman-go"
+ "github.com/onkernel/hypeman-go/internal/testutil"
+ "github.com/onkernel/hypeman-go/option"
+)
+
+func TestIngressNew(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.Ingresses.New(context.TODO(), hypeman.IngressNewParams{
+ Name: "my-api-ingress",
+ Rules: []hypeman.IngressRuleParam{{
+ Match: hypeman.IngressMatchParam{
+ Hostname: "api.example.com",
+ Port: hypeman.Int(8080),
+ },
+ Target: hypeman.IngressTargetParam{
+ Instance: "my-api",
+ Port: 8080,
+ },
+ }},
+ })
+ 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 TestIngressList(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.Ingresses.List(context.TODO())
+ 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 TestIngressDelete(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.Ingresses.Delete(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 TestIngressGet(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.Ingresses.Get(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())
+ }
+}
diff --git a/instance.go b/instance.go
index 69a15d7..2f9d571 100644
--- a/instance.go
+++ b/instance.go
@@ -4,6 +4,7 @@ package hypeman
import (
"context"
+ "encoding/json"
"errors"
"fmt"
"net/http"
@@ -49,18 +50,6 @@ func (r *InstanceService) New(ctx context.Context, body InstanceNewParams, 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)
- if id == "" {
- err = errors.New("missing required id parameter")
- return
- }
- path := fmt.Sprintf("instances/%s", id)
- err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
- return
-}
-
// List instances
func (r *InstanceService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Instance, err error) {
opts = slices.Concat(r.Options, opts)
@@ -82,6 +71,18 @@ func (r *InstanceService) Delete(ctx context.Context, id string, opts ...option.
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)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("instances/%s", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
// Streams instance console logs as Server-Sent Events. Returns the last N lines
// (controlled by `tail` parameter), then optionally continues streaming new lines
// if `follow=true`.
@@ -101,26 +102,26 @@ func (r *InstanceService) LogsStreaming(ctx context.Context, id string, query In
return ssestream.NewStream[string](ssestream.NewDecoder(raw), err)
}
-// Put instance in standby (pause, snapshot, delete VMM)
-func (r *InstanceService) PutInStandby(ctx context.Context, id string, opts ...option.RequestOption) (res *Instance, err error) {
+// Restore instance from standby
+func (r *InstanceService) Restore(ctx context.Context, id string, 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/standby", id)
+ path := fmt.Sprintf("instances/%s/restore", id)
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
return
}
-// Restore instance from standby
-func (r *InstanceService) RestoreFromStandby(ctx context.Context, id string, opts ...option.RequestOption) (res *Instance, err error) {
+// Put instance in standby (pause, snapshot, delete VMM)
+func (r *InstanceService) Standby(ctx context.Context, id string, 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/restore", id)
+ path := fmt.Sprintf("instances/%s/standby", id)
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
return
}
@@ -163,6 +164,8 @@ type Instance struct {
StoppedAt time.Time `json:"stopped_at,nullable" format:"date-time"`
// Number of virtual CPUs
Vcpus int64 `json:"vcpus"`
+ // Volumes attached to the instance
+ Volumes []VolumeMount `json:"volumes"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
ID respjson.Field
@@ -179,6 +182,7 @@ type Instance struct {
StartedAt respjson.Field
StoppedAt respjson.Field
Vcpus respjson.Field
+ Volumes respjson.Field
ExtraFields map[string]respjson.Field
raw string
} `json:"-"`
@@ -236,6 +240,69 @@ func (r *InstanceNetwork) 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,required"`
+ // Volume identifier
+ VolumeID string `json:"volume_id,required"`
+ // Create per-instance overlay for writes (requires readonly=true)
+ Overlay bool `json:"overlay"`
+ // Max overlay size as human-readable string (e.g., "1GB"). Required if
+ // overlay=true.
+ OverlaySize string `json:"overlay_size"`
+ // Whether volume is mounted read-only
+ Readonly bool `json:"readonly"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ MountPath respjson.Field
+ VolumeID respjson.Field
+ Overlay respjson.Field
+ OverlaySize respjson.Field
+ Readonly respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r VolumeMount) RawJSON() string { return r.JSON.raw }
+func (r *VolumeMount) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+// ToParam converts this VolumeMount to a VolumeMountParam.
+//
+// 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
+// VolumeMountParam.Overrides()
+func (r VolumeMount) ToParam() VolumeMountParam {
+ return param.Override[VolumeMountParam](json.RawMessage(r.RawJSON()))
+}
+
+// The properties MountPath, VolumeID are required.
+type VolumeMountParam struct {
+ // Path where volume is mounted in the guest
+ MountPath string `json:"mount_path,required"`
+ // Volume identifier
+ VolumeID string `json:"volume_id,required"`
+ // Create per-instance overlay for writes (requires readonly=true)
+ Overlay param.Opt[bool] `json:"overlay,omitzero"`
+ // Max overlay size as human-readable string (e.g., "1GB"). Required if
+ // overlay=true.
+ OverlaySize param.Opt[string] `json:"overlay_size,omitzero"`
+ // Whether volume is mounted read-only
+ Readonly param.Opt[bool] `json:"readonly,omitzero"`
+ paramObj
+}
+
+func (r VolumeMountParam) MarshalJSON() (data []byte, err error) {
+ type shadow VolumeMountParam
+ return param.MarshalObject(r, (*shadow)(&r))
+}
+func (r *VolumeMountParam) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
type InstanceNewParams struct {
// OCI image reference
Image string `json:"image,required"`
@@ -254,6 +321,8 @@ type InstanceNewParams struct {
Env map[string]string `json:"env,omitzero"`
// Network configuration for the instance
Network InstanceNewParamsNetwork `json:"network,omitzero"`
+ // Volumes to attach to the instance at creation time
+ Volumes []VolumeMountParam `json:"volumes,omitzero"`
paramObj
}
diff --git a/instance_test.go b/instance_test.go
index d88c1a6..d8ba3a7 100644
--- a/instance_test.go
+++ b/instance_test.go
@@ -40,6 +40,13 @@ func TestInstanceNewWithOptionalParams(t *testing.T) {
OverlaySize: hypeman.String("20GB"),
Size: hypeman.String("2GB"),
Vcpus: hypeman.Int(2),
+ Volumes: []hypeman.VolumeMountParam{{
+ MountPath: "/mnt/data",
+ VolumeID: "vol-abc123",
+ Overlay: hypeman.Bool(true),
+ OverlaySize: hypeman.String("1GB"),
+ Readonly: hypeman.Bool(true),
+ }},
})
if err != nil {
var apierr *hypeman.Error
@@ -50,7 +57,7 @@ func TestInstanceNewWithOptionalParams(t *testing.T) {
}
}
-func TestInstanceGet(t *testing.T) {
+func TestInstanceList(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -63,7 +70,7 @@ func TestInstanceGet(t *testing.T) {
option.WithBaseURL(baseURL),
option.WithAPIKey("My API Key"),
)
- _, err := client.Instances.Get(context.TODO(), "id")
+ _, err := client.Instances.List(context.TODO())
if err != nil {
var apierr *hypeman.Error
if errors.As(err, &apierr) {
@@ -73,7 +80,7 @@ func TestInstanceGet(t *testing.T) {
}
}
-func TestInstanceList(t *testing.T) {
+func TestInstanceDelete(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -86,7 +93,7 @@ func TestInstanceList(t *testing.T) {
option.WithBaseURL(baseURL),
option.WithAPIKey("My API Key"),
)
- _, err := client.Instances.List(context.TODO())
+ err := client.Instances.Delete(context.TODO(), "id")
if err != nil {
var apierr *hypeman.Error
if errors.As(err, &apierr) {
@@ -96,7 +103,7 @@ func TestInstanceList(t *testing.T) {
}
}
-func TestInstanceDelete(t *testing.T) {
+func TestInstanceGet(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -109,7 +116,7 @@ func TestInstanceDelete(t *testing.T) {
option.WithBaseURL(baseURL),
option.WithAPIKey("My API Key"),
)
- err := client.Instances.Delete(context.TODO(), "id")
+ _, err := client.Instances.Get(context.TODO(), "id")
if err != nil {
var apierr *hypeman.Error
if errors.As(err, &apierr) {
@@ -119,7 +126,7 @@ func TestInstanceDelete(t *testing.T) {
}
}
-func TestInstancePutInStandby(t *testing.T) {
+func TestInstanceRestore(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -132,7 +139,7 @@ func TestInstancePutInStandby(t *testing.T) {
option.WithBaseURL(baseURL),
option.WithAPIKey("My API Key"),
)
- _, err := client.Instances.PutInStandby(context.TODO(), "id")
+ _, err := client.Instances.Restore(context.TODO(), "id")
if err != nil {
var apierr *hypeman.Error
if errors.As(err, &apierr) {
@@ -142,7 +149,7 @@ func TestInstancePutInStandby(t *testing.T) {
}
}
-func TestInstanceRestoreFromStandby(t *testing.T) {
+func TestInstanceStandby(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -155,7 +162,7 @@ func TestInstanceRestoreFromStandby(t *testing.T) {
option.WithBaseURL(baseURL),
option.WithAPIKey("My API Key"),
)
- _, err := client.Instances.RestoreFromStandby(context.TODO(), "id")
+ _, err := client.Instances.Standby(context.TODO(), "id")
if err != nil {
var apierr *hypeman.Error
if errors.As(err, &apierr) {
diff --git a/internal/version.go b/internal/version.go
index 5c62cac..67c4d40 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -2,4 +2,4 @@
package internal
-const PackageVersion = "0.4.0" // x-release-please-version
+const PackageVersion = "0.5.0" // x-release-please-version
diff --git a/volume.go b/volume.go
index e99cbb5..e68abff 100644
--- a/volume.go
+++ b/volume.go
@@ -36,7 +36,11 @@ func NewVolumeService(opts ...option.RequestOption) (r VolumeService) {
return
}
-// Create volume
+// 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
func (r *VolumeService) New(ctx context.Context, body VolumeNewParams, opts ...option.RequestOption) (res *Volume, err error) {
opts = slices.Concat(r.Options, opts)
path := "volumes"
@@ -44,18 +48,6 @@ func (r *VolumeService) New(ctx context.Context, body VolumeNewParams, opts ...o
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)
- if id == "" {
- err = errors.New("missing required id parameter")
- return
- }
- path := fmt.Sprintf("volumes/%s", id)
- err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
- return
-}
-
// List volumes
func (r *VolumeService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Volume, err error) {
opts = slices.Concat(r.Options, opts)
@@ -77,6 +69,18 @@ func (r *VolumeService) Delete(ctx context.Context, id string, opts ...option.Re
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)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("volumes/%s", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
type Volume struct {
// Unique identifier
ID string `json:"id,required"`
@@ -86,18 +90,15 @@ type Volume struct {
Name string `json:"name,required"`
// Size in gigabytes
SizeGB int64 `json:"size_gb,required"`
- // Instance ID if attached
- AttachedTo string `json:"attached_to,nullable"`
- // Mount path if attached
- MountPath string `json:"mount_path,nullable"`
+ // List of current attachments (empty if not attached)
+ Attachments []VolumeAttachment `json:"attachments"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
ID respjson.Field
CreatedAt respjson.Field
Name respjson.Field
SizeGB respjson.Field
- AttachedTo respjson.Field
- MountPath respjson.Field
+ Attachments respjson.Field
ExtraFields map[string]respjson.Field
raw string
} `json:"-"`
@@ -109,6 +110,29 @@ func (r *Volume) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
+type VolumeAttachment struct {
+ // ID of the instance this volume is attached to
+ InstanceID string `json:"instance_id,required"`
+ // Mount path in the guest
+ MountPath string `json:"mount_path,required"`
+ // Whether the attachment is read-only
+ Readonly bool `json:"readonly,required"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ InstanceID respjson.Field
+ MountPath respjson.Field
+ Readonly respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r VolumeAttachment) RawJSON() string { return r.JSON.raw }
+func (r *VolumeAttachment) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
type VolumeNewParams struct {
// Volume name
Name string `json:"name,required"`
diff --git a/volume_test.go b/volume_test.go
index 26f2106..2528263 100644
--- a/volume_test.go
+++ b/volume_test.go
@@ -40,7 +40,7 @@ func TestVolumeNewWithOptionalParams(t *testing.T) {
}
}
-func TestVolumeGet(t *testing.T) {
+func TestVolumeList(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -53,7 +53,7 @@ func TestVolumeGet(t *testing.T) {
option.WithBaseURL(baseURL),
option.WithAPIKey("My API Key"),
)
- _, err := client.Volumes.Get(context.TODO(), "id")
+ _, err := client.Volumes.List(context.TODO())
if err != nil {
var apierr *hypeman.Error
if errors.As(err, &apierr) {
@@ -63,7 +63,7 @@ func TestVolumeGet(t *testing.T) {
}
}
-func TestVolumeList(t *testing.T) {
+func TestVolumeDelete(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -76,7 +76,7 @@ func TestVolumeList(t *testing.T) {
option.WithBaseURL(baseURL),
option.WithAPIKey("My API Key"),
)
- _, err := client.Volumes.List(context.TODO())
+ err := client.Volumes.Delete(context.TODO(), "id")
if err != nil {
var apierr *hypeman.Error
if errors.As(err, &apierr) {
@@ -86,7 +86,7 @@ func TestVolumeList(t *testing.T) {
}
}
-func TestVolumeDelete(t *testing.T) {
+func TestVolumeGet(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -99,7 +99,7 @@ func TestVolumeDelete(t *testing.T) {
option.WithBaseURL(baseURL),
option.WithAPIKey("My API Key"),
)
- err := client.Volumes.Delete(context.TODO(), "id")
+ _, err := client.Volumes.Get(context.TODO(), "id")
if err != nil {
var apierr *hypeman.Error
if errors.As(err, &apierr) {