Skip to content
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
28 changes: 28 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (

var BaseURL = "https://customer.cloudamqp.com/api"

var MetadataURL = "https://api.cloudamqp.com/api"

type Client struct {
apiKey string
baseURL string
Expand Down Expand Up @@ -107,6 +109,32 @@ func (c *Client) makeRequest(method, endpoint string, body any) ([]byte, error)
return respBody, nil
}

func (c *Client) makeExternalRequest(method, requestURL string) ([]byte, error) {
req, err := http.NewRequest(method, requestURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

req.Header.Set("User-Agent", fmt.Sprintf("cloudamqp-cli/%s", c.version))

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}

if resp.StatusCode >= 400 {
return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, string(respBody))
}

return respBody, nil
}

// Instance-specific operations using /instances/{id}/ endpoints

// Node management
Expand Down
5 changes: 5 additions & 0 deletions client/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type InstanceCreateRequest struct {
Name string `json:"name"`
Plan string `json:"plan"`
Region string `json:"region"`
RMQVersion string `json:"rmq_version,omitempty"`
Tags []string `json:"tags,omitempty"`
VPCSubnet string `json:"vpc_subnet,omitempty"`
VPCID *int `json:"vpc_id,omitempty"`
Expand Down Expand Up @@ -97,6 +98,10 @@ func (c *Client) CreateInstance(req *InstanceCreateRequest) (*InstanceCreateResp
}
}

if req.RMQVersion != "" {
formData.Set("rmq_version", req.RMQVersion)
}

if req.VPCSubnet != "" {
formData.Set("vpc_subnet", req.VPCSubnet)
}
Expand Down
25 changes: 25 additions & 0 deletions client/instances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,31 @@ func TestCreateInstance_WithTags(t *testing.T) {
assert.NoError(t, err)
}

func TestCreateInstance_WithRMQVersion(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
assert.NoError(t, err)

assert.Equal(t, "4.0.5", r.FormValue("rmq_version"))

response := InstanceCreateResponse{ID: 1234}
json.NewEncoder(w).Encode(response)
}))
defer server.Close()

client := NewWithBaseURL("test-api-key", server.URL, "test")

req := &InstanceCreateRequest{
Name: "test-instance",
Plan: "bunny-1",
Region: "amazon-web-services::us-east-1",
RMQVersion: "4.0.5",
}

_, err := client.CreateInstance(req)
assert.NoError(t, err)
}

func TestCreateInstance_WithVPC(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
Expand Down
14 changes: 14 additions & 0 deletions client/regions.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ func (c *Client) ListRegions(provider string) ([]Region, error) {
return regions, nil
}

func (c *Client) ListVersions() ([]string, error) {
respBody, err := c.makeExternalRequest("GET", MetadataURL+"/metadata/rabbitmq-versions")
if err != nil {
return nil, err
}

var versions []string
if err := json.Unmarshal(respBody, &versions); err != nil {
return nil, err
}

return versions, nil
}

func (c *Client) ListPlans(backend string) ([]Plan, error) {
endpoint := "/plans"
if backend != "" {
Expand Down
25 changes: 25 additions & 0 deletions client/regions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,28 @@ func TestListPlans_WithBackend(t *testing.T) {
assert.Len(t, plans, 1)
assert.Equal(t, "rabbitmq", plans[0].Backend)
}

func TestListVersions(t *testing.T) {
expectedVersions := []string{"3.13.7", "4.0.5", "4.1.0"}

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
assert.Equal(t, "/metadata/rabbitmq-versions", r.URL.Path)
assert.Empty(t, r.Header.Get("Authorization"), "metadata endpoint should be called without auth")

w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(expectedVersions)
}))
defer server.Close()

originalMetadataURL := MetadataURL
MetadataURL = server.URL
defer func() { MetadataURL = originalMetadataURL }()

client := NewWithBaseURL("test-api-key", server.URL, "test")

versions, err := client.ListVersions()

assert.NoError(t, err)
assert.Equal(t, expectedVersions, versions)
}
1 change: 1 addition & 0 deletions cmd/completion_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ func setCachedData(key string, ttl time.Duration, data interface{}) error {
const (
plansCacheTTL = 1 * time.Hour // Plans rarely change
regionsCacheTTL = 1 * time.Hour // Regions rarely change
versionsCacheTTL = 1 * time.Hour // Versions rarely change
instancesCacheTTL = 1 * time.Minute // Instances change frequently
vpcsCacheTTL = 1 * time.Minute // VPCs change frequently
)
57 changes: 57 additions & 0 deletions cmd/completion_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,63 @@ formatOutput:
return suggestions, cobra.ShellCompDirectiveNoFileComp
}

