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