Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b42404d293 | ||
|
|
6069f64a3e |
@@ -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 != "" {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Version = "0.5.0"
|
const Version = "0.6.0"
|
||||||
|
|
||||||
type Logger interface {
|
type Logger interface {
|
||||||
Printf(format string, args ...any)
|
Printf(format string, args ...any)
|
||||||
|
|||||||
@@ -71,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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user