// completeVersions returns available RabbitMQ versions for completion.
// Only suggests versions when the selected plan is positively identified as a rabbitmq plan.
func completeVersions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
apiKey, err := completionAPIKey()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}

c := client.New(apiKey, Version)

planName, _ := cmd.Flags().GetString("plan")
if planName == "" {
return nil, cobra.ShellCompDirectiveNoFileComp
}

var plans []client.Plan
if cachedData, ok := getCachedData("plans", plansCacheTTL); ok {
_ = json.Unmarshal(cachedData, &plans)
}
if len(plans) == 0 {
plans, err = c.ListPlans("")
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
if len(plans) > 0 {
setCachedData("plans", plansCacheTTL, plans)
}
}

isRabbitMQPlan := false
for _, p := range plans {
if p.Name == planName {
isRabbitMQPlan = p.Backend == "rabbitmq"
break
}
}
if !isRabbitMQPlan {
return nil, cobra.ShellCompDirectiveNoFileComp
}

var versions []string
if cachedData, ok := getCachedData("versions", versionsCacheTTL); ok {
if err := json.Unmarshal(cachedData, &versions); err == nil {
return versions, cobra.ShellCompDirectiveNoFileComp
}
}

versions, err = c.ListVersions()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}

setCachedData("versions", versionsCacheTTL, versions)

return versions, cobra.ShellCompDirectiveNoFileComp
}

// completeCopySettings returns the valid copy-settings options
func completeCopySettings(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
settings := []string{
Expand Down
13 changes: 9 additions & 4 deletions cmd/instance_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var (
instanceName string
instancePlan string
instanceRegion string
instanceRMQVersion string
instanceTags []string
instanceVPCSubnet string
instanceVPCID string
Expand All @@ -34,6 +35,7 @@ Required flags:
--region: Region identifier (e.g., amazon-web-services::us-east-1)

Optional flags:
--rmq-version: RabbitMQ version (e.g., 4.0.5) - only for rabbitmq plans
--tags: Instance tags (can be specified multiple times)
--vpc-subnet: VPC subnet for dedicated VPC
--vpc-id: ID of existing VPC to add instance to
Expand All @@ -55,10 +57,11 @@ Optional flags:
c := client.New(apiKey, Version)

req := &client.InstanceCreateRequest{
Name: instanceName,
Plan: instancePlan,
Region: instanceRegion,
Tags: instanceTags,
Name: instanceName,
Plan: instancePlan,
Region: instanceRegion,
RMQVersion: instanceRMQVersion,
Tags: instanceTags,
}

if instanceVPCSubnet != "" {
Expand Down Expand Up @@ -118,6 +121,7 @@ func init() {
instanceCreateCmd.Flags().StringVar(&instanceName, "name", "", "Name of the instance (required)")
instanceCreateCmd.Flags().StringVar(&instancePlan, "plan", "", "Subscription plan (required)")
instanceCreateCmd.Flags().StringVar(&instanceRegion, "region", "", "Region identifier (required)")
instanceCreateCmd.Flags().StringVar(&instanceRMQVersion, "rmq-version", "", "RabbitMQ version (e.g., 4.0.5); only applies to rabbitmq plans, ignored otherwise")
instanceCreateCmd.Flags().StringSliceVar(&instanceTags, "tags", []string{}, "Instance tags")
instanceCreateCmd.Flags().StringVar(&instanceVPCSubnet, "vpc-subnet", "", "VPC subnet")
instanceCreateCmd.Flags().StringVar(&instanceVPCID, "vpc-id", "", "VPC ID")
Expand All @@ -130,6 +134,7 @@ func init() {
instanceCreateCmd.MarkFlagRequired("plan")
instanceCreateCmd.MarkFlagRequired("region")

instanceCreateCmd.RegisterFlagCompletionFunc("rmq-version", completeVersions)
instanceCreateCmd.RegisterFlagCompletionFunc("plan", completePlans)
instanceCreateCmd.RegisterFlagCompletionFunc("region", completeRegions)
instanceCreateCmd.RegisterFlagCompletionFunc("vpc-id", completeVPCIDFlag)
Expand Down
Loading