Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b42404d293 | ||
|
|
6069f64a3e | ||
|
|
06b11d8121 | ||
|
|
4f7f1f0fba | ||
|
|
0a1b3ac8c0 |
@@ -44,6 +44,25 @@ type AddSSHKeyRequest struct {
|
|||||||
SSHKeyID string `json:"ssh_key_id"`
|
SSHKeyID string `json:"ssh_key_id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
type ProvisionSSHKeyRequest struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
PublicKey string `json:"public_key,omitempty"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
type RotateSSHKeyRequest struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
PublicKey string `json:"public_key,omitempty"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
ReplaceKeyID string `json:"replace_key_id,omitempty"`
|
||||||
|
}
|
||||||
|
type SSHPrivateKey struct {
|
||||||
|
KeyID string `json:"key_id"`
|
||||||
|
KeyName string `json:"key_name"`
|
||||||
|
Fingerprint string `json:"fingerprint"`
|
||||||
|
Algorithm string `json:"algorithm"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
PrivateKey Secret `json:"private_key"`
|
||||||
|
}
|
||||||
|
|
||||||
func (s *AccessService) GetLoginInfo(ctx context.Context, region, instanceID string) (*LoginInfo, error) {
|
func (s *AccessService) GetLoginInfo(ctx context.Context, region, instanceID string) (*LoginInfo, error) {
|
||||||
var result LoginInfo
|
var result LoginInfo
|
||||||
@@ -63,9 +82,21 @@ func (s *AccessService) ListSSHKeys(ctx context.Context, region, instanceID stri
|
|||||||
err := s.client.do(ctx, http.MethodGet, instancesPath(region, instanceID)+"/ssh-keys", nil, &result, "")
|
err := s.client.do(ctx, http.MethodGet, instancesPath(region, instanceID)+"/ssh-keys", nil, &result, "")
|
||||||
return &result, err
|
return &result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *AccessService) RevealSSHKey(ctx context.Context, region, instanceID, keyID string) (*SSHPrivateKey, error) {
|
||||||
|
var result SSHPrivateKey
|
||||||
|
err := s.client.do(ctx, http.MethodPost, instancesPath(region, instanceID)+"/ssh-keys/"+url.PathEscape(keyID)+"/reveal", map[string]any{}, &result, newID())
|
||||||
|
return &result, err
|
||||||
|
}
|
||||||
func (s *AccessService) AddSSHKey(ctx context.Context, region, instanceID string, input AddSSHKeyRequest, options ...RequestOption) (*Operation, error) {
|
func (s *AccessService) AddSSHKey(ctx context.Context, region, instanceID string, input AddSSHKeyRequest, options ...RequestOption) (*Operation, error) {
|
||||||
return (&InstancesService{client: s.client}).operation(ctx, http.MethodPost, instancesPath(region, instanceID)+"/ssh-keys", input, options...)
|
return (&InstancesService{client: s.client}).operation(ctx, http.MethodPost, instancesPath(region, instanceID)+"/ssh-keys", input, options...)
|
||||||
}
|
}
|
||||||
|
func (s *AccessService) ProvisionSSHKey(ctx context.Context, region, instanceID string, input ProvisionSSHKeyRequest, options ...RequestOption) (*Operation, error) {
|
||||||
|
return (&InstancesService{client: s.client}).operation(ctx, http.MethodPost, instancesPath(region, instanceID)+"/ssh-keys/provision", input, options...)
|
||||||
|
}
|
||||||
|
func (s *AccessService) RotateSSHKey(ctx context.Context, region, instanceID string, input RotateSSHKeyRequest, options ...RequestOption) (*Operation, error) {
|
||||||
|
return (&InstancesService{client: s.client}).operation(ctx, http.MethodPost, instancesPath(region, instanceID)+"/ssh-keys/rotate", input, options...)
|
||||||
|
}
|
||||||
func (s *AccessService) RemoveSSHKey(ctx context.Context, region, instanceID, keyID, username string, options ...RequestOption) (*Operation, error) {
|
func (s *AccessService) RemoveSSHKey(ctx context.Context, region, instanceID, keyID, username string, options ...RequestOption) (*Operation, error) {
|
||||||
path := instancesPath(region, instanceID) + "/ssh-keys/" + escaped(keyID)
|
path := instancesPath(region, instanceID) + "/ssh-keys/" + escaped(keyID)
|
||||||
if username != "" {
|
if username != "" {
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ func (s *PricingService) EstimateExistingInstance(ctx context.Context, region, i
|
|||||||
err := s.client.do(ctx, http.MethodPost, instancesPath(region, instanceID)+"/price-estimate", map[string]any{}, &result, "")
|
err := s.client.do(ctx, http.MethodPost, instancesPath(region, instanceID)+"/price-estimate", map[string]any{}, &result, "")
|
||||||
return &result, err
|
return &result, err
|
||||||
}
|
}
|
||||||
|
func (s *PricingService) EstimateInstanceChange(ctx context.Context, region, instanceID string, input InstanceChangePriceRequest) (*InstanceChangePriceEstimate, error) {
|
||||||
|
var result InstanceChangePriceEstimate
|
||||||
|
err := s.client.do(ctx, http.MethodPost, instancesPath(region, instanceID)+"/change-price-estimate", input, &result, newID())
|
||||||
|
return &result, err
|
||||||
|
}
|
||||||
func catalogPath(region, resource string, options ListOptions) string {
|
func catalogPath(region, resource string, options ListOptions) string {
|
||||||
values := url.Values{}
|
values := url.Values{}
|
||||||
if options.Query != "" {
|
if options.Query != "" {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Version = "0.1.0"
|
const Version = "0.6.0"
|
||||||
|
|
||||||
type Logger interface {
|
type Logger interface {
|
||||||
Printf(format string, args ...any)
|
Printf(format string, args ...any)
|
||||||
|
|||||||
+141
@@ -21,6 +21,24 @@ func testClient(t *testing.T, handler http.HandlerFunc) (*Client, *httptest.Serv
|
|||||||
return client, server
|
return client, server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateOptionsDecodeDetailedImageAndInstanceTypeMetadata(t *testing.T) {
|
||||||
|
var options CreateOptions
|
||||||
|
raw := `{
|
||||||
|
"region":"ap-northeast-1",
|
||||||
|
"images":[{"catalog_id":"image-row","region":"ap-northeast-1","category_id":"linux","image_id":"ami-1","source_scope":"quick-start","source_name":"al2023","display_name":"Amazon Linux","availability_status":"available","description":"Linux image","architecture":"x86_64","platform_details":"Linux/UNIX","os_family":"linux","recommended_user":"ec2-user","root_volume_size_gib":8,"root_volume_type":"gp3","virtualization_type":"hvm","ena_support":true,"public":true}],
|
||||||
|
"instance_types":[{"id":"type-row","region":"ap-northeast-1","category_id":"general","instance_type":"g4dn.xlarge","vcpus":4,"memory_mib":16384,"architectures":["x86_64"],"network_performance":"Up to 25 Gigabit","network_baseline_gbps":5,"network_peak_gbps":25,"gpu_count":1,"gpu_memory_mib":16384,"gpus":[{"manufacturer":"NVIDIA","name":"T4","count":1,"memory_mib":16384}],"current_generation":true}]
|
||||||
|
}`
|
||||||
|
if err := json.Unmarshal([]byte(raw), &options); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(options.Images) != 1 || options.Images[0].Architecture != "x86_64" || options.Images[0].RecommendedUser != "ec2-user" || options.Images[0].RootVolumeSizeGiB != 8 || !options.Images[0].ENASupport {
|
||||||
|
t.Fatalf("detailed image metadata was not decoded: %#v", options.Images)
|
||||||
|
}
|
||||||
|
if len(options.InstanceTypes) != 1 || options.InstanceTypes[0].NetworkPeakGbps != 25 || options.InstanceTypes[0].GPUMemoryMiB != 16384 || len(options.InstanceTypes[0].GPUs) != 1 || options.InstanceTypes[0].GPUs[0].Name != "T4" {
|
||||||
|
t.Fatalf("detailed instance type metadata was not decoded: %#v", options.InstanceTypes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestClientSendsAuthAndStableIdempotencyOnRetry(t *testing.T) {
|
func TestClientSendsAuthAndStableIdempotencyOnRetry(t *testing.T) {
|
||||||
var mutex sync.Mutex
|
var mutex sync.Mutex
|
||||||
attempts := 0
|
attempts := 0
|
||||||
@@ -53,6 +71,65 @@ func TestClientSendsAuthAndStableIdempotencyOnRetry(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProvisionAndRotateSSHKeyRequests(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
path string
|
||||||
|
call func(*Client) (*Operation, error)
|
||||||
|
want map[string]string
|
||||||
|
}{
|
||||||
|
{path: "/api/sdk/v1/regions/us-east-1/instances/instance-1/ssh-keys/provision", call: func(client *Client) (*Operation, error) {
|
||||||
|
return client.Access.ProvisionSSHKey(context.Background(), "us-east-1", "instance-1", ProvisionSSHKeyRequest{Name: "uploaded", PublicKey: "ssh-ed25519 AAAA", Username: "ec2-user"}, WithIdempotencyKey("add-key"))
|
||||||
|
}, want: map[string]string{"name": "uploaded", "public_key": "ssh-ed25519 AAAA", "username": "ec2-user"}},
|
||||||
|
{path: "/api/sdk/v1/regions/us-east-1/instances/instance-1/ssh-keys/rotate", call: func(client *Client) (*Operation, error) {
|
||||||
|
return client.Access.RotateSSHKey(context.Background(), "us-east-1", "instance-1", RotateSSHKeyRequest{Username: "ec2-user", ReplaceKeyID: "old-key"}, WithIdempotencyKey("rotate-key"))
|
||||||
|
}, want: map[string]string{"username": "ec2-user", "replace_key_id": "old-key"}},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.path, func(t *testing.T) {
|
||||||
|
client, _ := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost || r.URL.Path != test.path {
|
||||||
|
t.Fatalf("request = %s %s", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
var body map[string]string
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for key, value := range test.want {
|
||||||
|
if body[key] != value {
|
||||||
|
t.Fatalf("body[%s] = %q, want %q", key, body[key], value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"code":202,"message":"Accepted","data":{"command_id":"command-1","status":"queued"}}`))
|
||||||
|
})
|
||||||
|
operation, err := test.call(client)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if operation.TaskID != "command-1" {
|
||||||
|
t.Fatalf("task ID = %q", operation.TaskID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRevealSSHKeyRequestAndSecret(t *testing.T) {
|
||||||
|
client, _ := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost || r.URL.Path != "/api/sdk/v1/regions/us-east-1/instances/instance-1/ssh-keys/key-1/reveal" {
|
||||||
|
t.Fatalf("request = %s %s", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"code":200,"message":"Success","data":{"key_id":"key-1","key_name":"generated","username":"ec2-user","private_key":"secret-pem"}}`))
|
||||||
|
})
|
||||||
|
key, err := client.Access.RevealSSHKey(context.Background(), "us-east-1", "instance-1", "key-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if key.KeyID != "key-1" || key.PrivateKey.Reveal() != "secret-pem" || key.PrivateKey.String() != "[REDACTED]" {
|
||||||
|
t.Fatalf("unexpected private key response: %#v", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAPIErrorAndHelpers(t *testing.T) {
|
func TestAPIErrorAndHelpers(t *testing.T) {
|
||||||
client, _ := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
client, _ := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -138,3 +215,67 @@ func TestRuntimePricingAndInstanceEIPOperations(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInstanceChangePricingAndAsyncTraffic(t *testing.T) {
|
||||||
|
paths := []string{}
|
||||||
|
client, _ := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
paths = append(paths, r.Method+" "+r.URL.Path)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if r.URL.Path == "/api/sdk/v1/regions/us-east-1/instances/local-1/change-price-estimate" {
|
||||||
|
var body InstanceChangePriceRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Kind != InstanceChangeTrafficLimit || body.LimitGiB == nil || *body.LimitGiB != 200 {
|
||||||
|
t.Fatalf("unexpected quote body %#v err=%v", body, err)
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte(`{"code":200,"message":"Success","data":{"kind":"traffic_limit","currency":"USD","charge_price":5.5,"configuration_fingerprint":"fp"}}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Header.Get("Idempotency-Key") != "traffic-order-1" {
|
||||||
|
t.Fatalf("missing stable idempotency key")
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte(`{"code":202,"message":"Accepted","data":{"task_id":"task-traffic","status":"queued"}}`))
|
||||||
|
})
|
||||||
|
limit := 200.0
|
||||||
|
estimate, err := client.Pricing.EstimateInstanceChange(context.Background(), "us-east-1", "local-1", InstanceChangePriceRequest{Kind: InstanceChangeTrafficLimit, PeriodEnd: time.Now().AddDate(0, 1, 0), LimitGiB: &limit})
|
||||||
|
if err != nil || estimate.ConfigurationFingerprint != "fp" || estimate.ChargePrice != 5.5 {
|
||||||
|
t.Fatalf("estimate=%#v err=%v", estimate, err)
|
||||||
|
}
|
||||||
|
operation, err := client.Traffic.UpdateAsync(context.Background(), "us-east-1", "local-1", TrafficLimitRequest{LimitGiB: &limit, ExpectedConfigurationFingerprint: "fp"}, WithIdempotencyKey("traffic-order-1"))
|
||||||
|
if err != nil || operation.TaskID != "task-traffic" {
|
||||||
|
t.Fatalf("operation=%#v err=%v", operation, err)
|
||||||
|
}
|
||||||
|
want := []string{"POST /api/sdk/v1/regions/us-east-1/instances/local-1/change-price-estimate", "POST /api/sdk/v1/regions/us-east-1/instances/local-1/traffic-limit/update"}
|
||||||
|
if len(paths) != len(want) || paths[0] != want[0] || paths[1] != want[1] {
|
||||||
|
t.Fatalf("paths=%v want=%v", paths, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandRootVolumePathAndBody(t *testing.T) {
|
||||||
|
client, _ := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPatch || r.URL.Path != "/api/sdk/v1/regions/us-east-1/instances/local-1/root-volume" {
|
||||||
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
var body ExpandRootVolumeRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.SizeGiB != 80 || body.ExpectedConfigurationFingerprint != "fp" {
|
||||||
|
t.Fatalf("unexpected body %#v err=%v", body, err)
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte(`{"code":202,"message":"Accepted","data":{"task_id":"task-root","status":"queued"}}`))
|
||||||
|
})
|
||||||
|
operation, err := client.Volumes.ExpandRoot(context.Background(), "us-east-1", "local-1", ExpandRootVolumeRequest{SizeGiB: 80, ExpectedConfigurationFingerprint: "fp"}, WithIdempotencyKey("root-expand"))
|
||||||
|
if err != nil || operation.TaskID != "task-root" {
|
||||||
|
t.Fatalf("operation=%#v err=%v", operation, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstanceDetailsExposeCatalogMetadata(t *testing.T) {
|
||||||
|
client, _ := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"code":200,"message":"Success","data":{"instance":{"instance_id":"i-1"},"instance_type_details":{"instance_type":"m7i.large","vcpus":2,"catalog_match":true},"image_details":{"image_id":"ami-1","display_name":"Ubuntu 24.04","catalog_match":true}}}`))
|
||||||
|
})
|
||||||
|
details, err := client.Instances.GetDetails(context.Background(), "us-east-1", "local-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if details.InstanceTypeDetails()["instance_type"] != "m7i.large" || details.ImageDetails()["display_name"] != "Ubuntu 24.04" {
|
||||||
|
t.Fatalf("catalog details were not decoded: %#v", details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package awsserversdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRetryCreatePreservesIdentityAndKeyOnTransientFailure(t *testing.T) {
|
||||||
|
requests := 0
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requests++
|
||||||
|
if r.Method != "POST" || r.URL.Path != "/api/sdk/v1/regions/us-east-1/instances/local-1/retry" || r.Header.Get("Idempotency-Key") != "create-retry:1:2" {
|
||||||
|
t.Errorf("unexpected retry request: %s %s", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
var body map[string]any
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || len(body) != 0 {
|
||||||
|
t.Errorf("retry payload must not select another account: %v %v", body, err)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if requests == 1 {
|
||||||
|
w.WriteHeader(503)
|
||||||
|
w.Write([]byte(`{"code":503,"message":"temporarily unavailable"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(202)
|
||||||
|
w.Write([]byte(`{"code":202,"data":{"id":"local-1","task_id":"retry-task","placement_id":"original"}}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
client, err := NewClient(server.URL, "awsapp_test_secret", WithMaxRetries(1))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
operation, err := client.Instances.RetryCreate(context.Background(), "us-east-1", "local-1", WithIdempotencyKey("create-retry:1:2"))
|
||||||
|
if err != nil || operation.ID != "local-1" || operation.TaskID != "retry-task" || operation.PlacementID != "original" || requests != 2 {
|
||||||
|
t.Fatalf("result=%+v requests=%d err=%v", operation, requests, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-4
@@ -18,10 +18,11 @@ type ResizeInstanceRequest struct {
|
|||||||
InstanceType string `json:"instance_type"`
|
InstanceType string `json:"instance_type"`
|
||||||
}
|
}
|
||||||
type RebuildInstanceRequest struct {
|
type RebuildInstanceRequest struct {
|
||||||
ImageID string `json:"image_id"`
|
ImageID string `json:"image_id"`
|
||||||
InstanceType string `json:"instance_type,omitempty"`
|
InstanceType string `json:"instance_type,omitempty"`
|
||||||
AssociateEIP bool `json:"associate_eip,omitempty"`
|
AssociateEIP bool `json:"associate_eip,omitempty"`
|
||||||
InstallAgent *bool `json:"install_agent,omitempty"`
|
InstallAgent *bool `json:"install_agent,omitempty"`
|
||||||
|
ExpectedConfigurationFingerprint string `json:"expected_configuration_fingerprint,omitempty"`
|
||||||
}
|
}
|
||||||
type RefreshDetailsRequest struct {
|
type RefreshDetailsRequest struct {
|
||||||
Sections []string `json:"sections,omitempty"`
|
Sections []string `json:"sections,omitempty"`
|
||||||
|
|||||||
+31
-16
@@ -37,7 +37,8 @@ type EIPInfo struct {
|
|||||||
Available []map[string]any `json:"available"`
|
Available []map[string]any `json:"available"`
|
||||||
}
|
}
|
||||||
type EnsureEIPRequest struct {
|
type EnsureEIPRequest struct {
|
||||||
AllocationID string `json:"allocation_id,omitempty"`
|
AllocationID string `json:"allocation_id,omitempty"`
|
||||||
|
ExpectedConfigurationFingerprint string `json:"expected_configuration_fingerprint,omitempty"`
|
||||||
}
|
}
|
||||||
type ReverseDNSRequest struct {
|
type ReverseDNSRequest struct {
|
||||||
DomainName string `json:"domain_name,omitempty"`
|
DomainName string `json:"domain_name,omitempty"`
|
||||||
@@ -63,25 +64,31 @@ func (s *EIPService) SetReverseDNS(ctx context.Context, region, instanceID strin
|
|||||||
|
|
||||||
type Volume map[string]any
|
type Volume map[string]any
|
||||||
type CreateVolumeRequest struct {
|
type CreateVolumeRequest struct {
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
DeviceName string `json:"device_name"`
|
DeviceName string `json:"device_name"`
|
||||||
SizeGiB int32 `json:"size_gib"`
|
SizeGiB int32 `json:"size_gib"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
IOPS int32 `json:"iops,omitempty"`
|
IOPS int32 `json:"iops,omitempty"`
|
||||||
Throughput int32 `json:"throughput,omitempty"`
|
Throughput int32 `json:"throughput,omitempty"`
|
||||||
Encrypted bool `json:"encrypted"`
|
Encrypted bool `json:"encrypted"`
|
||||||
KMSKeyID string `json:"kms_key_id,omitempty"`
|
KMSKeyID string `json:"kms_key_id,omitempty"`
|
||||||
DeleteOnTermination bool `json:"delete_on_termination"`
|
DeleteOnTermination bool `json:"delete_on_termination"`
|
||||||
|
ExpectedConfigurationFingerprint string `json:"expected_configuration_fingerprint,omitempty"`
|
||||||
}
|
}
|
||||||
type AttachVolumeRequest struct {
|
type AttachVolumeRequest struct {
|
||||||
DeviceName string `json:"device_name"`
|
DeviceName string `json:"device_name"`
|
||||||
DeleteOnTermination bool `json:"delete_on_termination"`
|
DeleteOnTermination bool `json:"delete_on_termination"`
|
||||||
}
|
}
|
||||||
type ModifyVolumeRequest struct {
|
type ModifyVolumeRequest struct {
|
||||||
SizeGiB int32 `json:"size_gib,omitempty"`
|
SizeGiB int32 `json:"size_gib,omitempty"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
IOPS int32 `json:"iops,omitempty"`
|
IOPS int32 `json:"iops,omitempty"`
|
||||||
Throughput int32 `json:"throughput,omitempty"`
|
Throughput int32 `json:"throughput,omitempty"`
|
||||||
|
ExpectedConfigurationFingerprint string `json:"expected_configuration_fingerprint,omitempty"`
|
||||||
|
}
|
||||||
|
type ExpandRootVolumeRequest struct {
|
||||||
|
SizeGiB int32 `json:"size_gib"`
|
||||||
|
ExpectedConfigurationFingerprint string `json:"expected_configuration_fingerprint,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *VolumeService) List(ctx context.Context, region, instanceID string) ([]Volume, error) {
|
func (s *VolumeService) List(ctx context.Context, region, instanceID string) ([]Volume, error) {
|
||||||
@@ -103,14 +110,18 @@ func (s *VolumeService) Detach(ctx context.Context, region, instanceID, volumeID
|
|||||||
func (s *VolumeService) Modify(ctx context.Context, region, instanceID, volumeID string, input ModifyVolumeRequest, options ...RequestOption) (*Operation, error) {
|
func (s *VolumeService) Modify(ctx context.Context, region, instanceID, volumeID string, input ModifyVolumeRequest, options ...RequestOption) (*Operation, error) {
|
||||||
return (&InstancesService{client: s.client}).operation(ctx, http.MethodPatch, instancesPath(region, instanceID)+"/volumes/"+escaped(volumeID), input, options...)
|
return (&InstancesService{client: s.client}).operation(ctx, http.MethodPatch, instancesPath(region, instanceID)+"/volumes/"+escaped(volumeID), input, options...)
|
||||||
}
|
}
|
||||||
|
func (s *VolumeService) ExpandRoot(ctx context.Context, region, instanceID string, input ExpandRootVolumeRequest, options ...RequestOption) (*Operation, error) {
|
||||||
|
return (&InstancesService{client: s.client}).operation(ctx, http.MethodPatch, instancesPath(region, instanceID)+"/root-volume", input, options...)
|
||||||
|
}
|
||||||
func (s *VolumeService) Delete(ctx context.Context, region, instanceID, volumeID string, force bool, options ...RequestOption) (*Operation, error) {
|
func (s *VolumeService) Delete(ctx context.Context, region, instanceID, volumeID string, force bool, options ...RequestOption) (*Operation, error) {
|
||||||
return (&InstancesService{client: s.client}).operation(ctx, http.MethodDelete, instancesPath(region, instanceID)+"/volumes/"+escaped(volumeID), map[string]bool{"force": force}, options...)
|
return (&InstancesService{client: s.client}).operation(ctx, http.MethodDelete, instancesPath(region, instanceID)+"/volumes/"+escaped(volumeID), map[string]bool{"force": force}, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrafficLimit map[string]any
|
type TrafficLimit map[string]any
|
||||||
type TrafficLimitRequest struct {
|
type TrafficLimitRequest struct {
|
||||||
Enabled *bool `json:"enabled,omitempty"`
|
Enabled *bool `json:"enabled,omitempty"`
|
||||||
LimitGiB *float64 `json:"limit_gib,omitempty"`
|
LimitGiB *float64 `json:"limit_gib,omitempty"`
|
||||||
|
ExpectedConfigurationFingerprint string `json:"expected_configuration_fingerprint,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TrafficService) Get(ctx context.Context, region, instanceID string) (TrafficLimit, error) {
|
func (s *TrafficService) Get(ctx context.Context, region, instanceID string) (TrafficLimit, error) {
|
||||||
@@ -123,3 +134,7 @@ func (s *TrafficService) Update(ctx context.Context, region, instanceID string,
|
|||||||
err := s.client.do(ctx, http.MethodPatch, instancesPath(region, instanceID)+"/traffic-limit", input, &result, newID())
|
err := s.client.do(ctx, http.MethodPatch, instancesPath(region, instanceID)+"/traffic-limit", input, &result, newID())
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TrafficService) UpdateAsync(ctx context.Context, region, instanceID string, input TrafficLimitRequest, options ...RequestOption) (*Operation, error) {
|
||||||
|
return (&InstancesService{client: s.client}).operation(ctx, http.MethodPost, instancesPath(region, instanceID)+"/traffic-limit/update", input, options...)
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,25 +54,66 @@ type Image struct {
|
|||||||
DisplayName string `json:"display_name"`
|
DisplayName string `json:"display_name"`
|
||||||
DisplayIntro string `json:"display_intro"`
|
DisplayIntro string `json:"display_intro"`
|
||||||
AvailabilityStatus string `json:"availability_status"`
|
AvailabilityStatus string `json:"availability_status"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Architecture string `json:"architecture"`
|
||||||
|
Platform string `json:"platform,omitempty"`
|
||||||
|
PlatformDetails string `json:"platform_details,omitempty"`
|
||||||
|
OSFamily string `json:"os_family,omitempty"`
|
||||||
|
RecommendedUser string `json:"recommended_user,omitempty"`
|
||||||
|
CreationDate string `json:"creation_date,omitempty"`
|
||||||
|
DeprecationTime string `json:"deprecation_time,omitempty"`
|
||||||
|
OwnerID string `json:"owner_id,omitempty"`
|
||||||
|
OwnerAlias string `json:"owner_alias,omitempty"`
|
||||||
|
RootDeviceType string `json:"root_device_type,omitempty"`
|
||||||
|
RootDeviceName string `json:"root_device_name,omitempty"`
|
||||||
|
RootSnapshotID string `json:"root_snapshot_id,omitempty"`
|
||||||
|
RootVolumeSizeGiB int32 `json:"root_volume_size_gib,omitempty"`
|
||||||
|
RootVolumeType string `json:"root_volume_type,omitempty"`
|
||||||
|
VirtualizationType string `json:"virtualization_type,omitempty"`
|
||||||
|
Hypervisor string `json:"hypervisor,omitempty"`
|
||||||
|
BootMode string `json:"boot_mode,omitempty"`
|
||||||
|
IMDSSupport string `json:"imds_support,omitempty"`
|
||||||
|
UsageOperation string `json:"usage_operation,omitempty"`
|
||||||
|
ENASupport bool `json:"ena_support"`
|
||||||
|
Public bool `json:"public"`
|
||||||
|
ProductCodes []string `json:"product_codes,omitempty"`
|
||||||
Metadata map[string]any `json:"metadata"`
|
Metadata map[string]any `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GPUDeviceInfo struct {
|
||||||
|
Manufacturer string `json:"manufacturer,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Count int32 `json:"count"`
|
||||||
|
LogicalCount int32 `json:"logical_count,omitempty"`
|
||||||
|
PartitionSize float64 `json:"partition_size,omitempty"`
|
||||||
|
MemoryMiB int32 `json:"memory_mib,omitempty"`
|
||||||
|
SupportedWorkloads []string `json:"supported_workloads,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type InstanceType struct {
|
type InstanceType struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
CategoryID string `json:"category_id"`
|
CategoryID string `json:"category_id"`
|
||||||
InstanceType string `json:"instance_type"`
|
InstanceType string `json:"instance_type"`
|
||||||
DisplayName string `json:"display_name"`
|
DisplayName string `json:"display_name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
VCPUs int32 `json:"vcpus"`
|
VCPUs int32 `json:"vcpus"`
|
||||||
MemoryMiB int64 `json:"memory_mib"`
|
MemoryMiB int64 `json:"memory_mib"`
|
||||||
Architectures []string `json:"architectures"`
|
Architectures []string `json:"architectures"`
|
||||||
NetworkPerformance string `json:"network_performance"`
|
NetworkPerformance string `json:"network_performance"`
|
||||||
GPUCount float64 `json:"gpu_count"`
|
NetworkBaselineGbps float64 `json:"network_baseline_gbps"`
|
||||||
OSPrices map[string]float64 `json:"os_prices"`
|
NetworkPeakGbps float64 `json:"network_peak_gbps"`
|
||||||
HourlyRate float64 `json:"hourly_rate"`
|
GPUCount float64 `json:"gpu_count"`
|
||||||
Currency string `json:"currency"`
|
GPUMemoryMiB int64 `json:"gpu_memory_mib"`
|
||||||
PriceMode string `json:"price_mode"`
|
GPUs []GPUDeviceInfo `json:"gpus"`
|
||||||
PriceValue float64 `json:"price_value"`
|
CurrentGeneration bool `json:"current_generation"`
|
||||||
|
BurstablePerformance bool `json:"burstable_performance"`
|
||||||
|
FreeTierEligible bool `json:"free_tier_eligible"`
|
||||||
|
OSPrices map[string]float64 `json:"os_prices"`
|
||||||
|
HourlyRate float64 `json:"hourly_rate"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
PriceMode string `json:"price_mode"`
|
||||||
|
PriceValue float64 `json:"price_value"`
|
||||||
}
|
}
|
||||||
type CreateOptions struct {
|
type CreateOptions struct {
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
@@ -121,6 +162,45 @@ type PriceEstimate struct {
|
|||||||
ResourceSyncedAt time.Time `json:"resource_synced_at,omitempty"`
|
ResourceSyncedAt time.Time `json:"resource_synced_at,omitempty"`
|
||||||
Raw map[string]any `json:"-"`
|
Raw map[string]any `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
InstanceChangeEIPAdd = "eip_add"
|
||||||
|
InstanceChangeEIPReplace = "eip_replace"
|
||||||
|
InstanceChangeVolumeCreate = "volume_create"
|
||||||
|
InstanceChangeVolumeExpand = "volume_expand"
|
||||||
|
InstanceChangeRootVolumeExpand = "root_volume_expand"
|
||||||
|
InstanceChangeRebuildImage = "rebuild_image"
|
||||||
|
InstanceChangeTrafficLimit = "traffic_limit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InstanceChangePriceRequest struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
PeriodEnd time.Time `json:"period_end"`
|
||||||
|
VolumeID string `json:"volume_id,omitempty"`
|
||||||
|
Volume *CreateVolumeRequest `json:"volume,omitempty"`
|
||||||
|
SizeGiB int32 `json:"size_gib,omitempty"`
|
||||||
|
ImageID string `json:"image_id,omitempty"`
|
||||||
|
LimitGiB *float64 `json:"limit_gib,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InstanceChangePriceEstimate struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
QuotedAt time.Time `json:"quoted_at"`
|
||||||
|
ValidUntil time.Time `json:"valid_until"`
|
||||||
|
PeriodEnd time.Time `json:"period_end"`
|
||||||
|
CurrentMonthlyPrice float64 `json:"current_monthly_price"`
|
||||||
|
TargetMonthlyPrice float64 `json:"target_monthly_price"`
|
||||||
|
MonthlyDelta float64 `json:"monthly_delta"`
|
||||||
|
CurrentPeriodPrice float64 `json:"current_period_price"`
|
||||||
|
TargetPeriodPrice float64 `json:"target_period_price"`
|
||||||
|
PeriodDelta float64 `json:"period_delta"`
|
||||||
|
ChargePrice float64 `json:"charge_price"`
|
||||||
|
ConfigurationFingerprint string `json:"configuration_fingerprint"`
|
||||||
|
ResourceSyncedAt time.Time `json:"resource_synced_at"`
|
||||||
|
NormalizedRequest InstanceChangePriceRequest `json:"normalized_request"`
|
||||||
|
Breakdown map[string]any `json:"breakdown"`
|
||||||
|
}
|
||||||
type Instance struct {
|
type Instance struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
InstanceID string `json:"instance_id,omitempty"`
|
InstanceID string `json:"instance_id,omitempty"`
|
||||||
@@ -143,7 +223,28 @@ type Instance struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
type CatalogResourceDetails map[string]any
|
||||||
|
|
||||||
type InstanceDetails map[string]any
|
type InstanceDetails map[string]any
|
||||||
|
|
||||||
|
func (d InstanceDetails) InstanceTypeDetails() CatalogResourceDetails {
|
||||||
|
return catalogResourceDetails(d["instance_type_details"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d InstanceDetails) ImageDetails() CatalogResourceDetails {
|
||||||
|
return catalogResourceDetails(d["image_details"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func catalogResourceDetails(value any) CatalogResourceDetails {
|
||||||
|
if details, ok := value.(map[string]any); ok {
|
||||||
|
return CatalogResourceDetails(details)
|
||||||
|
}
|
||||||
|
if details, ok := value.(CatalogResourceDetails); ok {
|
||||||
|
return details
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type Operation struct {
|
type Operation struct {
|
||||||
client *Client
|
client *Client
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
|||||||
Reference in New Issue
Block a user