Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add SSH keys implementation #33

Merged
merged 2 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ go get github.com/MagaluCloud/mgc-sdk-go
- Volumes
- Snapshots
- Volume Types
- SSH Keys

## Authentication

Expand Down
2 changes: 2 additions & 0 deletions client/regions.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const (
BrSe1 MgcUrl = "https://api.magalu.cloud/br-se1"
// BrMgl1 is the URL for the Brazil Magalu region
BrMgl1 MgcUrl = "https://api.magalu.cloud/br-se-1"
// MgcUrl is the default URL for products that don't have a specific region
Global MgcUrl = "https://api.magalu.cloud"
)

// String returns the string representation of the MgcUrl
Expand Down
96 changes: 96 additions & 0 deletions cmd/examples/sshkeys/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package main

import (
"context"
"fmt"
"log"
"os"

"github.com/MagaluCloud/mgc-sdk-go/client"
"github.com/MagaluCloud/mgc-sdk-go/helpers"
"github.com/MagaluCloud/mgc-sdk-go/sshkeys"
)

func main() {
ExampleListSSHKeys()
id := ExampleCreateSSHKey()
ExampleGetSSHKey(id)
ExampleDeleteSSHKey(id)
}

func ExampleListSSHKeys() {
apiToken := os.Getenv("MGC_API_TOKEN")
if apiToken == "" {
log.Fatal("MGC_API_TOKEN environment variable is not set")
}
c := client.NewMgcClient(apiToken)
sshClient := sshkeys.New(c)

keys, err := sshClient.Keys().List(context.Background(), sshkeys.ListOptions{
Limit: helpers.IntPtr(10),
})
if err != nil {
log.Fatal(err)
}

fmt.Printf("Found %d SSH keys:\n", len(keys))
for _, key := range keys {
fmt.Printf("SSH Key: %s (ID: %s)\n", key.Name, key.ID)
fmt.Printf(" Type: %s\n", key.KeyType)
}
}

func ExampleCreateSSHKey() string {
apiToken := os.Getenv("MGC_API_TOKEN")
if apiToken == "" {
log.Fatal("MGC_API_TOKEN environment variable is not set")
}
c := client.NewMgcClient(apiToken)
sshClient := sshkeys.New(c)

key, err := sshClient.Keys().Create(context.Background(), sshkeys.CreateSSHKeyRequest{
Name: "example-key",
Key: "ssh-rsa AAAA... example@localhost",
})
if err != nil {
log.Fatal(err)
}

fmt.Printf("Created SSH key: %s (ID: %s)\n", key.Name, key.ID)
return key.ID
}

func ExampleGetSSHKey(id string) {
apiToken := os.Getenv("MGC_API_TOKEN")
if apiToken == "" {
log.Fatal("MGC_API_TOKEN environment variable is not set")
}
c := client.NewMgcClient(apiToken)
sshClient := sshkeys.New(c)

key, err := sshClient.Keys().Get(context.Background(), id)
if err != nil {
log.Fatal(err)
}

fmt.Printf("SSH Key Details:\n")
fmt.Printf(" ID: %s\n", key.ID)
fmt.Printf(" Name: %s\n", key.Name)
fmt.Printf(" Type: %s\n", key.KeyType)
}

func ExampleDeleteSSHKey(id string) {
apiToken := os.Getenv("MGC_API_TOKEN")
if apiToken == "" {
log.Fatal("MGC_API_TOKEN environment variable is not set")
}
c := client.NewMgcClient(apiToken)
sshClient := sshkeys.New(c)

key, err := sshClient.Keys().Delete(context.Background(), id)
if err != nil {
log.Fatal(err)
}

fmt.Printf("Successfully deleted SSH key: %s (ID: %s)\n", key.Name, key.ID)
}
64 changes: 64 additions & 0 deletions sshkeys/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Package sshkeys provides client implementation for managing SSH keys in the Magalu Cloud platform.
// SSH keys are managed as a global service, meaning they are not bound to any specific region.
// By default, the service uses the global endpoint, but this can be overridden if needed.
package sshkeys

import (
"context"
"net/http"

"github.com/MagaluCloud/mgc-sdk-go/client"
mgc_http "github.com/MagaluCloud/mgc-sdk-go/internal/http"
)

const (
DefaultBasePath = "/profile"
)

type SSHKeyClient struct {
*client.CoreClient
}

// ClientOption allows customizing the SSH key client configuration.
type ClientOption func(*SSHKeyClient)

// WithGlobalBasePath allows overriding the default global endpoint for SSH keys service.
// This is rarely needed as SSH keys are managed globally, but provided for flexibility.
//
// Example:
//
// client := sshkeys.New(core, sshkeys.WithGlobalBasePath("custom-endpoint"))
func WithGlobalBasePath(basePath client.MgcUrl) ClientOption {
return func(c *SSHKeyClient) {
c.GetConfig().BaseURL = basePath
}
}

