diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f14b480..aaf968a 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.1.0-alpha.2"
+ ".": "0.1.0-alpha.3"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 54aa04a..a2e8f7c 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 9
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company%2Fsfc-nodes-64b3af1a7aa80906205b3369d34afb7c2686ab1c184f4683d81f2e3adffa255e.yml
-openapi_spec_hash: 4a63ec0585f1f25c42f1a612345f7ab4
-config_hash: b63d685bc4feb4db73f82791193686bd
+configured_endpoints: 13
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company%2Fsfc-nodes-92d29637c0ceda06c752ad70b078ff8dad505843c16b923130774376890baaa5.yml
+openapi_spec_hash: 1f20f7d76aa5575ee0601ece537af956
+config_hash: cf202573c712b5d91a4d496f35f0ff57
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3547ab3..783e72f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,33 @@
# Changelog
+## 0.1.0-alpha.3 (2025-10-13)
+
+Full Changelog: [v0.1.0-alpha.2...v0.1.0-alpha.3](https://github.com/sfcompute/nodes-go/compare/v0.1.0-alpha.2...v0.1.0-alpha.3)
+
+### Features
+
+* **api:** add vm images resources and update formatting ([273c45f](https://github.com/sfcompute/nodes-go/commit/273c45f75e75104ad10eb1eab1bdb7acc3596cce))
+* **api:** api update ([97607e0](https://github.com/sfcompute/nodes-go/commit/97607e02d439b4fe1463a483d362ab5bf65fd708))
+* **api:** api update ([5788fb0](https://github.com/sfcompute/nodes-go/commit/5788fb0d297e3c4f79e76e1f2369bd086dad610b))
+* **api:** api update ([59b16bc](https://github.com/sfcompute/nodes-go/commit/59b16bcc748ffea703644d6332f33a79196aeeee))
+* **api:** api update ([d9c2daa](https://github.com/sfcompute/nodes-go/commit/d9c2daa0151d664a5bbff0609d0a6e1e6db3b8c8))
+* **api:** disable retries ([8c19a86](https://github.com/sfcompute/nodes-go/commit/8c19a865b584b8dc40feecd1a26e3d8d9a013230))
+
+
+### Bug Fixes
+
+* **api:** remove undocumented endpoints, add list endpoint ([ee795ee](https://github.com/sfcompute/nodes-go/commit/ee795ee93f88b5c379d9ce834356c59fae43e162))
+* **internal:** unmarshal correctly when there are multiple discriminators ([2836b68](https://github.com/sfcompute/nodes-go/commit/2836b6828c5f60e8c0c4e8892bbefa54e54b64b8))
+* use slices.Concat instead of sometimes modifying r.Options ([6adf0a1](https://github.com/sfcompute/nodes-go/commit/6adf0a14ecd1b5d20924b1b26816af8c5ed270ad))
+
+
+### Chores
+
+* bump minimum go version to 1.22 ([90c5664](https://github.com/sfcompute/nodes-go/commit/90c566457cad4e6964d6217feeb21eb699d2c385))
+* configure new SDK language ([42f31df](https://github.com/sfcompute/nodes-go/commit/42f31df1c143e56ca36cea4b5de2b937ac000c7c))
+* do not install brew dependencies in ./scripts/bootstrap by default ([5983889](https://github.com/sfcompute/nodes-go/commit/5983889bfd8681434b39372a0d58fe60115ff267))
+* update more docs for 1.22 ([82733b2](https://github.com/sfcompute/nodes-go/commit/82733b2eff62084fa0b0e415e8a3ad6e1f8f546c))
+
## 0.1.0-alpha.2 (2025-09-05)
Full Changelog: [v0.1.0-alpha.1...v0.1.0-alpha.2](https://github.com/sfcompute/nodes-go/compare/v0.1.0-alpha.1...v0.1.0-alpha.2)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 536f5f7..090e94c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,7 +9,7 @@ $ ./scripts/lint
This will install all the required dependencies and build the SDK.
-You can also [install go 1.18+ manually](https://go.dev/doc/install).
+You can also [install go 1.22+ manually](https://go.dev/doc/install).
## Modifying/Adding code
diff --git a/README.md b/README.md
index 858a28e..dbad93d 100644
--- a/README.md
+++ b/README.md
@@ -28,14 +28,14 @@ Or to pin the version:
```sh
-go get -u 'github.com/sfcompute/nodes-go@v0.1.0-alpha.2'
+go get -u 'github.com/sfcompute/nodes-go@v0.1.0-alpha.3'
```
## Requirements
-This library requires Go 1.18+.
+This library requires Go 1.22+.
## Usage
@@ -345,7 +345,7 @@ which can be used to wrap any `io.Reader` with the appropriate file name and con
### Retries
-Certain errors will be automatically retried 2 times by default, with a short exponential backoff.
+Certain errors will be automatically retried 0 times by default, with a short exponential backoff.
We retry by default all connection errors, 408 Request Timeout, 409 Conflict, 429 Rate Limit,
and >=500 Internal errors.
diff --git a/api.md b/api.md
index eb3ecaa..bb64d71 100644
--- a/api.md
+++ b/api.md
@@ -1,14 +1,14 @@
-# Vms
+# VMs
Response Types:
-- sfcnodes.VmLogsResponse
-- sfcnodes.VmSSHResponse
+- sfcnodes.VMLogsResponse
+- sfcnodes.VmsshResponse
Methods:
-- client.Vms.Logs(ctx context.Context, query sfcnodes.VmLogsParams) (sfcnodes.VmLogsResponse, error)
-- client.Vms.SSH(ctx context.Context, query sfcnodes.VmSSHParams) (sfcnodes.VmSSHResponse, error)
+- client.VMs.Logs(ctx context.Context, query sfcnodes.VMLogsParams) (sfcnodes.VMLogsResponse, error)
+- client.VMs.SSH(ctx context.Context, query sfcnodes.VMSSHParams) (sfcnodes.VmsshResponse, error)
## Script
@@ -19,13 +19,25 @@ Params Types:
Response Types:
- sfcnodes.UserDataUnion
-- sfcnodes.VmScriptNewResponse
-- sfcnodes.VmScriptGetResponse
+- sfcnodes.VMScriptNewResponse
+- sfcnodes.VMScriptGetResponse
Methods:
-- client.Vms.Script.New(ctx context.Context, body sfcnodes.VmScriptNewParams) (sfcnodes.VmScriptNewResponse, error)
-- client.Vms.Script.Get(ctx context.Context) (sfcnodes.VmScriptGetResponse, error)
+- client.VMs.Script.New(ctx context.Context, body sfcnodes.VMScriptNewParams) (sfcnodes.VMScriptNewResponse, error)
+- client.VMs.Script.Get(ctx context.Context) (sfcnodes.VMScriptGetResponse, error)
+
+## Images
+
+Response Types:
+
+- sfcnodes.VMImageListResponse
+- sfcnodes.VMImageGetResponse
+
+Methods:
+
+- client.VMs.Images.List(ctx context.Context) (sfcnodes.VMImageListResponse, error)
+- client.VMs.Images.Get(ctx context.Context, imageID string) (sfcnodes.VMImageGetResponse, error)
# Nodes
@@ -47,6 +59,8 @@ Methods:
- client.Nodes.New(ctx context.Context, body sfcnodes.NodeNewParams) (sfcnodes.ListResponseNode, error)
- client.Nodes.List(ctx context.Context, query sfcnodes.NodeListParams) (sfcnodes.ListResponseNode, error)
+- client.Nodes.Delete(ctx context.Context, id string) error
- client.Nodes.Extend(ctx context.Context, id string, body sfcnodes.NodeExtendParams) (sfcnodes.Node, error)
- client.Nodes.Get(ctx context.Context, id string) (sfcnodes.Node, error)
+- client.Nodes.Redeploy(ctx context.Context, id string, body sfcnodes.NodeRedeployParams) (sfcnodes.Node, error)
- client.Nodes.Release(ctx context.Context, id string) (sfcnodes.Node, error)
diff --git a/client.go b/client.go
index 4fe4421..8c783b6 100644
--- a/client.go
+++ b/client.go
@@ -6,6 +6,7 @@ import (
"context"
"net/http"
"os"
+ "slices"
"github.com/sfcompute/nodes-go/internal/requestconfig"
"github.com/sfcompute/nodes-go/option"
@@ -16,7 +17,7 @@ import (
// directly, and instead use the [NewClient] method instead.
type Client struct {
Options []option.RequestOption
- Vms VmService
+ VMs VMService
Nodes NodeService
}
@@ -42,7 +43,7 @@ func NewClient(opts ...option.RequestOption) (r Client) {
r = Client{Options: opts}
- r.Vms = NewVmService(opts...)
+ r.VMs = NewVMService(opts...)
r.Nodes = NewNodeService(opts...)
return
@@ -80,7 +81,7 @@ func NewClient(opts ...option.RequestOption) (r Client) {
// For even greater flexibility, see [option.WithResponseInto] and
// [option.WithResponseBodyInto].
func (r *Client) Execute(ctx context.Context, method string, path string, params any, res any, opts ...option.RequestOption) error {
- opts = append(r.Options, opts...)
+ opts = slices.Concat(r.Options, opts)
return requestconfig.ExecuteNewRequest(ctx, method, path, params, res, opts...)
}
diff --git a/client_test.go b/client_test.go
index c5a3d51..eebfb89 100644
--- a/client_test.go
+++ b/client_test.go
@@ -68,11 +68,11 @@ func TestRetryAfter(t *testing.T) {
}
attempts := len(retryCountHeaders)
- if attempts != 3 {
- t.Errorf("Expected %d attempts, got %d", 3, attempts)
+ if attempts != 1 {
+ t.Errorf("Expected %d attempts, got %d", 1, attempts)
}
- expectedRetryCountHeaders := []string{"0", "1", "2"}
+ expectedRetryCountHeaders := []string{"0"}
if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) {
t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders)
}
@@ -102,7 +102,7 @@ func TestDeleteRetryCountHeader(t *testing.T) {
t.Error("Expected there to be a cancel error")
}
- expectedRetryCountHeaders := []string{"", "", ""}
+ expectedRetryCountHeaders := []string{""}
if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) {
t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders)
}
@@ -132,7 +132,7 @@ func TestOverwriteRetryCountHeader(t *testing.T) {
t.Error("Expected there to be a cancel error")
}
- expectedRetryCountHeaders := []string{"42", "42", "42"}
+ expectedRetryCountHeaders := []string{"42"}
if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) {
t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders)
}
@@ -160,7 +160,7 @@ func TestRetryAfterMs(t *testing.T) {
if err == nil {
t.Error("Expected there to be a cancel error")
}
- if want := 3; attempts != want {
+ if want := 1; attempts != want {
t.Errorf("Expected %d attempts, got %d", want, attempts)
}
}
diff --git a/go.mod b/go.mod
index f115e1a..3332421 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/sfcompute/nodes-go
-go 1.21
+go 1.22
require (
github.com/tidwall/gjson v1.14.4
diff --git a/internal/apijson/decodeparam_test.go b/internal/apijson/decodeparam_test.go
index 8f08d4e..b6ab607 100644
--- a/internal/apijson/decodeparam_test.go
+++ b/internal/apijson/decodeparam_test.go
@@ -351,6 +351,36 @@ func init() {
})
}
+type FooVariant struct {
+ Type string `json:"type,required"`
+ Value string `json:"value,required"`
+}
+
+type BarVariant struct {
+ Type string `json:"type,required"`
+ Enable bool `json:"enable,required"`
+}
+
+type MultiDiscriminatorUnion struct {
+ OfFoo *FooVariant `json:",inline"`
+ OfBar *BarVariant `json:",inline"`
+
+ paramUnion
+}
+
+func init() {
+ apijson.RegisterDiscriminatedUnion[MultiDiscriminatorUnion]("type", map[string]reflect.Type{
+ "foo": reflect.TypeOf(FooVariant{}),
+ "foo_v2": reflect.TypeOf(FooVariant{}),
+ "bar": reflect.TypeOf(BarVariant{}),
+ "bar_legacy": reflect.TypeOf(BarVariant{}),
+ })
+}
+
+func (m *MultiDiscriminatorUnion) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, m)
+}
+
func (d *DiscriminatedUnion) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, d)
}
@@ -408,3 +438,61 @@ func TestDiscriminatedUnion(t *testing.T) {
})
}
}
+
+func TestMultiDiscriminatorUnion(t *testing.T) {
+ tests := map[string]struct {
+ raw string
+ target MultiDiscriminatorUnion
+ shouldFail bool
+ }{
+ "foo_variant": {
+ raw: `{"type":"foo","value":"test"}`,
+ target: MultiDiscriminatorUnion{OfFoo: &FooVariant{
+ Type: "foo",
+ Value: "test",
+ }},
+ },
+ "foo_v2_variant": {
+ raw: `{"type":"foo_v2","value":"test_v2"}`,
+ target: MultiDiscriminatorUnion{OfFoo: &FooVariant{
+ Type: "foo_v2",
+ Value: "test_v2",
+ }},
+ },
+ "bar_variant": {
+ raw: `{"type":"bar","enable":true}`,
+ target: MultiDiscriminatorUnion{OfBar: &BarVariant{
+ Type: "bar",
+ Enable: true,
+ }},
+ },
+ "bar_legacy_variant": {
+ raw: `{"type":"bar_legacy","enable":false}`,
+ target: MultiDiscriminatorUnion{OfBar: &BarVariant{
+ Type: "bar_legacy",
+ Enable: false,
+ }},
+ },
+ "invalid_type": {
+ raw: `{"type":"unknown","value":"test"}`,
+ target: MultiDiscriminatorUnion{},
+ shouldFail: true,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ var dst MultiDiscriminatorUnion
+ err := json.Unmarshal([]byte(test.raw), &dst)
+ if err != nil && !test.shouldFail {
+ t.Fatalf("failed unmarshal with err: %v", err)
+ }
+ if err == nil && test.shouldFail {
+ t.Fatalf("expected unmarshal to fail but it succeeded")
+ }
+ if !reflect.DeepEqual(dst, test.target) {
+ t.Fatalf("failed equality, got %#v but expected %#v", dst, test.target)
+ }
+ })
+ }
+}
diff --git a/internal/apijson/union.go b/internal/apijson/union.go
index 7f3d8dc..c961275 100644
--- a/internal/apijson/union.go
+++ b/internal/apijson/union.go
@@ -39,12 +39,10 @@ func RegisterDiscriminatedUnion[T any](key string, mappings map[string]reflect.T
func (d *decoderBuilder) newStructUnionDecoder(t reflect.Type) decoderFunc {
type variantDecoder struct {
- decoder decoderFunc
- field reflect.StructField
- discriminatorValue any
+ decoder decoderFunc
+ field reflect.StructField
}
-
- variants := []variantDecoder{}
+ decoders := []variantDecoder{}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
@@ -53,18 +51,26 @@ func (d *decoderBuilder) newStructUnionDecoder(t reflect.Type) decoderFunc {
}
decoder := d.typeDecoder(field.Type)
- variants = append(variants, variantDecoder{
+ decoders = append(decoders, variantDecoder{
decoder: decoder,
field: field,
})
}
+ type discriminatedDecoder struct {
+ variantDecoder
+ discriminator any
+ }
+ discriminatedDecoders := []discriminatedDecoder{}
unionEntry, discriminated := unionRegistry[t]
- for _, unionVariant := range unionEntry.variants {
- for i := 0; i < len(variants); i++ {
- variant := &variants[i]
- if variant.field.Type.Elem() == unionVariant.Type {
- variant.discriminatorValue = unionVariant.DiscriminatorValue
+ for _, variant := range unionEntry.variants {
+ // For each union variant, find a matching decoder and save it
+ for _, decoder := range decoders {
+ if decoder.field.Type.Elem() == variant.Type {
+ discriminatedDecoders = append(discriminatedDecoders, discriminatedDecoder{
+ decoder,
+ variant.DiscriminatorValue,
+ })
break
}
}
@@ -73,10 +79,10 @@ func (d *decoderBuilder) newStructUnionDecoder(t reflect.Type) decoderFunc {
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
if discriminated && n.Type == gjson.JSON && len(unionEntry.discriminatorKey) != 0 {
discriminator := n.Get(unionEntry.discriminatorKey).Value()
- for _, variant := range variants {
- if discriminator == variant.discriminatorValue {
- inner := v.FieldByIndex(variant.field.Index)
- return variant.decoder(n, inner, state)
+ for _, decoder := range discriminatedDecoders {
+ if discriminator == decoder.discriminator {
+ inner := v.FieldByIndex(decoder.field.Index)
+ return decoder.decoder(n, inner, state)
}
}
return errors.New("apijson: was not able to find discriminated union variant")
@@ -85,15 +91,15 @@ func (d *decoderBuilder) newStructUnionDecoder(t reflect.Type) decoderFunc {
// Set bestExactness to worse than loose
bestExactness := loose - 1
bestVariant := -1
- for i, variant := range variants {
+ for i, decoder := range decoders {
// Pointers are used to discern JSON object variants from value variants
- if n.Type != gjson.JSON && variant.field.Type.Kind() == reflect.Ptr {
+ if n.Type != gjson.JSON && decoder.field.Type.Kind() == reflect.Ptr {
continue
}
sub := decoderState{strict: state.strict, exactness: exact}
- inner := v.FieldByIndex(variant.field.Index)
- err := variant.decoder(n, inner, &sub)
+ inner := v.FieldByIndex(decoder.field.Index)
+ err := decoder.decoder(n, inner, &sub)
if err != nil {
continue
}
@@ -116,11 +122,11 @@ func (d *decoderBuilder) newStructUnionDecoder(t reflect.Type) decoderFunc {
return errors.New("apijson: was not able to coerce type as union strictly")
}
- for i := 0; i < len(variants); i++ {
+ for i := 0; i < len(decoders); i++ {
if i == bestVariant {
continue
}
- v.FieldByIndex(variants[i].field.Index).SetZero()
+ v.FieldByIndex(decoders[i].field.Index).SetZero()
}
return nil
diff --git a/internal/encoding/json/shims/shims.go b/internal/encoding/json/shims/shims.go
index b65a016..fe9a71a 100644
--- a/internal/encoding/json/shims/shims.go
+++ b/internal/encoding/json/shims/shims.go
@@ -1,5 +1,5 @@
// This package provides shims over Go 1.2{2,3} APIs
-// which are missing from Go 1.21, and used by the Go 1.24 encoding/json package.
+// which are missing from Go 1.22, and used by the Go 1.24 encoding/json package.
//
// Inside the vendored package, all shim code has comments that begin look like
// // SHIM(...): ...
diff --git a/internal/requestconfig/requestconfig.go b/internal/requestconfig/requestconfig.go
index 8e5db66..f36e5e7 100644
--- a/internal/requestconfig/requestconfig.go
+++ b/internal/requestconfig/requestconfig.go
@@ -164,7 +164,7 @@ func NewRequestConfig(ctx context.Context, method string, u string, body any, ds
req.Header.Add(k, v)
}
cfg := RequestConfig{
- MaxRetries: 2,
+ MaxRetries: 0,
Context: ctx,
Request: req,
HTTPClient: http.DefaultClient,
diff --git a/internal/version.go b/internal/version.go
index d6f40b3..2d1d85e 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -2,4 +2,4 @@
package internal
-const PackageVersion = "0.1.0-alpha.2" // x-release-please-version
+const PackageVersion = "0.1.0-alpha.3" // x-release-please-version
diff --git a/node.go b/node.go
index c07bfec..73f0ea9 100644
--- a/node.go
+++ b/node.go
@@ -9,6 +9,7 @@ import (
"fmt"
"net/http"
"net/url"
+ "slices"
"github.com/sfcompute/nodes-go/internal/apijson"
"github.com/sfcompute/nodes-go/internal/apiquery"
@@ -40,23 +41,36 @@ func NewNodeService(opts ...option.RequestOption) (r NodeService) {
// Create VM nodes
func (r *NodeService) New(ctx context.Context, body NodeNewParams, opts ...option.RequestOption) (res *ListResponseNode, err error) {
- opts = append(r.Options[:], opts...)
+ opts = slices.Concat(r.Options, opts)
path := "v1/nodes"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
return
}
-// List all VM nodes for the authenticated account
+// List all nodes for the authenticated account
func (r *NodeService) List(ctx context.Context, query NodeListParams, opts ...option.RequestOption) (res *ListResponseNode, err error) {
- opts = append(r.Options[:], opts...)
+ opts = slices.Concat(r.Options, opts)
path := "v1/nodes"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
return
}
+// Delete a node by id. The node cannot be deleted if it has active or pending VMs.
+func (r *NodeService) 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("v1/nodes/%s", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...)
+ return
+}
+
// Purchase additional time to extend the end time of a reserved VM node
func (r *NodeService) Extend(ctx context.Context, id string, body NodeExtendParams, opts ...option.RequestOption) (res *Node, err error) {
- opts = append(r.Options[:], opts...)
+ opts = slices.Concat(r.Options, opts)
if id == "" {
err = errors.New("missing required id parameter")
return
@@ -68,7 +82,7 @@ func (r *NodeService) Extend(ctx context.Context, id string, body NodeExtendPara
// Retrieve details of a specific node by its ID or name
func (r *NodeService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Node, err error) {
- opts = append(r.Options[:], opts...)
+ opts = slices.Concat(r.Options, opts)
if id == "" {
err = errors.New("missing required id parameter")
return
@@ -78,10 +92,23 @@ func (r *NodeService) Get(ctx context.Context, id string, opts ...option.Request
return
}
+// Redeploy a node by replacing its current VM with a new one. Optionally update
+// the VM image and cloud init user data.
+func (r *NodeService) Redeploy(ctx context.Context, id string, body NodeRedeployParams, opts ...option.RequestOption) (res *Node, err error) {
+ opts = slices.Concat(r.Options, opts)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("v1/nodes/%s/redeploy", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPut, path, body, &res, opts...)
+ return
+}
+
// Release an auto reserved VM node from its procurement, reducing the
// procurement's desired quantity by 1
func (r *NodeService) Release(ctx context.Context, id string, opts ...option.RequestOption) (res *Node, err error) {
- opts = append(r.Options[:], opts...)
+ opts = slices.Concat(r.Options, opts)
if id == "" {
err = errors.New("missing required id parameter")
return
@@ -105,15 +132,18 @@ type CreateNodesRequestParam struct {
MaxPricePerNodeHour int64 `json:"max_price_per_node_hour,required"`
// Zone to create the nodes in
Zone string `json:"zone,required"`
- // End time as Unix timestamp in seconds. If provided, end time must be aligned to
- // the hour. If not provided, the node will be created as an autoreserved node.
+ // End time as Unix timestamp in seconds If provided, end time must be aligned to
+ // the hour If not provided, the node will be created as an autoreserved node
EndAt param.Opt[int64] `json:"end_at,omitzero"`
- // Start time as Unix timestamp in seconds
+ // User script to be executed during the VM's boot process Data should be base64
+ // encoded
+ CloudInitUserData param.Opt[string] `json:"cloud_init_user_data,omitzero" format:"byte"`
+ // Custom image ID to use for the VM instances
+ ImageID param.Opt[string] `json:"image_id,omitzero"`
+ // Start time as Unix timestamp in seconds Required for reserved nodes
StartAt param.Opt[int64] `json:"start_at,omitzero"`
- // User script to be executed during the VM's boot process
- CloudInitUserData []int64 `json:"cloud_init_user_data,omitzero"`
- // Custom node names. Names cannot follow the vm\_{alpha_numeric_chars} as this is
- // reserved for system-generated IDs. Names cannot be numeric strings.
+ // Custom node names Names cannot follow the vm\_{alpha_numeric_chars} as this is
+ // reserved for system-generated IDs Names cannot be numeric strings
Names []string `json:"names,omitzero"`
// Any of "autoreserved", "reserved".
NodeType NodeType `json:"node_type,omitzero"`
@@ -179,7 +209,8 @@ type ListResponseNodeData struct {
// "deleted", "failed", "unknown".
Status Status `json:"status,required"`
// Creation time as Unix timestamp in seconds
- CreatedAt int64 `json:"created_at,nullable"`
+ CreatedAt int64 `json:"created_at,nullable"`
+ CurrentVM ListResponseNodeDataCurrentVM `json:"current_vm,nullable"`
// Deletion time as Unix timestamp in seconds
DeletedAt int64 `json:"deleted_at,nullable"`
// End time as Unix timestamp in seconds
@@ -191,7 +222,7 @@ type ListResponseNodeData struct {
StartAt int64 `json:"start_at,nullable"`
// Last updated time as Unix timestamp in seconds
UpdatedAt int64 `json:"updated_at,nullable"`
- Vms ListResponseNodeDataVms `json:"vms,nullable"`
+ VMs ListResponseNodeDataVMs `json:"vms,nullable"`
Zone string `json:"zone,nullable"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
@@ -203,13 +234,14 @@ type ListResponseNodeData struct {
Owner respjson.Field
Status respjson.Field
CreatedAt respjson.Field
+ CurrentVM respjson.Field
DeletedAt respjson.Field
EndAt respjson.Field
MaxPricePerNodeHour respjson.Field
ProcurementID respjson.Field
StartAt respjson.Field
UpdatedAt respjson.Field
- Vms respjson.Field
+ VMs respjson.Field
Zone respjson.Field
ExtraFields map[string]respjson.Field
raw string
@@ -222,8 +254,39 @@ func (r *ListResponseNodeData) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
-type ListResponseNodeDataVms struct {
- Data []ListResponseNodeDataVmsData `json:"data,required"`
+type ListResponseNodeDataCurrentVM struct {
+ ID string `json:"id,required"`
+ CreatedAt int64 `json:"created_at,required"`
+ EndAt int64 `json:"end_at,required"`
+ Object string `json:"object,required"`
+ StartAt int64 `json:"start_at,required"`
+ // Any of "Pending", "Running", "Destroyed", "NodeFailure", "Unspecified".
+ Status string `json:"status,required"`
+ UpdatedAt int64 `json:"updated_at,required"`
+ ImageID string `json:"image_id,nullable"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ ID respjson.Field
+ CreatedAt respjson.Field
+ EndAt respjson.Field
+ Object respjson.Field
+ StartAt respjson.Field
+ Status respjson.Field
+ UpdatedAt respjson.Field
+ ImageID respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r ListResponseNodeDataCurrentVM) RawJSON() string { return r.JSON.raw }
+func (r *ListResponseNodeDataCurrentVM) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type ListResponseNodeDataVMs struct {
+ Data []ListResponseNodeDataVMsData `json:"data,required"`
Object string `json:"object,required"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
@@ -235,12 +298,12 @@ type ListResponseNodeDataVms struct {
}
// Returns the unmodified JSON received from the API
-func (r ListResponseNodeDataVms) RawJSON() string { return r.JSON.raw }
-func (r *ListResponseNodeDataVms) UnmarshalJSON(data []byte) error {
+func (r ListResponseNodeDataVMs) RawJSON() string { return r.JSON.raw }
+func (r *ListResponseNodeDataVMs) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
-type ListResponseNodeDataVmsData struct {
+type ListResponseNodeDataVMsData struct {
ID string `json:"id,required"`
CreatedAt int64 `json:"created_at,required"`
EndAt int64 `json:"end_at,required"`
@@ -249,6 +312,7 @@ type ListResponseNodeDataVmsData struct {
// Any of "Pending", "Running", "Destroyed", "NodeFailure", "Unspecified".
Status string `json:"status,required"`
UpdatedAt int64 `json:"updated_at,required"`
+ ImageID string `json:"image_id,nullable"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
ID respjson.Field
@@ -258,14 +322,15 @@ type ListResponseNodeDataVmsData struct {
StartAt respjson.Field
Status respjson.Field
UpdatedAt respjson.Field
+ ImageID respjson.Field
ExtraFields map[string]respjson.Field
raw string
} `json:"-"`
}
// Returns the unmodified JSON received from the API
-func (r ListResponseNodeDataVmsData) RawJSON() string { return r.JSON.raw }
-func (r *ListResponseNodeDataVmsData) UnmarshalJSON(data []byte) error {
+func (r ListResponseNodeDataVMsData) RawJSON() string { return r.JSON.raw }
+func (r *ListResponseNodeDataVMsData) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
@@ -284,7 +349,8 @@ type Node struct {
// "deleted", "failed", "unknown".
Status Status `json:"status,required"`
// Creation time as Unix timestamp in seconds
- CreatedAt int64 `json:"created_at,nullable"`
+ CreatedAt int64 `json:"created_at,nullable"`
+ CurrentVM NodeCurrentVM `json:"current_vm,nullable"`
// Deletion time as Unix timestamp in seconds
DeletedAt int64 `json:"deleted_at,nullable"`
// End time as Unix timestamp in seconds
@@ -296,7 +362,7 @@ type Node struct {
StartAt int64 `json:"start_at,nullable"`
// Last updated time as Unix timestamp in seconds
UpdatedAt int64 `json:"updated_at,nullable"`
- Vms NodeVms `json:"vms,nullable"`
+ VMs NodeVMs `json:"vms,nullable"`
Zone string `json:"zone,nullable"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
@@ -308,13 +374,14 @@ type Node struct {
Owner respjson.Field
Status respjson.Field
CreatedAt respjson.Field
+ CurrentVM respjson.Field
DeletedAt respjson.Field
EndAt respjson.Field
MaxPricePerNodeHour respjson.Field
ProcurementID respjson.Field
StartAt respjson.Field
UpdatedAt respjson.Field
- Vms respjson.Field
+ VMs respjson.Field
Zone respjson.Field
ExtraFields map[string]respjson.Field
raw string
@@ -327,8 +394,39 @@ func (r *Node) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
-type NodeVms struct {
- Data []NodeVmsData `json:"data,required"`
+type NodeCurrentVM struct {
+ ID string `json:"id,required"`
+ CreatedAt int64 `json:"created_at,required"`
+ EndAt int64 `json:"end_at,required"`
+ Object string `json:"object,required"`
+ StartAt int64 `json:"start_at,required"`
+ // Any of "Pending", "Running", "Destroyed", "NodeFailure", "Unspecified".
+ Status string `json:"status,required"`
+ UpdatedAt int64 `json:"updated_at,required"`
+ ImageID string `json:"image_id,nullable"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ ID respjson.Field
+ CreatedAt respjson.Field
+ EndAt respjson.Field
+ Object respjson.Field
+ StartAt respjson.Field
+ Status respjson.Field
+ UpdatedAt respjson.Field
+ ImageID respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r NodeCurrentVM) RawJSON() string { return r.JSON.raw }
+func (r *NodeCurrentVM) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type NodeVMs struct {
+ Data []NodeVMsData `json:"data,required"`
Object string `json:"object,required"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
@@ -340,12 +438,12 @@ type NodeVms struct {
}
// Returns the unmodified JSON received from the API
-func (r NodeVms) RawJSON() string { return r.JSON.raw }
-func (r *NodeVms) UnmarshalJSON(data []byte) error {
+func (r NodeVMs) RawJSON() string { return r.JSON.raw }
+func (r *NodeVMs) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
-type NodeVmsData struct {
+type NodeVMsData struct {
ID string `json:"id,required"`
CreatedAt int64 `json:"created_at,required"`
EndAt int64 `json:"end_at,required"`
@@ -354,6 +452,7 @@ type NodeVmsData struct {
// Any of "Pending", "Running", "Destroyed", "NodeFailure", "Unspecified".
Status string `json:"status,required"`
UpdatedAt int64 `json:"updated_at,required"`
+ ImageID string `json:"image_id,nullable"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
ID respjson.Field
@@ -363,14 +462,15 @@ type NodeVmsData struct {
StartAt respjson.Field
Status respjson.Field
UpdatedAt respjson.Field
+ ImageID respjson.Field
ExtraFields map[string]respjson.Field
raw string
} `json:"-"`
}
// Returns the unmodified JSON received from the API
-func (r NodeVmsData) RawJSON() string { return r.JSON.raw }
-func (r *NodeVmsData) UnmarshalJSON(data []byte) error {
+func (r NodeVMsData) RawJSON() string { return r.JSON.raw }
+func (r *NodeVMsData) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
@@ -409,11 +509,15 @@ func (r *NodeNewParams) UnmarshalJSON(data []byte) error {
type NodeListParams struct {
// Filter nodes by node_id Use ?id=n_b1dc52505c6db142&id=n_b1dc52505c6db133 to
- // specify multiple IDs. Cannot be used with name
+ // specify multiple IDs. Cannot combine with name or node_type
ID []string `query:"id,omitzero" json:"-"`
// Filter nodes by their names Use ?name=val1&name=val2 to specify multiple names.
- // Cannot be used with id
+ // Cannot combine with id or node_type
Name []string `query:"name,omitzero" json:"-"`
+ // Filter nodes by their type Cannot combine with id or name
+ //
+ // Any of "autoreserved", "reserved".
+ Type NodeType `query:"type,omitzero" json:"-"`
paramObj
}
@@ -436,3 +540,27 @@ func (r NodeExtendParams) MarshalJSON() (data []byte, err error) {
func (r *NodeExtendParams) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &r.ExtendNodeRequest)
}
+
+type NodeRedeployParams struct {
+ // Update the cloud init user data for VMs running on this node Data should be
+ // base64 encoded
+ CloudInitUserData param.Opt[string] `json:"cloud_init_user_data,omitzero" format:"byte"`
+ // Redeploy node with this VM image ID
+ ImageID param.Opt[string] `json:"image_id,omitzero"`
+ // If false, then the new VM will inherit any configuration (like image_id,
+ // cloud_init_user_data) that is left empty in this request from the current VM.
+ //
+ // If true, then any configuration left empty will be set as empty in the new VM.
+ // E.g if cloud_init_user_data is left unset and override_empty is true, then the
+ // new VM will not have any cloud init user data. override_empty defaults to false.
+ OverrideEmpty param.Opt[bool] `json:"override_empty,omitzero"`
+ paramObj
+}
+
+func (r NodeRedeployParams) MarshalJSON() (data []byte, err error) {
+ type shadow NodeRedeployParams
+ return param.MarshalObject(r, (*shadow)(&r))
+}
+func (r *NodeRedeployParams) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
diff --git a/node_test.go b/node_test.go
index f7f5011..88ca793 100644
--- a/node_test.go
+++ b/node_test.go
@@ -31,8 +31,9 @@ func TestNodeNewWithOptionalParams(t *testing.T) {
DesiredCount: 1,
MaxPricePerNodeHour: 1000,
Zone: "hayesvalley",
- CloudInitUserData: []int64{0},
+ CloudInitUserData: sfcnodes.String("aGVsbG8gd29ybGQ="),
EndAt: sfcnodes.Int(0),
+ ImageID: sfcnodes.String("vmi_1234567890abcdef"),
Names: []string{"cuda-crunch"},
NodeType: sfcnodes.NodeTypeAutoreserved,
StartAt: sfcnodes.Int(1640995200),
@@ -63,6 +64,7 @@ func TestNodeListWithOptionalParams(t *testing.T) {
_, err := client.Nodes.List(context.TODO(), sfcnodes.NodeListParams{
ID: []string{"string"},
Name: []string{"string"},
+ Type: sfcnodes.NodeTypeAutoreserved,
})
if err != nil {
var apierr *sfcnodes.Error
@@ -73,6 +75,29 @@ func TestNodeListWithOptionalParams(t *testing.T) {
}
}
+func TestNodeDelete(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 := sfcnodes.NewClient(
+ option.WithBaseURL(baseURL),
+ option.WithBearerToken("My Bearer Token"),
+ )
+ err := client.Nodes.Delete(context.TODO(), "id")
+ if err != nil {
+ var apierr *sfcnodes.Error
+ if errors.As(err, &apierr) {
+ t.Log(string(apierr.DumpRequest(true)))
+ }
+ t.Fatalf("err should be nil: %s", err.Error())
+ }
+}
+
func TestNodeExtend(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
@@ -128,6 +153,37 @@ func TestNodeGet(t *testing.T) {
}
}
+func TestNodeRedeployWithOptionalParams(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 := sfcnodes.NewClient(
+ option.WithBaseURL(baseURL),
+ option.WithBearerToken("My Bearer Token"),
+ )
+ _, err := client.Nodes.Redeploy(
+ context.TODO(),
+ "id",
+ sfcnodes.NodeRedeployParams{
+ CloudInitUserData: sfcnodes.String("aGVsbG8gd29ybGQ="),
+ ImageID: sfcnodes.String("vmi_1234567890abcdef"),
+ OverrideEmpty: sfcnodes.Bool(true),
+ },
+ )
+ if err != nil {
+ var apierr *sfcnodes.Error
+ if errors.As(err, &apierr) {
+ t.Log(string(apierr.DumpRequest(true)))
+ }
+ t.Fatalf("err should be nil: %s", err.Error())
+ }
+}
+
func TestNodeRelease(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
diff --git a/scripts/bootstrap b/scripts/bootstrap
index d6ac165..5ab3066 100755
--- a/scripts/bootstrap
+++ b/scripts/bootstrap
@@ -4,10 +4,18 @@ set -e
cd "$(dirname "$0")/.."
-if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ]; then
+if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then
brew bundle check >/dev/null 2>&1 || {
- echo "==> Installing Homebrew dependencies…"
- brew bundle
+ echo -n "==> Install Homebrew dependencies? (y/N): "
+ read -r response
+ case "$response" in
+ [yY][eE][sS]|[yY])
+ brew bundle
+ ;;
+ *)
+ ;;
+ esac
+ echo
}
fi
diff --git a/vm.go b/vm.go
index b55da6e..675fc64 100644
--- a/vm.go
+++ b/vm.go
@@ -6,6 +6,7 @@ import (
"context"
"net/http"
"net/url"
+ "slices"
"github.com/sfcompute/nodes-go/internal/apijson"
"github.com/sfcompute/nodes-go/internal/apiquery"
@@ -15,43 +16,45 @@ import (
"github.com/sfcompute/nodes-go/packages/respjson"
)
-// VmService contains methods and other services that help with interacting with
+// VMService contains methods and other services that help with interacting with
// the sfc-nodes 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 [NewVmService] method instead.
-type VmService struct {
+// the [NewVMService] method instead.
+type VMService struct {
Options []option.RequestOption
- Script VmScriptService
+ Script VMScriptService
+ Images VMImageService
}
-// NewVmService generates a new service that applies the given options to each
+// NewVMService 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 NewVmService(opts ...option.RequestOption) (r VmService) {
- r = VmService{}
+func NewVMService(opts ...option.RequestOption) (r VMService) {
+ r = VMService{}
r.Options = opts
- r.Script = NewVmScriptService(opts...)
+ r.Script = NewVMScriptService(opts...)
+ r.Images = NewVMImageService(opts...)
return
}
-func (r *VmService) Logs(ctx context.Context, query VmLogsParams, opts ...option.RequestOption) (res *VmLogsResponse, err error) {
- opts = append(r.Options[:], opts...)
+func (r *VMService) Logs(ctx context.Context, query VMLogsParams, opts ...option.RequestOption) (res *VMLogsResponse, err error) {
+ opts = slices.Concat(r.Options, opts)
path := "v0/vms/logs2"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
return
}
-func (r *VmService) SSH(ctx context.Context, query VmSSHParams, opts ...option.RequestOption) (res *VmSSHResponse, err error) {
- opts = append(r.Options[:], opts...)
+func (r *VMService) SSH(ctx context.Context, query VMSSHParams, opts ...option.RequestOption) (res *VmsshResponse, err error) {
+ opts = slices.Concat(r.Options, opts)
path := "v0/vms/ssh"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
return
}
-type VmLogsResponse struct {
- Data []VmLogsResponseData `json:"data,required"`
+type VMLogsResponse struct {
+ Data []VMLogsResponseData `json:"data,required"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
Data respjson.Field
@@ -61,12 +64,12 @@ type VmLogsResponse struct {
}
// Returns the unmodified JSON received from the API
-func (r VmLogsResponse) RawJSON() string { return r.JSON.raw }
-func (r *VmLogsResponse) UnmarshalJSON(data []byte) error {
+func (r VMLogsResponse) RawJSON() string { return r.JSON.raw }
+func (r *VMLogsResponse) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
-type VmLogsResponseData struct {
+type VMLogsResponseData struct {
Data []int64 `json:"data,required"`
InstanceID string `json:"instance_id,required"`
MonotonicTimestampNanoSec int64 `json:"monotonic_timestamp_nano_sec,required"`
@@ -88,15 +91,15 @@ type VmLogsResponseData struct {
}
// Returns the unmodified JSON received from the API
-func (r VmLogsResponseData) RawJSON() string { return r.JSON.raw }
-func (r *VmLogsResponseData) UnmarshalJSON(data []byte) error {
+func (r VMLogsResponseData) RawJSON() string { return r.JSON.raw }
+func (r *VMLogsResponseData) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
-type VmSSHResponse struct {
+type VmsshResponse struct {
SSHHostname string `json:"ssh_hostname,required"`
SSHPort int64 `json:"ssh_port,required"`
- SSHHostKeys []VmSSHResponseSSHHostKey `json:"ssh_host_keys,nullable"`
+ SSHHostKeys []VmsshResponseSSHHostKey `json:"ssh_host_keys,nullable"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
SSHHostname respjson.Field
@@ -108,12 +111,12 @@ type VmSSHResponse struct {
}
// Returns the unmodified JSON received from the API
-func (r VmSSHResponse) RawJSON() string { return r.JSON.raw }
-func (r *VmSSHResponse) UnmarshalJSON(data []byte) error {
+func (r VmsshResponse) RawJSON() string { return r.JSON.raw }
+func (r *VmsshResponse) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
-type VmSSHResponseSSHHostKey struct {
+type VmsshResponseSSHHostKey struct {
Base64EncodedKey string `json:"base64_encoded_key,required"`
KeyType string `json:"key_type,required"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
@@ -126,15 +129,15 @@ type VmSSHResponseSSHHostKey struct {
}
// Returns the unmodified JSON received from the API
-func (r VmSSHResponseSSHHostKey) RawJSON() string { return r.JSON.raw }
-func (r *VmSSHResponseSSHHostKey) UnmarshalJSON(data []byte) error {
+func (r VmsshResponseSSHHostKey) RawJSON() string { return r.JSON.raw }
+func (r *VmsshResponseSSHHostKey) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
-type VmLogsParams struct {
+type VMLogsParams struct {
InstanceID string `query:"instance_id,required" json:"-"`
// Any of "seqnum_asc", "seqnum_desc".
- OrderBy VmLogsParamsOrderBy `query:"order_by,omitzero,required" json:"-"`
+ OrderBy VMLogsParamsOrderBy `query:"order_by,omitzero,required" json:"-"`
BeforeRealtimeTimestamp param.Opt[string] `query:"before_realtime_timestamp,omitzero" json:"-"`
BeforeSeqnum param.Opt[int64] `query:"before_seqnum,omitzero" json:"-"`
Limit param.Opt[int64] `query:"limit,omitzero" json:"-"`
@@ -143,28 +146,28 @@ type VmLogsParams struct {
paramObj
}
-// URLQuery serializes [VmLogsParams]'s query parameters as `url.Values`.
-func (r VmLogsParams) URLQuery() (v url.Values, err error) {
+// URLQuery serializes [VMLogsParams]'s query parameters as `url.Values`.
+func (r VMLogsParams) URLQuery() (v url.Values, err error) {
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
ArrayFormat: apiquery.ArrayQueryFormatRepeat,
NestedFormat: apiquery.NestedQueryFormatBrackets,
})
}
-type VmLogsParamsOrderBy string
+type VMLogsParamsOrderBy string
const (
- VmLogsParamsOrderBySeqnumAsc VmLogsParamsOrderBy = "seqnum_asc"
- VmLogsParamsOrderBySeqnumDesc VmLogsParamsOrderBy = "seqnum_desc"
+ VMLogsParamsOrderBySeqnumAsc VMLogsParamsOrderBy = "seqnum_asc"
+ VMLogsParamsOrderBySeqnumDesc VMLogsParamsOrderBy = "seqnum_desc"
)
-type VmSSHParams struct {
- VmID string `query:"vm_id,required" json:"-"`
+type VMSSHParams struct {
+ VMID string `query:"vm_id,required" json:"-"`
paramObj
}
-// URLQuery serializes [VmSSHParams]'s query parameters as `url.Values`.
-func (r VmSSHParams) URLQuery() (v url.Values, err error) {
+// URLQuery serializes [VMSSHParams]'s query parameters as `url.Values`.
+func (r VMSSHParams) URLQuery() (v url.Values, err error) {
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
ArrayFormat: apiquery.ArrayQueryFormatRepeat,
NestedFormat: apiquery.NestedQueryFormatBrackets,
diff --git a/vm_test.go b/vm_test.go
index ab9821e..2adcbbd 100644
--- a/vm_test.go
+++ b/vm_test.go
@@ -13,7 +13,7 @@ import (
"github.com/sfcompute/nodes-go/option"
)
-func TestVmLogsWithOptionalParams(t *testing.T) {
+func TestVMLogsWithOptionalParams(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -26,9 +26,9 @@ func TestVmLogsWithOptionalParams(t *testing.T) {
option.WithBaseURL(baseURL),
option.WithBearerToken("My Bearer Token"),
)
- _, err := client.Vms.Logs(context.TODO(), sfcnodes.VmLogsParams{
+ _, err := client.VMs.Logs(context.TODO(), sfcnodes.VMLogsParams{
InstanceID: "instance_id",
- OrderBy: sfcnodes.VmLogsParamsOrderBySeqnumAsc,
+ OrderBy: sfcnodes.VMLogsParamsOrderBySeqnumAsc,
BeforeRealtimeTimestamp: sfcnodes.String("before_realtime_timestamp"),
BeforeSeqnum: sfcnodes.Int(0),
Limit: sfcnodes.Int(1),
@@ -44,7 +44,7 @@ func TestVmLogsWithOptionalParams(t *testing.T) {
}
}
-func TestVmSSH(t *testing.T) {
+func TestVMSSH(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -57,8 +57,8 @@ func TestVmSSH(t *testing.T) {
option.WithBaseURL(baseURL),
option.WithBearerToken("My Bearer Token"),
)
- _, err := client.Vms.SSH(context.TODO(), sfcnodes.VmSSHParams{
- VmID: "vm_id",
+ _, err := client.VMs.SSH(context.TODO(), sfcnodes.VMSSHParams{
+ VMID: "vm_id",
})
if err != nil {
var apierr *sfcnodes.Error
diff --git a/vmimage.go b/vmimage.go
new file mode 100644
index 0000000..90fcc19
--- /dev/null
+++ b/vmimage.go
@@ -0,0 +1,155 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package sfcnodes
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "slices"
+
+ "github.com/sfcompute/nodes-go/internal/apijson"
+ "github.com/sfcompute/nodes-go/internal/requestconfig"
+ "github.com/sfcompute/nodes-go/option"
+ "github.com/sfcompute/nodes-go/packages/respjson"
+)
+
+// VMImageService contains methods and other services that help with interacting
+// with the sfc-nodes 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 [NewVMImageService] method instead.
+type VMImageService struct {
+ Options []option.RequestOption
+}
+
+// NewVMImageService 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 NewVMImageService(opts ...option.RequestOption) (r VMImageService) {
+ r = VMImageService{}
+ r.Options = opts
+ return
+}
+
+// List all VM Images for the authenticated account
+func (r *VMImageService) List(ctx context.Context, opts ...option.RequestOption) (res *VMImageListResponse, err error) {
+ opts = slices.Concat(r.Options, opts)
+ path := "v1/vms/images"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
+// Get the download URL for a VM image by ID
+func (r *VMImageService) Get(ctx context.Context, imageID string, opts ...option.RequestOption) (res *VMImageGetResponse, err error) {
+ opts = slices.Concat(r.Options, opts)
+ if imageID == "" {
+ err = errors.New("missing required image_id parameter")
+ return
+ }
+ path := fmt.Sprintf("v1/vms/images/%s", imageID)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
+// Response body for listing images
+type VMImageListResponse struct {
+ Data []VMImageListResponseData `json:"data,required"`
+ HasMore bool `json:"has_more,required"`
+ // Any of "list".
+ Object VMImageListResponseObject `json:"object,required"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ Data respjson.Field
+ HasMore respjson.Field
+ Object respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r VMImageListResponse) RawJSON() string { return r.JSON.raw }
+func (r *VMImageListResponse) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+// Response body for individual image info (used in lists)
+type VMImageListResponseData struct {
+ // Creation timestamp as Unix timestamp in seconds
+ CreatedAt int64 `json:"created_at,required"`
+ // The image ID
+ ImageID string `json:"image_id,required"`
+ // Client given name of the image
+ Name string `json:"name,required"`
+ // Any of "image".
+ Object string `json:"object,required"`
+ // Upload status of the image
+ UploadStatus string `json:"upload_status,required"`
+ // SHA256 hash of the image file for integrity verification
+ Sha256Hash string `json:"sha256_hash,nullable"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ CreatedAt respjson.Field
+ ImageID respjson.Field
+ Name respjson.Field
+ Object respjson.Field
+ UploadStatus respjson.Field
+ Sha256Hash respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r VMImageListResponseData) RawJSON() string { return r.JSON.raw }
+func (r *VMImageListResponseData) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type VMImageListResponseObject string
+
+const (
+ VMImageListResponseObjectList VMImageListResponseObject = "list"
+)
+
+// Response body for image download presigned URL generation
+type VMImageGetResponse struct {
+ // The presigned URL that can be used to download the image
+ DownloadURL string `json:"download_url,required"`
+ // Timestamp when the presigned URL expires (RFC 3339 format)
+ ExpiresAt string `json:"expires_at,required"`
+ // The image ID
+ ImageID string `json:"image_id,required"`
+ // Human readable name of the image
+ Name string `json:"name,required"`
+ // Any of "image".
+ Object VMImageGetResponseObject `json:"object,required"`
+ // SHA256 hash of the image file for integrity verification
+ Sha256Hash string `json:"sha256_hash,required"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ DownloadURL respjson.Field
+ ExpiresAt respjson.Field
+ ImageID respjson.Field
+ Name respjson.Field
+ Object respjson.Field
+ Sha256Hash respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r VMImageGetResponse) RawJSON() string { return r.JSON.raw }
+func (r *VMImageGetResponse) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type VMImageGetResponseObject string
+
+const (
+ VMImageGetResponseObjectImage VMImageGetResponseObject = "image"
+)
diff --git a/vmimage_test.go b/vmimage_test.go
new file mode 100644
index 0000000..d27aa39
--- /dev/null
+++ b/vmimage_test.go
@@ -0,0 +1,60 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package sfcnodes_test
+
+import (
+ "context"
+ "errors"
+ "os"
+ "testing"
+
+ "github.com/sfcompute/nodes-go"
+ "github.com/sfcompute/nodes-go/internal/testutil"
+ "github.com/sfcompute/nodes-go/option"
+)
+
+func TestVMImageList(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 := sfcnodes.NewClient(
+ option.WithBaseURL(baseURL),
+ option.WithBearerToken("My Bearer Token"),
+ )
+ _, err := client.VMs.Images.List(context.TODO())
+ if err != nil {
+ var apierr *sfcnodes.Error
+ if errors.As(err, &apierr) {
+ t.Log(string(apierr.DumpRequest(true)))
+ }
+ t.Fatalf("err should be nil: %s", err.Error())
+ }
+}
+
+func TestVMImageGet(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 := sfcnodes.NewClient(
+ option.WithBaseURL(baseURL),
+ option.WithBearerToken("My Bearer Token"),
+ )
+ _, err := client.VMs.Images.Get(context.TODO(), "image_id")
+ if err != nil {
+ var apierr *sfcnodes.Error
+ if errors.As(err, &apierr) {
+ t.Log(string(apierr.DumpRequest(true)))
+ }
+ t.Fatalf("err should be nil: %s", err.Error())
+ }
+}
diff --git a/vmscript.go b/vmscript.go
index 278d01a..b44b255 100644
--- a/vmscript.go
+++ b/vmscript.go
@@ -6,6 +6,7 @@ import (
"context"
"encoding/json"
"net/http"
+ "slices"
"github.com/sfcompute/nodes-go/internal/apijson"
"github.com/sfcompute/nodes-go/internal/requestconfig"
@@ -14,34 +15,34 @@ import (
"github.com/sfcompute/nodes-go/packages/respjson"
)
-// VmScriptService contains methods and other services that help with interacting
+// VMScriptService contains methods and other services that help with interacting
// with the sfc-nodes 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 [NewVmScriptService] method instead.
-type VmScriptService struct {
+// the [NewVMScriptService] method instead.
+type VMScriptService struct {
Options []option.RequestOption
}
-// NewVmScriptService generates a new service that applies the given options to
+// NewVMScriptService 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 NewVmScriptService(opts ...option.RequestOption) (r VmScriptService) {
- r = VmScriptService{}
+func NewVMScriptService(opts ...option.RequestOption) (r VMScriptService) {
+ r = VMScriptService{}
r.Options = opts
return
}
-func (r *VmScriptService) New(ctx context.Context, body VmScriptNewParams, opts ...option.RequestOption) (res *VmScriptNewResponse, err error) {
- opts = append(r.Options[:], opts...)
+func (r *VMScriptService) New(ctx context.Context, body VMScriptNewParams, opts ...option.RequestOption) (res *VMScriptNewResponse, err error) {
+ opts = slices.Concat(r.Options, opts)
path := "v0/vms/script"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
return
}
-func (r *VmScriptService) Get(ctx context.Context, opts ...option.RequestOption) (res *VmScriptGetResponse, err error) {
- opts = append(r.Options[:], opts...)
+func (r *VMScriptService) Get(ctx context.Context, opts ...option.RequestOption) (res *VMScriptGetResponse, err error) {
+ opts = slices.Concat(r.Options, opts)
path := "v0/vms/script"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
return
@@ -117,7 +118,7 @@ func (u *UserDataUnionParam) asAny() any {
return nil
}
-type VmScriptNewResponse struct {
+type VMScriptNewResponse struct {
// if the script is valid utf8 then the response may be in either string, or byte
// form and the client must handle both
Script UserDataUnion `json:"script,required"`
@@ -130,12 +131,12 @@ type VmScriptNewResponse struct {
}
// Returns the unmodified JSON received from the API
-func (r VmScriptNewResponse) RawJSON() string { return r.JSON.raw }
-func (r *VmScriptNewResponse) UnmarshalJSON(data []byte) error {
+func (r VMScriptNewResponse) RawJSON() string { return r.JSON.raw }
+func (r *VMScriptNewResponse) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
-type VmScriptGetResponse struct {
+type VMScriptGetResponse struct {
// if the script is valid utf8 then the response may be in either string, or byte
// form and the client must handle both
Script UserDataUnion `json:"script,required"`
@@ -148,22 +149,22 @@ type VmScriptGetResponse struct {
}
// Returns the unmodified JSON received from the API
-func (r VmScriptGetResponse) RawJSON() string { return r.JSON.raw }
-func (r *VmScriptGetResponse) UnmarshalJSON(data []byte) error {
+func (r VMScriptGetResponse) RawJSON() string { return r.JSON.raw }
+func (r *VMScriptGetResponse) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
-type VmScriptNewParams struct {
+type VMScriptNewParams struct {
// if the script is valid utf8 then the response may be in either string, or byte
// form and the client must handle both
Script UserDataUnionParam `json:"script,omitzero,required"`
paramObj
}
-func (r VmScriptNewParams) MarshalJSON() (data []byte, err error) {
- type shadow VmScriptNewParams
+func (r VMScriptNewParams) MarshalJSON() (data []byte, err error) {
+ type shadow VMScriptNewParams
return param.MarshalObject(r, (*shadow)(&r))
}
-func (r *VmScriptNewParams) UnmarshalJSON(data []byte) error {
+func (r *VMScriptNewParams) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
diff --git a/vmscript_test.go b/vmscript_test.go
index 5a8ffb1..78af7cd 100644
--- a/vmscript_test.go
+++ b/vmscript_test.go
@@ -13,7 +13,7 @@ import (
"github.com/sfcompute/nodes-go/option"
)
-func TestVmScriptNew(t *testing.T) {
+func TestVMScriptNew(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -26,7 +26,7 @@ func TestVmScriptNew(t *testing.T) {
option.WithBaseURL(baseURL),
option.WithBearerToken("My Bearer Token"),
)
- _, err := client.Vms.Script.New(context.TODO(), sfcnodes.VmScriptNewParams{
+ _, err := client.VMs.Script.New(context.TODO(), sfcnodes.VMScriptNewParams{
Script: sfcnodes.UserDataUnionParam{
OfString: sfcnodes.String("string"),
},
@@ -40,7 +40,7 @@ func TestVmScriptNew(t *testing.T) {
}
}
-func TestVmScriptGet(t *testing.T) {
+func TestVMScriptGet(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 TestVmScriptGet(t *testing.T) {
option.WithBaseURL(baseURL),
option.WithBearerToken("My Bearer Token"),
)
- _, err := client.Vms.Script.Get(context.TODO())
+ _, err := client.VMs.Script.Get(context.TODO())
if err != nil {
var apierr *sfcnodes.Error
if errors.As(err, &apierr) {