// New creates a new SSH key client using the provided core client.
// The SSH keys service operates globally and is not region-specific.
// By default, it uses the global endpoint (api.magalu.cloud).
//
// To customize the endpoint, use WithGlobalBasePath option.
func New(core *client.CoreClient, opts ...ClientOption) *SSHKeyClient {
if core == nil {
return nil
}
sshClient := &SSHKeyClient{
CoreClient: core,
}

core.GetConfig().BaseURL = client.Global

for _, opt := range opts {
opt(sshClient)
}
return sshClient
}

func (c *SSHKeyClient) newRequest(ctx context.Context, method, path string, body any) (*http.Request, error) {
return mgc_http.NewRequest(c.GetConfig(), ctx, method, DefaultBasePath+path, &body)
}

func (c *SSHKeyClient) Keys() KeyService {
return &keyService{client: c}
}
168 changes: 168 additions & 0 deletions sshkeys/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package sshkeys

import (
"context"
"net/http"
"testing"

"github.com/MagaluCloud/mgc-sdk-go/client"
)

func newTestCoreClient() *client.CoreClient {
httpClient := &http.Client{}
return client.NewMgcClient("test-api",
client.WithBaseURL(client.MgcUrl("http://test-api.com")),
client.WithHTTPClient(httpClient))
}

func TestNewSSHKeyClient(t *testing.T) {
core := newTestCoreClient()
sshClient := New(core)

if sshClient == nil {
t.Error("expected sshClient to not be nil")
return
}
if sshClient.CoreClient != core {
t.Errorf("expected CoreClient to be %v, got %v", core, sshClient.CoreClient)
}
}

func TestSSHKeyClient_newRequest(t *testing.T) {
tests := []struct {
name string
method string
path string
body interface{}
wantErr bool
}{
{
name: "valid GET request",
method: http.MethodGet,
path: "/v0/ssh-keys",
body: nil,
wantErr: false,
},
{
name: "valid POST request with body",
method: http.MethodPost,
path: "/v0/ssh-keys",
body: map[string]string{"name": "test-key"},
wantErr: false,
},
{
name: "invalid body",
method: http.MethodPost,
path: "/v0/ssh-keys",
body: make(chan int),
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
core := newTestCoreClient()
sshClient := New(core)

req, err := sshClient.newRequest(context.Background(), tt.method, tt.path, tt.body)

if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}

if err != nil {
t.Errorf("unexpected error: %v", err)
return
}

if req == nil {
t.Error("expected request to not be nil")
return
}

expectedPath := DefaultBasePath + tt.path
if req.URL.Path != expectedPath {
t.Errorf("expected path %s, got %s", expectedPath, req.URL.Path)
}

if req.Method != tt.method {
t.Errorf("expected method %s, got %s", tt.method, req.Method)
}
})
}
}

func TestNewSSHKeyClient_WithOptions(t *testing.T) {
core := newTestCoreClient()
customEndpoint := client.MgcUrl("http://custom-endpoint")

sshClient := New(core, WithGlobalBasePath(customEndpoint))

if sshClient == nil {
t.Fatal("expected sshClient to not be nil")
}

if sshClient.GetConfig().BaseURL != customEndpoint {
t.Errorf("expected BaseURL to be %s, got %s", customEndpoint, sshClient.GetConfig().BaseURL)
}
}

func TestSSHKeyClient_newRequest_Headers(t *testing.T) {
core := newTestCoreClient()
sshClient := New(core)

ctx := context.Background()
req, err := sshClient.newRequest(ctx, http.MethodGet, "/v0/ssh-keys", nil)

if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if contentType := req.Header.Get("Content-Type"); contentType != "application/json" {
t.Errorf("expected Content-Type header to be application/json, got %s", contentType)
}

if userAgent := req.Header.Get("User-Agent"); userAgent == "" {
t.Error("expected User-Agent header to be set")
}
}

func TestNewSSHKeyClient_WithNilCore(t *testing.T) {
sshClient := New(nil)
if sshClient != nil {
t.Error("expected nil client when core is nil")
}
}

func TestSSHKeyClient_Services(t *testing.T) {
core := newTestCoreClient()
sshClient := New(core)

t.Run("Keys", func(t *testing.T) {
svc := sshClient.Keys()
if svc == nil {
t.Error("expected KeyService to not be nil")
}
if _, ok := svc.(*keyService); !ok {
t.Error("expected KeyService to be of type *keyService")
}
})
}

func TestSSHKeyClient_DefaultBasePath(t *testing.T) {
core := newTestCoreClient()
sshClient := New(core)

req, err := sshClient.newRequest(context.Background(), http.MethodGet, "/v0/test", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

expectedPath := DefaultBasePath + "/v0/test"
if req.URL.Path != expectedPath {
t.Errorf("expected path %s, got %s", expectedPath, req.URL.Path)
}
}
Loading
Loading