diff --git a/api/account.go b/api/account.go new file mode 100644 index 00000000..414324d9 --- /dev/null +++ b/api/account.go @@ -0,0 +1,95 @@ +package api + +import ( + "fmt" + "log" + "strconv" +) + +func (api *API) ListInstances() ([]map[string]interface{}, error) { + var ( + data []map[string]interface{} + failed map[string]interface{} + path = "api/instances" + ) + + response, err := api.sling.New().Path(path).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::account::list_instances data: %v", data) + if err != nil { + return nil, err + } + if response.StatusCode != 200 { + return nil, fmt.Errorf("ListInstances failed, status: %v, message: %s", response.StatusCode, failed) + } + return data, nil +} + +func (api *API) ListVpcs() ([]map[string]interface{}, error) { + var ( + data []map[string]interface{} + failed map[string]interface{} + path = "/api/vpcs" + ) + + response, err := api.sling.New().Path(path).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::vpc::list data: %v", data) + + if err != nil { + return nil, err + } + if response.StatusCode != 200 { + return nil, fmt.Errorf("ListVpcs failed, status: %v, message: %v", response.StatusCode, failed) + } + + for k := range data { + vpcID := strconv.FormatFloat(data[k]["id"].(float64), 'f', 0, 64) + data_temp, _ := api.readVpcName(vpcID) + data[k]["vpc_name"] = data_temp["name"] + } + + return data, nil +} + +func (api *API) RotatePassword(instanceID int) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("api/instances/%d/account/rotate-password", instanceID) + ) + + response, err := api.sling.New().Post(path).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 200: + return nil + case 204: + return nil + default: + return fmt.Errorf("failed to rotate api key, statusCode: %v, failed: %v", + response.StatusCode, failed) + } +} + +func (api *API) RotateApiKey(instanceID int) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("api/instances/%d/account/rotate-apikey", instanceID) + ) + + response, err := api.sling.New().Post(path).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 200: + return nil + case 204: + return nil + default: + return fmt.Errorf("failed to rotate api key, statusCode: %v, failed: %v", + response.StatusCode, failed) + } +} diff --git a/api/alarms.go b/api/alarms.go new file mode 100644 index 00000000..81960e3a --- /dev/null +++ b/api/alarms.go @@ -0,0 +1,122 @@ +package api + +import ( + "errors" + "fmt" + "log" + "strconv" + "time" +) + +func (api *API) CreateAlarm(instanceID int, params map[string]interface{}) (map[string]interface{}, error) { + data := make(map[string]interface{}) + failed := make(map[string]interface{}) + log.Printf("[DEBUG] go-api::alarm::create instance ID: %v, params: %v", instanceID, params) + path := fmt.Sprintf("/api/instances/%d/alarms", instanceID) + response, err := api.sling.New().Post(path).BodyJSON(params).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::alarm::create data: %v", data) + + if err != nil { + return nil, err + } + if response.StatusCode != 201 { + return nil, fmt.Errorf("CreateAlarm failed, status: %v, message: %s", response.StatusCode, failed) + } + + if id, ok := data["id"]; ok { + data["id"] = strconv.FormatFloat(id.(float64), 'f', 0, 64) + log.Printf("[DEBUG] go-api::alarm::create id set: %v", data["id"]) + } else { + msg := fmt.Sprintf("go-api::instance::create Invalid alarm identifier: %v", data["id"]) + log.Printf("[ERROR] %s", msg) + return nil, errors.New(msg) + } + + return data, err +} + +func (api *API) ReadAlarm(instanceID int, alarmID string) (map[string]interface{}, error) { + data := make(map[string]interface{}) + failed := make(map[string]interface{}) + log.Printf("[DEBUG] go-api::alarm::read instance ID: %v, alarm ID: %v", instanceID, alarmID) + path := fmt.Sprintf("/api/instances/%v/alarms/%v", instanceID, alarmID) + response, err := api.sling.New().Get(path).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::alarm::read data : %v", data) + + if err != nil { + return nil, err + } + if response.StatusCode != 200 { + return nil, fmt.Errorf("ReadAlarm failed, status: %v, message: %s", response.StatusCode, failed) + } + + return data, err +} + +func (api *API) ReadAlarms(instanceID int) ([]map[string]interface{}, error) { + var data []map[string]interface{} + failed := make(map[string]interface{}) + log.Printf("[DEBUG] go-api::alarm::read instance ID: %v", instanceID) + path := fmt.Sprintf("/api/instances/%d/alarms", instanceID) + response, err := api.sling.New().Get(path).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::alarm::read data: %v", data) + + if err != nil { + return nil, err + } + if response.StatusCode != 200 { + return nil, fmt.Errorf("Alarms::ReadAlarms failed, status: %v, message: %s", response.StatusCode, failed) + } + + return data, err +} + +func (api *API) UpdateAlarm(instanceID int, params map[string]interface{}) error { + failed := make(map[string]interface{}) + log.Printf("[DEBUG] go-api::alarm::update instance ID: %v, params: %v", instanceID, params) + path := fmt.Sprintf("/api/instances/%v/alarms/%v", instanceID, params["id"]) + response, err := api.sling.New().Put(path).BodyJSON(params).Receive(nil, &failed) + + if err != nil { + return err + } + if response.StatusCode != 201 { + return fmt.Errorf("Alarms::UpdateAlarm failed, status: %v, message: %s", response.StatusCode, failed) + } + + return err +} + +func (api *API) DeleteAlarm(instanceID int, params map[string]interface{}) error { + failed := make(map[string]interface{}) + log.Printf("[DEBUG] go-api::alarm::delete instance id: %v, params: %v", instanceID, params) + path := fmt.Sprintf("/api/instances/%v/alarms/%v", instanceID, params["id"]) + response, _ := api.sling.New().Delete(path).BodyJSON(params).Receive(nil, &failed) + + if response.StatusCode != 204 { + return fmt.Errorf("Alarm::DeleteAlarm failed, status: %v, message: %s", response.StatusCode, failed) + } + + return api.waitUntilAlarmDeletion(instanceID, params["id"].(string)) +} + +func (api *API) waitUntilAlarmDeletion(instanceID int, id string) error { + log.Printf("[DEBUG] go-api::alarm::waitUntilAlarmDeletion waiting") + data := make(map[string]interface{}) + failed := make(map[string]interface{}) + for { + path := fmt.Sprintf("/api/instances/%v/alarms/%v", instanceID, id) + response, err := api.sling.New().Path(path).Receive(&data, &failed) + + if err != nil { + log.Printf("[DEBUG] go-api::alarm::waitUntilAlarmDeletion error: %v", err) + return err + } + if response.StatusCode == 404 { + log.Print("[DEBUG] go-api::alarm::waitUntilAlarmDeletion deleted") + return nil + } + + time.Sleep(10 * time.Second) + } +} diff --git a/api/api.go b/api/api.go new file mode 100644 index 00000000..b601d477 --- /dev/null +++ b/api/api.go @@ -0,0 +1,36 @@ +package api + +import ( + "net/http" + + "github.com/dghubble/sling" +) + +type API struct { + sling *sling.Sling + client *http.Client +} + +func (api *API) DefaultRmqVersion() (map[string]interface{}, error) { + data := make(map[string]interface{}) + failed := make(map[string]interface{}) + _, err := api.sling.New().Get("/api/default_rabbitmq_version").Receive(&data, &failed) + if err != nil { + return nil, err + } + return data, nil +} + +func New(baseUrl, apiKey string, useragent string, client *http.Client) *API { + if len(useragent) == 0 { + useragent = "84codes go-api" + } + return &API{ + sling: sling.New(). + Client(client). + Base(baseUrl). + SetBasicAuth("", apiKey). + Set("User-Agent", useragent), + client: client, + } +} diff --git a/api/aws_eventbridge.go b/api/aws_eventbridge.go new file mode 100644 index 00000000..8746b92c --- /dev/null +++ b/api/aws_eventbridge.go @@ -0,0 +1,94 @@ +package api + +import ( + "fmt" + "log" + "strconv" +) + +func (api *API) CreateAwsEventBridge(instanceID int, params map[string]interface{}) (map[string]interface{}, error) { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/eventbridges", instanceID) + ) + + log.Printf("[DEBUG] go-api::aws-eventbridge::create instance ID: %d, params: %v", instanceID, params) + response, err := api.sling.New().Post(path).BodyJSON(params).Receive(&data, &failed) + if err != nil { + return nil, err + } + if response.StatusCode != 201 { + return nil, fmt.Errorf("failed to create AWS EventBridge, status: %v, message: %s", + response.StatusCode, failed) + } + if id, ok := data["id"]; ok { + data["id"] = strconv.FormatFloat(id.(float64), 'f', 0, 64) + log.Printf("[DEBUG] go-api::aws-eventbridge::create EventBridge identifier: %v", data["id"]) + } else { + return nil, fmt.Errorf("go-api::aws-eventbridge::create Invalid identifier: %v", data["id"]) + } + + return data, nil +} + +func (api *API) ReadAwsEventBridge(instanceID int, eventbridgeID string) (map[string]interface{}, error) { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/eventbridges/%s", instanceID, eventbridgeID) + ) + + response, err := api.sling.New().Get(path).Receive(&data, &failed) + if err != nil { + return nil, err + } + if response.StatusCode != 200 { + return nil, fmt.Errorf("failed to read AWS EventBridge, status: %v, message: %s", + response.StatusCode, failed) + } + + return data, nil +} + +func (api *API) ReadAwsEventBridges(instanceID int) (map[string]interface{}, error) { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/eventbridges", instanceID) + ) + + response, err := api.sling.New().Get(path).Receive(&data, &failed) + if err != nil { + return nil, err + } + if response.StatusCode != 200 { + return nil, fmt.Errorf("failed to read AWS EventBridges, status: %v, message: %s", + response.StatusCode, failed) + } + + return data, nil +} + +func (api *API) DeleteAwsEventBridge(instanceID int, eventbridgeID string) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/eventbridges/%s", instanceID, eventbridgeID) + ) + + log.Printf("[DEBUG] go-api::aws-eventbridge::delete instance id: %d, eventbridge id: %s", instanceID, eventbridgeID) + response, err := api.sling.New().Delete(path).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 204: + return nil + case 404: + // AWS EventBridge not found in the backend. Silent let the resource be deleted. + return nil + } + + return fmt.Errorf("failed to delete AWS EventBridge, status: %v, message: %s", response.StatusCode, failed) +} diff --git a/api/credentials.go b/api/credentials.go new file mode 100644 index 00000000..7a4ae343 --- /dev/null +++ b/api/credentials.go @@ -0,0 +1,41 @@ +package api + +import ( + "fmt" + "log" + "regexp" + "strconv" +) + +func (api *API) ReadCredentials(id int) (map[string]interface{}, error) { + data := make(map[string]interface{}) + failed := make(map[string]interface{}) + instanceID := strconv.Itoa(id) + log.Printf("[DEBUG] go-api::credentials::read instance ID: %v", instanceID) + response, err := api.sling.New().Path("/api/instances/").Get(instanceID).Receive(&data, &failed) + if err != nil { + return nil, err + } + if response.StatusCode != 200 { + return nil, fmt.Errorf("ReadCredentials failed, status: %v, message: %s", response.StatusCode, failed) + } + + return extractInfo(data["url"].(string)), nil +} + +func extractInfo(url string) map[string]interface{} { + paramsMap := make(map[string]interface{}) + r := regexp.MustCompile(`^.*:\/\/(?P(.*)):(?P(.*))@`) + match := r.FindStringSubmatch(url) + + for i, name := range r.SubexpNames() { + if name == "username" { + paramsMap["username"] = match[i] + } + if name == "password" { + paramsMap["password"] = match[i] + } + } + + return paramsMap +} diff --git a/api/custom_domain.go b/api/custom_domain.go new file mode 100644 index 00000000..c06e298a --- /dev/null +++ b/api/custom_domain.go @@ -0,0 +1,107 @@ +package api + +import ( + "fmt" + "log" + "time" +) + +func (api *API) waitUntilCustomDomainConfigured(instanceID int, configured bool) (map[string]interface{}, error) { + for { + response, err := api.ReadCustomDomain(instanceID) + + if err != nil { + return nil, err + } + + if response["configured"] == configured { + return response, nil + } + + log.Printf("[DEBUG] go-api::custom_domain#waitUntilCustomDomainConfigured: still waiting, response: %s", response) + time.Sleep(1 * time.Second) + } +} + +func (api *API) CreateCustomDomain(instanceID int, hostname string) (map[string]interface{}, error) { + log.Printf("[DEBUG] go-api::custom_domain::create custom domain ID: %v, hostname: %v", instanceID, hostname) + + failed := make(map[string]interface{}) + params := make(map[string]string) + params["hostname"] = hostname + path := fmt.Sprintf("/api/instances/%d/custom-domain", instanceID) + response, err := api.sling.New().Post(path).BodyJSON(params).Receive(nil, &failed) + + if err != nil { + return nil, err + } + + if response.StatusCode != 202 { + return nil, fmt.Errorf("CreateCustomDomain failed, status: %v, message: %s", response.StatusCode, failed) + } + + return api.waitUntilCustomDomainConfigured(instanceID, true) +} + +func (api *API) ReadCustomDomain(instanceID int) (map[string]interface{}, error) { + log.Printf("[DEBUG] go-api::custom_domain#read instanceID: %v", instanceID) + + failed := make(map[string]interface{}) + data := make(map[string]interface{}) + path := fmt.Sprintf("/api/instances/%d/custom-domain", instanceID) + response, err := api.sling.New().Path(path).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::custom_domain::read data: %v", data) + + if err != nil { + return nil, err + } + + if response.StatusCode == 200 { + return data, nil + } else { + return nil, fmt.Errorf("ReadCustomDomain failed, status: %v, message: %s", response.StatusCode, failed) + } +} + +func (api *API) UpdateCustomDomain(instanceID int, hostname string) (map[string]interface{}, error) { + log.Printf("[DEBUG] go-api::custom_domain#update instanceID: %v", instanceID) + + // delete and wait + _, err := api.DeleteCustomDomain(instanceID) + if err != nil { + return nil, err + } + _, err = api.waitUntilCustomDomainConfigured(instanceID, false) + if err != nil { + return nil, err + } + + // create and wait + _, err = api.CreateCustomDomain(instanceID, hostname) + if err != nil { + return nil, err + } + return api.waitUntilCustomDomainConfigured(instanceID, true) +} + +func (api *API) DeleteCustomDomain(instanceID int) (map[string]interface{}, error) { + log.Printf("[DEBUG] go-api::custom_domain#delete instanceID: %v", instanceID) + + failed := make(map[string]interface{}) + path := fmt.Sprintf("/api/instances/%d/custom-domain", instanceID) + response, err := api.sling.New().Delete(path).Receive(nil, &failed) + + if err != nil { + return nil, err + } + + if response.StatusCode == 200 { + // no custom domain configured + return nil, nil + } else if response.StatusCode == 202 { + // wait until the remove completed successfully + return api.waitUntilCustomDomainConfigured(instanceID, false) + } else { + return nil, fmt.Errorf("DeleteCustomDomain failed, status: %v, message: %s", response.StatusCode, failed) + } +} diff --git a/api/disk.go b/api/disk.go new file mode 100644 index 00000000..72eec378 --- /dev/null +++ b/api/disk.go @@ -0,0 +1,57 @@ +package api + +import ( + "fmt" + "log" + "strconv" + "time" +) + +func (api *API) ResizeDisk(instanceID int, params map[string]interface{}, sleep, timeout int) (map[string]interface{}, error) { + var ( + id = strconv.Itoa(instanceID) + path = fmt.Sprintf("api/instances/%s/disk", id) + ) + log.Printf("[DEBUG] go-api::resizeDisk::resizeDiskWithRetry path: %s, "+ + "attempt: %d, sleep: %d, timeout: %d", path, 1, sleep, timeout) + return api.resizeDiskWithRetry(id, params, 1, sleep, timeout) +} + +func (api *API) resizeDiskWithRetry(id string, params map[string]interface{}, attempt, sleep, timeout int) (map[string]interface{}, error) { + var ( + data = make(map[string]interface{}) + failed = make(map[string]interface{}) + path = fmt.Sprintf("api/instances/%s/disk", id) + ) + + response, err := api.sling.New().Put(path).BodyJSON(params).Receive(&data, &failed) + if err != nil { + return nil, err + } else if attempt*sleep > timeout { + return nil, fmt.Errorf("resize disk timeout reached after %d seconds", timeout) + } + + switch response.StatusCode { + case 200: + if err = api.waitWithTimeoutUntilAllNodesConfigured(id, attempt, sleep, timeout); err != nil { + return nil, err + } + return data, nil + case 400: + log.Printf("[DEBUG] go-api::resizeDisk::resizeDiskWithRetry failed: %v", failed) + switch { + case failed["error_code"] == nil: + break + case failed["error_code"].(float64) == 40099: + log.Printf("[DEBUG] go-api::resizeDisk::resizeDiskWithRetry %s, will try again, attempt: %d, until "+ + "timeout: %d", failed["error"].(string), attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.resizeDiskWithRetry(id, params, attempt, sleep, timeout) + default: + return nil, fmt.Errorf("resize disk failed: %s", failed["error"].(string)) + } + } + return nil, fmt.Errorf("resize disk failed, status: %v, message: %s", + response.StatusCode, failed["error"].(string)) +} diff --git a/api/instance.go b/api/instance.go new file mode 100644 index 00000000..3e433a19 --- /dev/null +++ b/api/instance.go @@ -0,0 +1,275 @@ +package api + +import ( + "fmt" + "log" + "regexp" + "strconv" + "time" +) + +func (api *API) waitUntilReady(instanceID string) (map[string]interface{}, error) { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%s", instanceID) + ) + + log.Printf("[DEBUG] go-api::instance::waitUntilReady waiting") + + for { + response, err := api.sling.New().Path(path).Receive(&data, &failed) + if err != nil { + return nil, err + } + + switch response.StatusCode { + case 200: + if data["ready"] == true { + data["id"] = instanceID + return data, nil + } + default: + return nil, fmt.Errorf("waitUntilReady failed, status: %v, message: %s", + response.StatusCode, failed) + } + time.Sleep(10 * time.Second) + } +} + +func (api *API) waitUntilAllNodesReady(instanceID string) error { + var ( + data []map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("api/instances/%v/nodes", instanceID) + ) + + for { + _, err := api.sling.New().Path(path).Receive(&data, &failed) + if err != nil { + return err + } + + log.Printf("[DEBUG] go-api::instance::waitUntilAllNodesReady numberOfNodes: %v", len(data)) + ready := true + for _, node := range data { + log.Printf("[DEBUG] go-api::instance::waitUntilAllNodesReady ready: %v, configured: %v", + ready, node["configured"]) + ready = ready && node["configured"].(bool) + } + if ready { + return nil + } + time.Sleep(15 * time.Second) + } +} + +func (api *API) waitWithTimeoutUntilAllNodesConfigured(instanceID string, attempt, sleep, + timeout int) error { + + var ( + data []map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("api/instances/%v/nodes", instanceID) + ) + log.Printf("[DEBUG] go-api::instance::waitWithTimeoutUntilAllNodesConfigured not yet ready, "+ + " will try again, attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + + _, err := api.sling.New().Path(path).Receive(&data, &failed) + if err != nil { + return err + } else if attempt*sleep > timeout { + return fmt.Errorf("all nodes configured timeout reached after %d seconds", timeout) + } + + ready := true + for _, node := range data { + log.Printf("[DEBUG] go-api::instance::waitWithTimeoutUntilAllNodesConfigured ready: %v, configured: %v", + ready, node["configured"]) + ready = ready && node["configured"].(bool) + } + log.Printf("[DEBUG] go-api::instance::waitWithTimeoutUntilAllNodesConfigured ready: %v", ready) + if ready { + return nil + } + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.waitWithTimeoutUntilAllNodesConfigured(instanceID, attempt, sleep, timeout) +} + +func (api *API) waitUntilDeletion(instanceID string) error { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%s", instanceID) + ) + + log.Printf("[DEBUG] go-api::instance::waitUntilDeletion waiting") + for { + response, err := api.sling.New().Path(path).Receive(&data, &failed) + if err != nil { + log.Printf("[DEBUG] go-api::instance::waitUntilDeletion error: %v", err) + return err + } + + switch response.StatusCode { + case 404: + log.Print("[DEBUG] go-api::instance::waitUntilDeletion deleted") + return nil + case 410: + log.Print("[DEBUG] go-api::instance::waitUntilDeletion deleted") + return nil + } + time.Sleep(10 * time.Second) + } +} + +func (api *API) CreateInstance(params map[string]interface{}) (map[string]interface{}, error) { + var ( + data map[string]interface{} + failed map[string]interface{} + ) + + log.Printf("[DEBUG] go-api::instance::create params: %v", params) + response, err := api.sling.New().Post("/api/instances").BodyJSON(params).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::instance::waitUntilReady data: %v", data) + if err != nil { + return nil, err + } + + switch response.StatusCode { + case 200: + if id, ok := data["id"]; ok { + data["id"] = strconv.FormatFloat(id.(float64), 'f', 0, 64) + log.Printf("[DEBUG] go-api::instance::create id set: %v", data["id"]) + } else { + return nil, fmt.Errorf("go-api::instance::create Invalid instance identifier: %v", data["id"]) + } + return api.waitUntilReady(data["id"].(string)) + default: + return nil, fmt.Errorf("create instance failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +func (api *API) ReadInstance(instanceID string) (map[string]interface{}, error) { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%s", instanceID) + ) + + log.Printf("[DEBUG] go-api::instance::read instance ID: %v", instanceID) + response, err := api.sling.New().Path(path).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::instance::read data: %v", data) + if err != nil { + return nil, err + } + + switch response.StatusCode { + case 200: + return data, nil + case 410: + log.Printf("[WARN] go-api::instance::read status: 410, message: The instance has been deleted") + return nil, nil + default: + return nil, fmt.Errorf("read instance failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// TODO: Rename to ListInstances +func (api *API) ReadInstances() ([]map[string]interface{}, error) { + var ( + data []map[string]interface{} + failed map[string]interface{} + ) + + response, err := api.sling.New().Get("/api/instances").Receive(&data, &failed) + log.Printf("[DEBUG] go-api::instance::list data: %v", data) + if err != nil { + return nil, err + } + + switch response.StatusCode { + case 200: + return data, nil + case 410: + log.Printf("[WARN] go-api::instance::list status: 410, message: The instance has been deleted") + return nil, nil + default: + return nil, fmt.Errorf("list instances failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +func (api *API) UpdateInstance(instanceID string, params map[string]interface{}) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("api/instances/%v", instanceID) + ) + + log.Printf("[DEBUG] go-api::instance::update instance ID: %v, params: %v", instanceID, params) + response, err := api.sling.New().Put(path).BodyJSON(params).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 200: + return api.waitUntilAllNodesReady(instanceID) + case 410: + log.Printf("[WARN] go-api::instance::update status: 410, message: The instance has been deleted") + return nil + default: + return fmt.Errorf("update instance failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +func (api *API) DeleteInstance(instanceID string, keep_vpc bool) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("api/instances/%s?keep_vpc=%v", instanceID, keep_vpc) + ) + + log.Printf("[DEBUG] go-api::instance::delete instance ID: %v", instanceID) + response, err := api.sling.New().Delete(path).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 204: + return api.waitUntilDeletion(instanceID) + case 410: + log.Printf("[WARN] go-api::instance::delete status: 410, message: The instance has been deleted") + return nil + default: + return fmt.Errorf("delete instance failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +func (api *API) UrlInformation(url string) map[string]interface{} { + paramsMap := make(map[string]interface{}) + r := regexp.MustCompile(`^.*:\/\/(?P(.*)):(?P(.*))@(?P(.*))\/(?P(.*))`) + match := r.FindStringSubmatch(url) + + for i, value := range r.SubexpNames() { + if value == "username" { + paramsMap["username"] = match[i] + } + if value == "password" { + paramsMap["password"] = match[i] + } + if value == "host" { + paramsMap["host"] = match[i] + } + if value == "vhost" { + paramsMap["vhost"] = match[i] + } + } + + return paramsMap +} diff --git a/api/integration.go b/api/integration.go new file mode 100644 index 00000000..76d960de --- /dev/null +++ b/api/integration.go @@ -0,0 +1,95 @@ +package api + +import ( + "fmt" + "log" + "strconv" +) + +// CreateIntegration enables integration communication, either for logs or metrics. +func (api *API) CreateIntegration(instanceID int, intType string, intName string, params map[string]interface{}) (map[string]interface{}, error) { + data := make(map[string]interface{}) + failed := make(map[string]interface{}) + log.Printf("[DEBUG] go-api::integration::create params: %v", params) + path := fmt.Sprintf("/api/instances/%d/integrations/%s/%s", instanceID, intType, intName) + response, err := api.sling.New().Post(path).BodyJSON(params).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::integration::create response data: %v", data) + + if err != nil { + return nil, err + } + if response.StatusCode != 201 { + return nil, fmt.Errorf(fmt.Sprintf("CreateIntegration failed, status: %v, message: %s", response.StatusCode, failed)) + } + + if v, ok := data["id"]; ok { + data["id"] = strconv.FormatFloat(v.(float64), 'f', 0, 64) + } else { + msg := fmt.Sprintf("go-api::integration::create Invalid integration identifier: %v", data["id"]) + log.Printf("[ERROR] %s", msg) + return nil, fmt.Errorf(msg) + } + + return data, err +} + +// ReadIntegration retrieves a specific logs or metrics integration +func (api *API) ReadIntegration(instanceID int, intType, intID string) (map[string]interface{}, error) { + data := make(map[string]interface{}) + failed := make(map[string]interface{}) + log.Printf("[DEBUG] go-api::integration::read instance ID: %d, intType: %s, intID: %s", instanceID, intType, intID) + path := fmt.Sprintf("/api/instances/%d/integrations/%s/%s", instanceID, intType, intID) + response, err := api.sling.New().Path(path).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::integration::read data: %v", data) + + if err != nil { + return nil, err + } + if response.StatusCode != 200 { + return nil, fmt.Errorf("ReadIntegration failed, status: %v, message: %s", response.StatusCode, failed) + } + + // Convert API response body, config part, into single map + convertedData := make(map[string]interface{}) + for k, v := range data { + if k == "id" { + convertedData[k] = v + } else if k == "type" { + convertedData[k] = v + } else if k == "config" { + for configK, configV := range data["config"].(map[string]interface{}) { + convertedData[configK] = configV + } + } + } + log.Printf("[DEBUG] go-api::integration::read convertedDatat: %v", convertedData) + return convertedData, err +} + +// UpdateIntegration updated the integration with new information +func (api *API) UpdateIntegration(instanceID int, intType, intID string, params map[string]interface{}) error { + failed := make(map[string]interface{}) + log.Printf("[DEBUG] go-api::integration::update instance ID: %d, intType: %s, intID: %s", instanceID, intType, intID) + path := fmt.Sprintf("/api/instances/%d/integrations/%s/%s", instanceID, intType, intID) + response, err := api.sling.New().Put(path).BodyJSON(params).Receive(nil, &failed) + + if response.StatusCode != 204 { + return fmt.Errorf("UpdateIntegration failed, status: %v, message: %s", response.StatusCode, failed) + } + + return err +} + +// DeleteIntegration removes log or metric integration. +func (api *API) DeleteIntegration(instanceID int, intType, intID string) error { + failed := make(map[string]interface{}) + log.Printf("[DEBUG] go-api::integration::delete instance ID: %d, intType: %s, intID: %s", instanceID, intType, intID) + path := fmt.Sprintf("/api/instances/%d/integrations/%s/%s", instanceID, intType, intID) + response, err := api.sling.New().Delete(path).Receive(nil, &failed) + + if response.StatusCode != 204 { + return fmt.Errorf("DeleteNotification failed, status: %v, message: %s", response.StatusCode, failed) + } + + return err +} diff --git a/api/metadata.go b/api/metadata.go new file mode 100644 index 00000000..5ae4a64c --- /dev/null +++ b/api/metadata.go @@ -0,0 +1,109 @@ +package api + +import ( + "fmt" +) + +type Plan struct { + Name string `json:"name"` + Backend string `json:"backend"` + Shared bool `json:"shared"` +} + +type Region struct { + Provider string `json:"provider"` + Region string `json:"region"` +} + +// ValidatePlan: Check with backend if plan is valid +func (api *API) ValidatePlan(name string) error { + var ( + data []Plan + failed map[string]interface{} + path = "api/plans" + ) + + response, err := api.sling.New().Get(path).Receive(&data, &failed) + if err != nil { + return err + } + + if response.StatusCode != 200 { + return fmt.Errorf("validate subscription plan. Status code: %d, message: %v", + response.StatusCode, failed) + } + + for _, plan := range data { + if name == plan.Name { + return nil + } + } + return fmt.Errorf("subscription plan: %s is not valid", name) +} + +// PlanTypes: Fetch if old/new plans are shared/dedicated +func (api *API) PlanTypes(old, new string) (string, string, error) { + var ( + data []Plan + failed map[string]interface{} + path = "api/plans" + oldPlanType string + newPlanType string + ) + + response, err := api.sling.New().Get(path).Receive(&data, &failed) + if err != nil { + return "", "", err + } + + if response.StatusCode != 200 { + return "", "", fmt.Errorf("Plan types. "+ + "Status code: %d, message: %v", response.StatusCode, failed) + } + + for _, plan := range data { + if old == plan.Name { + oldPlanType = planType(plan.Shared) + } else if new == plan.Name { + newPlanType = planType(plan.Shared) + } + } + return oldPlanType, newPlanType, nil +} + +func planType(shared bool) string { + if shared { + return "shared" + } else { + return "dedicated" + } +} + +// ValidateRegion: Check with backend if region is valid +func (api *API) ValidateRegion(region string) error { + var ( + data []Region + failed map[string]interface{} + path = "api/regions" + platform string + ) + + response, err := api.sling.New().Get(path).Receive(&data, &failed) + if err != nil { + return err + } + + if response.StatusCode != 200 { + return fmt.Errorf("validate region. Status code: %d, message: %v", + response.StatusCode, failed) + } + + for _, v := range data { + platform = fmt.Sprintf("%s::%s", v.Provider, v.Region) + if region == platform { + return nil + } + } + + return fmt.Errorf("provider & region: %s is not valid", region) +} diff --git a/api/nodes.go b/api/nodes.go new file mode 100644 index 00000000..5ee587a4 --- /dev/null +++ b/api/nodes.go @@ -0,0 +1,88 @@ +package api + +import ( + "fmt" + "log" + "time" +) + +// ReadNodes - read out node information of the cluster +func (api *API) ReadNodes(instanceID int) ([]map[string]interface{}, error) { + var data []map[string]interface{} + failed := make(map[string]interface{}) + log.Printf("[DEBUG] go-api::nodes::read_nodes instance id: %d", instanceID) + path := fmt.Sprintf("api/instances/%d/nodes", instanceID) + response, err := api.sling.New().Path(path).Receive(&data, &failed) + if err != nil { + return nil, err + } + if response.StatusCode != 200 { + return nil, fmt.Errorf("ReadNodes failed, status: %v, message: %s", response.StatusCode, failed) + } + return data, nil +} + +// ReadNode - read out node information of a single node +func (api *API) ReadNode(instanceID int, nodeName string) (map[string]interface{}, error) { + data := make(map[string]interface{}) + log.Printf("[DEBUG] go-api::nodes::read_node instance id: %d node name: %s", instanceID, nodeName) + response, err := api.ReadNodes(instanceID) + if err != nil { + return nil, err + } + for i := range response { + if response[i]["name"] == nodeName { + data = response[i] + break + } + } + return data, nil +} + +// PostAction - request an action for the node (e.g. start/stop/restart RabbitMQ) +func (api *API) PostAction(instanceID int, nodeName string, action string) (map[string]interface{}, error) { + var actionAsRoute string + params := make(map[string][]string) + data := make(map[string]interface{}) + failed := make(map[string]interface{}) + if action == "mgmt.restart" { + actionAsRoute = "mgmt-restart" + } else { + actionAsRoute = action + } + params["nodes"] = append(params["nodes"], nodeName) + path := fmt.Sprintf("api/instances/%d/actions/%s", instanceID, actionAsRoute) + response, err := api.sling.New().Post(path).BodyJSON(params).Receive(&data, &failed) + + if err != nil { + return nil, err + } + if response.StatusCode != 200 { + return nil, fmt.Errorf("action failed, status: %v, message: %s", response.StatusCode, failed) + } + + return api.waitOnNodeAction(instanceID, nodeName, action) +} + +func (api *API) waitOnNodeAction(instanceID int, nodeName string, action string) (map[string]interface{}, error) { + log.Printf("[DEBUG] go-api::nodes::waitOnNodeAction waiting") + for { + data, err := api.ReadNode(instanceID, nodeName) + + if err != nil { + return nil, err + } + + switch action { + case "start", "restart", "reboot", "mgmt.restart": + if data["running"] == true { + return data, nil + } + case "stop": + if data["running"] == false { + return data, nil + } + } + time.Sleep(20 * time.Second) + } +} diff --git a/api/notifications.go b/api/notifications.go new file mode 100644 index 00000000..12ab273e --- /dev/null +++ b/api/notifications.go @@ -0,0 +1,113 @@ +package api + +import ( + "fmt" + "log" + "strconv" +) + +func (api *API) CreateNotification(instanceID int, params map[string]interface{}) (map[string]interface{}, error) { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/alarms/recipients", instanceID) + ) + + log.Printf("[DEBUG] go-api::notification::create path: %s", path) + response, err := api.sling.New().Post(path).BodyJSON(params).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::notification::create data: %v", data) + + if err != nil { + return nil, err + } + if response.StatusCode != 201 { + return nil, fmt.Errorf("create notification failed, status: %d, message: %s", + response.StatusCode, failed) + } + + if v, ok := data["id"]; ok { + data["id"] = strconv.FormatFloat(v.(float64), 'f', 0, 64) + } else { + return nil, fmt.Errorf("create notification invalid identifier: %v", data["id"]) + } + + return data, err +} + +func (api *API) ReadNotification(instanceID int, recipientID string) (map[string]interface{}, error) { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/alarms/recipients/%s", instanceID, recipientID) + ) + + log.Printf("[DEBUG] go-api::notification::read path: %s", path) + response, err := api.sling.New().Path(path).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::notification::read data: %v", data) + + if err != nil { + return nil, err + } + if response.StatusCode != 200 { + return nil, fmt.Errorf("read notification failed, status: %v, message: %s", + response.StatusCode, failed) + } + + return data, err +} + +func (api *API) ReadNotifications(instanceID int) ([]map[string]interface{}, error) { + var ( + data []map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/alarms/recipients", instanceID) + ) + + log.Printf("[DEBUG] go-api::ReadNotifications::read path: %s", path) + response, err := api.sling.New().Path(path).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::ReadNotifications::read data: %v", data) + + if err != nil { + return nil, err + } + if response.StatusCode != 200 { + return nil, fmt.Errorf("read notification failed, status: %d, message: %s", + response.StatusCode, failed) + } + + return data, err +} + +func (api *API) UpdateNotification(instanceID int, recipientID string, params map[string]interface{}) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/alarms/recipients/%s", instanceID, recipientID) + ) + + log.Printf("[DEBUG] go-api::notification::update path: %s", path) + response, err := api.sling.New().Put(path).BodyJSON(params).Receive(nil, &failed) + + if response.StatusCode != 200 { + return fmt.Errorf("update notification failed, status: %d, message: %s", + response.StatusCode, failed) + } + + return err +} + +func (api *API) DeleteNotification(instanceID int, recipientID string) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/alarms/recipients/%s", instanceID, recipientID) + ) + + log.Printf("[DEBUG] go-api::notification::delete path: %s", path) + response, err := api.sling.New().Delete(path).Receive(nil, &failed) + + if response.StatusCode != 204 { + return fmt.Errorf("delete notification failed, status: %d, message: %s", + response.StatusCode, failed) + } + + return err +} diff --git a/api/plugins.go b/api/plugins.go new file mode 100644 index 00000000..dc8cdee8 --- /dev/null +++ b/api/plugins.go @@ -0,0 +1,197 @@ +package api + +import ( + "fmt" + "log" + "strings" + "time" +) + +// EnablePlugin: enable a plugin on an instance. +func (api *API) EnablePlugin(instanceID int, pluginName string, sleep, timeout int) ( + map[string]interface{}, error) { + + var ( + failed map[string]interface{} + params = make(map[string]interface{}) + path = fmt.Sprintf("/api/instances/%d/plugins?async=true", instanceID) + ) + + params["plugin_name"] = pluginName + log.Printf("[DEBUG] go-api::plugin::enable instance id: %v, params: %v", instanceID, params) + response, err := api.sling.New().Post(path).BodyJSON(params).Receive(nil, &failed) + + if err != nil { + return nil, err + } + + switch response.StatusCode { + case 204: + return api.waitUntilPluginChanged(instanceID, pluginName, true, 1, sleep, timeout) + default: + return nil, + fmt.Errorf("enable plugin failed, status: %v, message: %s", response.StatusCode, failed) + } +} + +// ReadPlugin: reads a specific plugin from an instance. +func (api *API) ReadPlugin(instanceID int, pluginName string, sleep, timeout int) ( + map[string]interface{}, error) { + + log.Printf("[DEBUG] go-api::plugin::read instance id: %v, name: %v", instanceID, pluginName) + data, err := api.ListPlugins(instanceID, sleep, timeout) + if err != nil { + return nil, err + } + + for _, plugin := range data { + if plugin["name"] == pluginName { + log.Printf("[DEBUG] go-api::plugin::read plugin found: %v", pluginName) + return plugin, nil + } + } + + return nil, nil +} + +// ListPlugins: list plugins from an instance. +func (api *API) ListPlugins(instanceID, sleep, timeout int) ([]map[string]interface{}, error) { + return api.listPluginsWithRetry(instanceID, 1, sleep, timeout) +} + +// listPluginsWithRetry: list plugins from an instance, with retry if backend is busy. +func (api *API) listPluginsWithRetry(instanceID, attempt, sleep, timeout int) ( + []map[string]interface{}, error) { + + var ( + data []map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/plugins", instanceID) + ) + + response, err := api.sling.New().Get(path).Receive(&data, &failed) + if err != nil { + return nil, err + } else if attempt*sleep > timeout { + return nil, fmt.Errorf("read plugins reached timeout of %d seconds", timeout) + } + + switch response.StatusCode { + case 200: + return data, nil + case 400: + if strings.Compare(failed["error"].(string), "Timeout talking to backend") == 0 { + log.Printf("[INFO] go-api::plugins::read Timeout talking to backend "+ + "attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.listPluginsWithRetry(instanceID, attempt, sleep, timeout) + } + return nil, fmt.Errorf("ReadWithRetry failed, status: %v, message: %s", 400, failed) + default: + return nil, + fmt.Errorf("list plugin with retry failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// UpdatePlugin: updates a plugin from an instance. +func (api *API) UpdatePlugin(instanceID int, pluginName string, enabled bool, sleep, timeout int) ( + map[string]interface{}, error) { + + var ( + failed map[string]interface{} + params = make(map[string]interface{}) + path = fmt.Sprintf("/api/instances/%d/plugins?async=true", instanceID) + ) + + params["plugin_name"] = pluginName + params["enabled"] = enabled + log.Printf("[DEBUG] go-api::plugin::update instance ID: %v, params: %v", instanceID, params) + response, err := api.sling.New().Put(path).BodyJSON(params).Receive(nil, &failed) + + if err != nil { + return nil, err + } + + switch response.StatusCode { + case 204: + return api.waitUntilPluginChanged(instanceID, pluginName, enabled, 1, sleep, timeout) + default: + return nil, + fmt.Errorf("update plugin failed, status: %v, message: %s", response.StatusCode, failed) + } +} + +// DisablePlugin: disables a plugin from an instance. +func (api *API) DisablePlugin(instanceID int, pluginName string, sleep, timeout int) ( + map[string]interface{}, error) { + + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/plugins/%s?async=true", instanceID, pluginName) + ) + + log.Printf("[DEBUG] go-api::plugin::disable path: %s", path) + response, err := api.sling.New().Delete(path).Receive(nil, &failed) + + if err != nil { + return nil, err + } + + switch response.StatusCode { + case 204: + return api.waitUntilPluginChanged(instanceID, pluginName, false, 1, sleep, timeout) + default: + return nil, fmt.Errorf("disable plugin failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// DeletePlugin: deletes a plugin from an instance. +func (api *API) DeletePlugin(instanceID int, pluginName string, sleep, timeout int) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/plugins/%s?async=true", instanceID, pluginName) + ) + + log.Printf("[DEBUG] go-api::plugin::delete path: %s", path) + response, err := api.sling.New().Delete(path).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 204: + _, err = api.waitUntilPluginChanged(instanceID, pluginName, false, 1, sleep, timeout) + return err + default: + return fmt.Errorf("delete plugin failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// waitUntilPluginChanged: wait until plugin changed. +func (api *API) waitUntilPluginChanged(instanceID int, pluginName string, enabled bool, + attempt, sleep, timeout int) (map[string]interface{}, error) { + + for { + if attempt*sleep > timeout { + return nil, fmt.Errorf("wait until plugin changed reached timeout of %d seconds", timeout) + } + + response, err := api.ReadPlugin(instanceID, pluginName, sleep, timeout) + log.Printf("[DEBUG] go-api::plugin::waitUntilPluginChanged response: %v", response) + if err != nil { + return nil, err + } + if response["required"] != nil && response["required"] != false { + return response, nil + } + if response["enabled"] == enabled { + return response, nil + } + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + } +} diff --git a/api/plugins_community.go b/api/plugins_community.go new file mode 100644 index 00000000..79955216 --- /dev/null +++ b/api/plugins_community.go @@ -0,0 +1,168 @@ +package api + +import ( + "fmt" + "log" + "strings" + "time" +) + +// InstallPluginCommunity: install a community plugin on an instance. +func (api *API) InstallPluginCommunity(instanceID int, pluginName string, sleep, timeout int) ( + map[string]interface{}, error) { + + var ( + failed map[string]interface{} + params = make(map[string]interface{}) + path = fmt.Sprintf("/api/instances/%d/plugins/community?async=true", instanceID) + ) + + params["plugin_name"] = pluginName + log.Printf("[DEBUG] go-api::plugin_community::enable path: %s", path) + response, err := api.sling.New().Post(path).BodyJSON(params).Receive(nil, &failed) + if err != nil { + return nil, err + } + + switch response.StatusCode { + case 204: + return api.waitUntilPluginChanged(instanceID, pluginName, true, 1, sleep, timeout) + default: + return nil, fmt.Errorf("install community plugin failed, status: %v, message: %v", + response.StatusCode, failed) + } +} + +// ReadPluginCommunity: reads a specific community plugin from an instance. +func (api *API) ReadPluginCommunity(instanceID int, pluginName string, sleep, timeout int) ( + map[string]interface{}, error) { + + log.Printf("[DEBUG] go-api::plugin_community::read instance ID: %v, name: %v", instanceID, pluginName) + data, err := api.ListPluginsCommunity(instanceID, sleep, timeout) + if err != nil { + return nil, err + } + + for _, plugin := range data { + if plugin["name"] == pluginName { + log.Printf("[DEBUG] go-api::plugin_community::read found plugin: %v", pluginName) + return plugin, nil + } + } + + return nil, nil +} + +// ListPluginsCommunity: list all community plugins for an instance. +func (api *API) ListPluginsCommunity(instanceID, sleep, timeout int) ([]map[string]interface{}, error) { + return api.listPluginsCommunityWithRetry(instanceID, 1, sleep, timeout) +} + +// listPluginsCommunityWithRetry: list all community plugins for an instance, +// with retry if the backend is busy. +func (api *API) listPluginsCommunityWithRetry(instanceID, attempt, sleep, timeout int) ( + []map[string]interface{}, error) { + + var ( + data []map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/plugins/community", instanceID) + ) + + log.Printf("[DEBUG] go-api::plugin_community::listPluginsCommunityWithRetry path: %s", path) + response, err := api.sling.New().Get(path).Receive(&data, &failed) + + if err != nil { + return nil, err + } else if attempt*sleep > timeout { + return nil, fmt.Errorf("read plugins reached timeout of %d seconds", timeout) + } + + statusCode := response.StatusCode + log.Printf("[DEBUG] go-api::plugin_community::listPluginsCommunityWithRetry statusCode: %d", statusCode) + switch { + case statusCode == 400: + if strings.Compare(failed["error"].(string), "Timeout talking to backend") == 0 { + log.Printf("[INFO] go-api::plugins-community::read Timeout talking to backend "+ + "attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.listPluginsCommunityWithRetry(instanceID, attempt, sleep, timeout) + } + } + return data, nil +} + +// UpdatePluginCommunity: updates a community plugin from an instance. +func (api *API) UpdatePluginCommunity(instanceID int, pluginName string, enabled bool, + sleep, timeout int) (map[string]interface{}, error) { + + var ( + failed map[string]interface{} + params = make(map[string]interface{}) + path = fmt.Sprintf("/api/instances/%d/plugins/community?async=true", instanceID) + ) + + params["plugin_name"] = pluginName + params["enabled"] = enabled + log.Printf("[DEBUG] go-api::plugin_community::update path: %s", path) + response, err := api.sling.New().Put(path).BodyJSON(params).Receive(nil, &failed) + if err != nil { + return nil, err + } + + switch response.StatusCode { + case 204: + return api.waitUntilPluginChanged(instanceID, pluginName, enabled, 1, sleep, timeout) + default: + return nil, fmt.Errorf("UpdatePluginCommunity failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// UninstallPluginCommunity: uninstall a community plugin from an instance. +func (api *API) UninstallPluginCommunity(instanceID int, pluginName string, sleep, timeout int) ( + map[string]interface{}, error) { + + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/plugins/community/%s?async=true", instanceID, pluginName) + ) + + log.Printf("[DEBUG] go-api::plugin_community::disable path: %s", path) + response, err := api.sling.New().Delete(path).Receive(nil, &failed) + if err != nil { + return nil, err + } + + switch response.StatusCode { + case 204: + return api.waitUntilPluginUninstalled(instanceID, pluginName, 1, sleep, timeout) + default: + return nil, fmt.Errorf("DisablePluginCommunity failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// waitUntilPluginUninstalled: wait until a community plugin been uninstalled. +func (api *API) waitUntilPluginUninstalled(instanceID int, pluginName string, + attempt, sleep, timeout int) (map[string]interface{}, error) { + + log.Printf("[DEBUG] go-api::plugin_community::waitUntilPluginUninstalled instance id: %v, name: %v", + instanceID, pluginName) + for { + if attempt*sleep > timeout { + return nil, fmt.Errorf("wait until plugin uninstalled reached timeout of %d seconds", timeout) + } + + response, err := api.ReadPlugin(instanceID, pluginName, sleep, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return response, nil + } + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + } +} diff --git a/api/privatelink.go b/api/privatelink.go new file mode 100644 index 00000000..2af5b81c --- /dev/null +++ b/api/privatelink.go @@ -0,0 +1,151 @@ +package api + +import ( + "fmt" + "log" + "strings" + "time" +) + +// EnablePrivatelink: Enable PrivateLink and wait until finished. +// Need to enable VPC for an instance, if no standalone VPC used. +// Wait until finished with configureable sleep and timeout. +func (api *API) EnablePrivatelink(instanceID int, params map[string][]interface{}, + sleep, timeout int) error { + + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/privatelink", instanceID) + ) + + if err := api.EnableVPC(instanceID); err != nil { + return err + } + + response, err := api.sling.New().Post(path).BodyJSON(params).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 204: + return api.waitForEnablePrivatelinkWithRetry(instanceID, 1, sleep, timeout) + default: + return fmt.Errorf("enable PrivateLink failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// ReadPrivatelink: Reads PrivateLink information +func (api *API) ReadPrivatelink(instanceID, sleep, timeout int) (map[string]interface{}, error) { + return api.readPrivateLinkWithRetry(instanceID, 1, sleep, timeout) +} + +func (api *API) readPrivateLinkWithRetry(instanceID, attempt, sleep, timeout int) ( + map[string]interface{}, error) { + + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/privatelink", instanceID) + ) + + response, err := api.sling.New().Get(path).Receive(&data, &failed) + if err != nil { + return nil, err + } else if attempt*sleep > timeout { + return nil, fmt.Errorf("read PrivateLink failed, reached timeout of %d seconds", timeout) + } + + switch response.StatusCode { + case 200: + return data, nil + case 400: + if strings.Compare(failed["error"].(string), "Timeout talking to backend") == 0 { + log.Printf("[INFO] go-api::privatelink::read Timeout talking to backend "+ + "attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.readPrivateLinkWithRetry(instanceID, attempt, sleep, timeout) + } + } + + return nil, fmt.Errorf("read PrivateLink failed, status: %v, message: %s", + response.StatusCode, failed) +} + +// UpdatePrivatelink: Update allowed principals or subscriptions +func (api *API) UpdatePrivatelink(instanceID int, params map[string][]interface{}) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/privatelink", instanceID) + ) + + response, err := api.sling.New().Put(path).BodyJSON(params).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 204: + return nil + default: + return fmt.Errorf("update Privatelink failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// DisablePrivatelink: Disable the PrivateLink feature +func (api *API) DisablePrivatelink(instanceID int) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/privatelink", instanceID) + ) + + response, err := api.sling.New().Delete(path).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 204: + return nil + default: + return fmt.Errorf("disable Privatelink failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// waitForEnablePrivatelinkWithRetry: Wait until status change from pending to enable +func (api *API) waitForEnablePrivatelinkWithRetry(instanceID, attempt, sleep, timeout int) error { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/privatelink", instanceID) + ) + + response, err := api.sling.New().Get(path).Receive(&data, &failed) + if err != nil { + return err + } else if attempt*sleep > timeout { + return fmt.Errorf("enable PrivateLink failed, reached timeout of %d seconds", timeout) + } + log.Printf("[DEBUG] PrivateLink: waitForEnablePrivatelinkWithRetry data: %v", data) + + switch response.StatusCode { + case 200: + switch data["status"].(string) { + case "enabled": + return nil + case "pending": + log.Printf("[DEBUG] go-api::privatelink::enable not finished and will retry, "+ + "attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.waitForEnablePrivatelinkWithRetry(instanceID, attempt, sleep, timeout) + } + } + + return fmt.Errorf("wait for enable PrivateLink failed, status: %v, message: %s", + response.StatusCode, failed) +} diff --git a/api/rabbitmq_configuration.go b/api/rabbitmq_configuration.go new file mode 100644 index 00000000..8d3465b2 --- /dev/null +++ b/api/rabbitmq_configuration.go @@ -0,0 +1,99 @@ +package api + +import ( + "fmt" + "log" + "strings" + "time" +) + +func (api *API) ReadRabbitMqConfiguration(instanceID, sleep, timeout int) (map[string]interface{}, error) { + return api.readRabbitMqConfigurationWithRetry(instanceID, 1, sleep, timeout) +} + +func (api *API) readRabbitMqConfigurationWithRetry(instanceID, attempt, sleep, timeout int) (map[string]interface{}, error) { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/config", instanceID) + ) + response, err := api.sling.New().Get(path).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::rabbitmq-configuration#readWithRetry data: %v", data) + + if err != nil { + return nil, err + } + + switch response.StatusCode { + case 200: + return data, nil + case 400: + if strings.Compare(failed["error"].(string), "Timeout talking to backend") == 0 { + log.Printf("[DEBUG] go-api::rabbitmq-configuration::readWithRetry Timeout talking to backend, "+ + "attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.readRabbitMqConfigurationWithRetry(instanceID, attempt, sleep, timeout) + } else { + break + } + case 503: + if strings.Compare(failed["error"].(string), "Timeout talking to backend") == 0 { + log.Printf("[DEBUG] go-api::rabbitmq-configuration::readWithRetry Timeout talking to backend, "+ + "attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.readRabbitMqConfigurationWithRetry(instanceID, attempt, sleep, timeout) + } + } + return nil, fmt.Errorf("read RabbitMQ configuration failed, status: %v, message: %s", response.StatusCode, failed) +} + +func (api *API) UpdateRabbitMqConfiguration(instanceID int, params map[string]interface{}, + sleep, timeout int) error { + return api.updateRabbitMqConfigurationWithRetry(instanceID, params, 1, sleep, timeout) +} + +func (api *API) updateRabbitMqConfigurationWithRetry(instanceID int, params map[string]interface{}, + attempt, sleep, timeout int) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("api/instances/%d/config", instanceID) + ) + response, err := api.sling.New().Put(path).BodyJSON(params).Receive(nil, &failed) + if err != nil { + return err + } else if attempt*sleep > timeout { + return fmt.Errorf("update RabbitMQ configuraiton failed, reached timeout of %d seconds", timeout) + } + + switch response.StatusCode { + case 200: + return nil + case 400: + if strings.Compare(failed["error"].(string), "Timeout talking to backend") == 0 { + log.Printf("[DEBUG] go-api::rabbitmq-configuration::updateWithRetry Timeout talking to backend, "+ + "attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.updateRabbitMqConfigurationWithRetry(instanceID, params, attempt, sleep, timeout) + } else { + break + } + case 503: + if strings.Compare(failed["error"].(string), "Timeout talking to backend") == 0 { + log.Printf("[DEBUG] go-api::rabbitmq-configuration::updateWithRetry Timeout talking to backend, "+ + "attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.updateRabbitMqConfigurationWithRetry(instanceID, params, attempt, sleep, timeout) + } else { + break + } + } + return fmt.Errorf("update RabbitMQ configuration failed, status: %v, message: %s", response.StatusCode, failed) +} + +func (api *API) DeleteRabbitMqConfiguration() error { + return nil +} diff --git a/api/security_firewall.go b/api/security_firewall.go new file mode 100644 index 00000000..430e826e --- /dev/null +++ b/api/security_firewall.go @@ -0,0 +1,218 @@ +package api + +import ( + "fmt" + "log" + "time" +) + +func (api *API) waitUntilFirewallConfigured(instanceID, attempt, sleep, timeout int) error { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/security/firewall/configured", instanceID) + ) + + for { + response, err := api.sling.New().Path(path).Receive(&data, &failed) + if err != nil { + return err + } else if attempt*sleep > timeout { + return fmt.Errorf("wait until firewall configured failed, reached timeout of %d seconds", timeout) + } + + switch response.StatusCode { + case 200: + return nil + case 400: + log.Printf("[DEBUG] go-api::security_firewall#waitUntilFirewallConfigured: The cluster is unavailable, firewall configuring") + default: + return fmt.Errorf("waitUntilReady failed, status: %v, message: %s", response.StatusCode, failed) + } + + log.Printf("[INFO] go-api::security_firewall::waitUntilFirewallConfigured The cluster is unavailable, "+ + "firewall configuring. Attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + } +} + +func (api *API) CreateFirewallSettings(instanceID int, params []map[string]interface{}, sleep, + timeout int) ([]map[string]interface{}, error) { + attempt, err := api.createFirewallSettingsWithRetry(instanceID, params, 1, sleep, timeout) + if err != nil { + return nil, err + } + err = api.waitUntilFirewallConfigured(instanceID, attempt, sleep, timeout) + if err != nil { + return nil, err + } + return api.ReadFirewallSettings(instanceID) +} + +func (api *API) createFirewallSettingsWithRetry(instanceID int, params []map[string]interface{}, + attempt, sleep, timeout int) (int, error) { + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/security/firewall", instanceID) + ) + log.Printf("[DEBUG] go-api::security_firewall::create instance ID: %v, params: %v", instanceID, params) + response, err := api.sling.New().Post(path).BodyJSON(params).Receive(nil, &failed) + + if err != nil { + return attempt, err + } else if attempt*sleep > timeout { + return attempt, fmt.Errorf("create firewall settings failed, reached timeout of %d seconds", timeout) + } + + switch { + case response.StatusCode == 201: + return attempt, nil + case response.StatusCode == 400: + switch { + case failed["error_code"] == nil: + break + case failed["error_code"].(float64) == 40001: + log.Printf("[INFO] go-api::security_firewall::create Firewall not finished configuring "+ + "attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.createFirewallSettingsWithRetry(instanceID, params, attempt, sleep, timeout) + case failed["error_code"].(float64) == 40002: + return attempt, fmt.Errorf("firewall rules validation failed due to: %s", failed["error"].(string)) + } + } + return attempt, fmt.Errorf("create new firewall rules failed, status: %v, message: %s", response.StatusCode, failed) +} + +func (api *API) ReadFirewallSettings(instanceID int) ([]map[string]interface{}, error) { + var ( + data []map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/security/firewall", instanceID) + ) + response, err := api.sling.New().Path(path).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::security_firewall::read data: %v", data) + + if err != nil { + return nil, err + } + + if response.StatusCode == 200 { + return data, nil + } + return nil, fmt.Errorf("ReadFirewallSettings failed, status: %v, message: %s", response.StatusCode, failed) +} + +func (api *API) UpdateFirewallSettings(instanceID int, params []map[string]interface{}, + sleep, timeout int) ([]map[string]interface{}, error) { + log.Printf("[DEBUG] go-api::security_firewall::update instance id: %v, params: %v, sleep: %d, timeout: %d", + instanceID, params, sleep, timeout) + attempt, err := api.updateFirewallSettingsWithRetry(instanceID, params, 1, sleep, timeout) + if err != nil { + return nil, err + } + err = api.waitUntilFirewallConfigured(instanceID, attempt, sleep, timeout) + if err != nil { + return nil, err + } + return api.ReadFirewallSettings(instanceID) +} + +func (api *API) updateFirewallSettingsWithRetry(instanceID int, params []map[string]interface{}, + attempt, sleep, timeout int) (int, error) { + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/security/firewall", instanceID) + ) + + response, err := api.sling.New().Put(path).BodyJSON(params).Receive(nil, &failed) + if err != nil { + return attempt, err + } else if attempt*sleep > timeout { + return attempt, fmt.Errorf("update firewall settings failed, reached timeout of %d seconds", timeout) + } + + switch response.StatusCode { + case 204: + return attempt, nil + case 400: + switch { + case failed["error_code"] == nil: + break + case failed["error_code"].(float64) == 40001: + log.Printf("[INFO] go-api::security_firewall::update Firewall not finished configuring "+ + "attempt: %d until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.updateFirewallSettingsWithRetry(instanceID, params, attempt, sleep, timeout) + case failed["error_code"].(float64) == 40002: + return attempt, fmt.Errorf("firewall rules validation failed due to: %s", failed["error"].(string)) + } + } + return attempt, fmt.Errorf("update firewall rules failed, status: %v, message: %v", + response.StatusCode, failed) +} + +func (api *API) DeleteFirewallSettings(instanceID, sleep, timeout int) ([]map[string]interface{}, error) { + log.Printf("[DEBUG] go-api::security_firewall::delete instance id: %v, sleep: %d, timeout: %d", + instanceID, sleep, timeout) + attempt, err := api.deleteFirewallSettingsWithRetry(instanceID, 1, sleep, timeout) + if err != nil { + return nil, err + } + + err = api.waitUntilFirewallConfigured(instanceID, attempt, sleep, timeout) + if err != nil { + return nil, err + } + return api.ReadFirewallSettings(instanceID) +} + +func (api *API) deleteFirewallSettingsWithRetry(instanceID, attempt, sleep, timeout int) (int, error) { + var ( + params [1]map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/security/firewall", instanceID) + ) + + // Use default firewall rule and update firewall upon delete. + params[0] = DefaultFirewallSettings() + log.Printf("[DEBUG] go-api::security_firewall::delete default firewall: %v", params[0]) + response, err := api.sling.New().Put(path).BodyJSON(params).Receive(nil, &failed) + if err != nil { + return attempt, err + } else if attempt*sleep > timeout { + return attempt, fmt.Errorf("delete firewall settings failed, reached timeout of %d seconds", timeout) + } + + switch response.StatusCode { + case 204: + return attempt, nil + case 400: + switch { + case failed["error_code"] == nil: + break + case failed["error_code"].(float64) == 40001: + log.Printf("[INFO] go-api::security_firewall::delete Firewall not finished configuring "+ + "attempt: %d until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.deleteFirewallSettingsWithRetry(instanceID, attempt, sleep, timeout) + case failed["error_code"].(float64) == 40002: + return attempt, fmt.Errorf("firewall rules validation failed due to: %s", failed["error"].(string)) + } + } + return attempt, fmt.Errorf("delete firewall rules failed, status: %v, message: %v", + response.StatusCode, failed) +} + +func DefaultFirewallSettings() map[string]interface{} { + defaultRule := map[string]interface{}{ + "services": []string{"AMQP", "AMQPS", "STOMP", "STOMPS", "MQTT", "MQTTS", "HTTPS", "STREAM", "STREAM_SSL"}, + "ports": []int{}, + "ip": "0.0.0.0/0", + "description": "Default", + } + return defaultRule +} diff --git a/api/upgrade_rabbitmq.go b/api/upgrade_rabbitmq.go new file mode 100644 index 00000000..ff50228e --- /dev/null +++ b/api/upgrade_rabbitmq.go @@ -0,0 +1,69 @@ +package api + +import ( + "fmt" + "log" + "time" +) + +// ReadVersions - Read versions RabbitMQ and Erlang can upgrade to +func (api *API) ReadVersions(instanceID int) (map[string]interface{}, error) { + data := make(map[string]interface{}) + failed := make(map[string]interface{}) + log.Printf("[DEBUG] go-api::upgrade_rabbitmq::read_versions instance id: %d", instanceID) + path := fmt.Sprintf("api/instances/%d/actions/new-rabbitmq-erlang-versions", instanceID) + response, err := api.sling.New().Path(path).Receive(&data, &failed) + if err != nil { + return nil, err + } + if response.StatusCode != 200 { + return nil, fmt.Errorf("ReadVersions failed, status: %v, message: %s", response.StatusCode, failed) + } + return data, nil +} + +// UpgradeRabbitMQ - Upgrade to latest possible versions for both RabbitMQ and Erlang. +func (api *API) UpgradeRabbitMQ(instanceID int) (string, error) { + data := make(map[string]interface{}) + failed := make(map[string]interface{}) + path := fmt.Sprintf("api/instances/%d/actions/upgrade-rabbitmq-erlang", instanceID) + response, err := api.sling.New().Post(path).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::upgrade_rabbitmq::upgrade_rabbitmq_mq data: %v, status code: %v", data, response.StatusCode) + if err != nil { + return "", err + } + if response.StatusCode == 200 { + return "Already at highest possible version", nil + } else if response.StatusCode != 202 { + return "", fmt.Errorf("upgrade RabbitMQ failed, status: %v, message: %s", response.StatusCode, failed) + } + + return api.waitUntilUpgraded(instanceID) +} + +func (api *API) waitUntilUpgraded(instanceID int) (string, error) { + var data []map[string]interface{} + failed := make(map[string]interface{}) + + for { + path := fmt.Sprintf("api/instances/%v/nodes", instanceID) + _, err := api.sling.New().Path(path).Receive(&data, &failed) + if err != nil { + log.Printf("[ERROR] go-api::upgrade_rabbitmq::waitUntilUpgraded error: %v", err) + return "", err + } + log.Printf("[DEBUG] go-api::upgrade_rabbitmq::waitUntilUpgraded numberOfNodes: %v", len(data)) + log.Printf("[DEBUG] go-api::upgrade_rabbitmq::waitUntilUpgraded data: %v", data) + ready := true + for _, node := range data { + log.Printf("[DEBUG] go-api::upgrade_rabbitmq::waitUntilUpgraded ready: %v, configured: %v", + ready, node["configured"]) + ready = ready && node["configured"].(bool) + } + log.Printf("[DEBUG] go-api::upgrade_rabbitmq::waitUntilUpgraded ready: %v", ready) + if ready { + return "", nil + } + time.Sleep(30 * time.Second) + } +} diff --git a/api/vpc.go b/api/vpc.go new file mode 100644 index 00000000..c07344ae --- /dev/null +++ b/api/vpc.go @@ -0,0 +1,161 @@ +package api + +import ( + "fmt" + "log" + "strconv" + "time" +) + +func (api *API) waitUntilVpcReady(vpcID string) error { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("api/vpcs/%s/vpc-peering/info", vpcID) + ) + + log.Printf("[DEBUG] go-api::vpc::waitUntilVpcReady waiting") + for { + response, err := api.sling.New().Get(path).Receive(&data, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 200: + return nil + case 400: + log.Printf("[WARN] go-api::vpc::waitUntilVpcReady status: %v, message: %s", + response.StatusCode, failed) + default: + return fmt.Errorf("waitUntilReady failed, status: %v, message: %s", + response.StatusCode, failed) + } + time.Sleep(10 * time.Second) + } +} + +func (api *API) readVpcName(vpcID string) (map[string]interface{}, error) { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("api/vpcs/%s/vpc-peering/info", vpcID) + ) + + response, err := api.sling.New().Get(path).Receive(&data, &failed) + if err != nil { + return nil, err + } + + switch response.StatusCode { + case 200: + return data, nil + default: + return nil, fmt.Errorf("readVpcName failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +func (api *API) CreateVpcInstance(params map[string]interface{}) (map[string]interface{}, error) { + var ( + data map[string]interface{} + failed map[string]interface{} + path = "/api/vpcs" + ) + + log.Printf("[DEBUG] go-api::vpc::create params: %v", params) + response, err := api.sling.New().Post(path).BodyJSON(params).Receive(&data, &failed) + if err != nil { + return nil, err + } + + switch response.StatusCode { + case 200: + if id, ok := data["id"]; ok { + data["id"] = strconv.FormatFloat(id.(float64), 'f', 0, 64) + log.Printf("[DEBUG] go-api::vpc::create id set: %v", data["id"]) + } else { + return nil, fmt.Errorf("create VPC invalid instance identifier: %v", data["id"]) + } + api.waitUntilVpcReady(data["id"].(string)) + return data, nil + default: + return nil, fmt.Errorf("create VPC failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +func (api *API) ReadVpcInstance(vpcID string) (map[string]interface{}, error) { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/vpcs/%s", vpcID) + ) + + log.Printf("[DEBUG] go-api::vpc::read vpc ID: %s", vpcID) + response, err := api.sling.New().Path(path).Receive(&data, &failed) + if err != nil { + return nil, err + } + + switch response.StatusCode { + case 200: + data_temp, _ := api.readVpcName(vpcID) + data["vpc_name"] = data_temp["name"] + return data, nil + case 410: + log.Printf("[WARN] go-api::vpc::read status: 410, message: The VPC has been deleted") + return nil, nil + default: + return nil, fmt.Errorf("read VPC failed, status: %v, message: %v", + response.StatusCode, failed) + } +} + +func (api *API) UpdateVpcInstance(vpcID string, params map[string]interface{}) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("api/vpcs/%s", vpcID) + ) + + log.Printf("[DEBUG] go-api::instance::update vpc ID: %s, params: %v", vpcID, params) + response, err := api.sling.New().Put(path).BodyJSON(params).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 200: + return nil + case 410: + log.Printf("[WARN] go-api::vpc::update status: 410, message: The VPC has been deleted") + return nil + default: + return fmt.Errorf("update VPC failed, status: %v, message: %v", + response.StatusCode, failed) + } +} + +func (api *API) DeleteVpcInstance(vpcID string) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("api/vpcs/%s", vpcID) + ) + + log.Printf("[DEBUG] go-api::vpc::delete vpc ID: %s", vpcID) + response, err := api.sling.New().Delete(path).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 204: + return nil + case 410: + log.Printf("[WARN] go-api::vpc::delete status: 410, message: The VPC has been deleted") + return nil + default: + return fmt.Errorf("delete VPC failed, status: %v, message: %v", + response.StatusCode, failed) + } +} diff --git a/api/vpc_connect.go b/api/vpc_connect.go new file mode 100644 index 00000000..94b034c8 --- /dev/null +++ b/api/vpc_connect.go @@ -0,0 +1,163 @@ +package api + +import ( + "fmt" + "log" + "time" +) + +// EnableVpcConnect: Enable VPC Connect and wait until finished. +// Need to enable VPC for an instance, if no standalone VPC used. +// Wait until finished with configureable sleep and timeout. +func (api *API) EnableVpcConnect(instanceID int, params map[string][]interface{}, + sleep, timeout int) error { + + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/vpc-connect", instanceID) + ) + + if err := api.EnableVPC(instanceID); err != nil { + return err + } + + response, err := api.sling.New().Post(path).BodyJSON(params).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 204: + return api.waitForEnableVpcConnectWithRetry(instanceID, 1, sleep, timeout) + default: + return fmt.Errorf("enable VPC Connect failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// ReadVpcConnect: Reads VPC Connect information +func (api *API) ReadVpcConnect(instanceID int) (map[string]interface{}, error) { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/vpc-connect", instanceID) + ) + + response, err := api.sling.New().Get(path).Receive(&data, &failed) + if err != nil { + return nil, err + } + + switch response.StatusCode { + case 200: + return data, nil + default: + return nil, fmt.Errorf("read VPC Connect failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// UpdateVpcConnect: Update allowlist for the VPC Connect +func (api *API) UpdateVpcConnect(instanceID int, params map[string][]interface{}) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/vpc-connect", instanceID) + ) + + response, err := api.sling.New().Put(path).BodyJSON(params).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 204: + return nil + default: + return fmt.Errorf("update VPC connect failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// DisableVpcConnect: Disable the VPC Connect feature +func (api *API) DisableVpcConnect(instanceID int) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/vpc-connect", instanceID) + ) + + response, err := api.sling.New().Delete(path).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 204: + return nil + default: + return fmt.Errorf("disable VPC Connect failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// waitForEnableVpcConnectWithRetry: Wait until status change from pending to enable +func (api *API) waitForEnableVpcConnectWithRetry(instanceID, attempt, sleep, timeout int) error { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/vpc-connect", instanceID) + ) + + response, err := api.sling.New().Get(path).Receive(&data, &failed) + if err != nil { + return err + } else if attempt*sleep > timeout { + return fmt.Errorf("enable VPC Connect failed, reached timeout of %d seconds", timeout) + } + log.Printf("[DEBUG] VPC-Connect: waitForEnableVpcConnectWithRetry data: %v", data) + + switch response.StatusCode { + case 200: + switch data["status"].(string) { + case "enabled": + return nil + case "pending": + log.Printf("[DEBUG] go-api::vpc-connect::enable not finished and will retry, "+ + "attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.waitForEnableVpcConnectWithRetry(instanceID, attempt, sleep, timeout) + } + } + + return fmt.Errorf("wait for enable VPC Connect failed, status: %v, message: %s", + response.StatusCode, failed) +} + +// enableVPC: Enable VPC for an instance +// Check if the instance already have a standalone VPC +func (api *API) EnableVPC(instanceID int) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/vpc", instanceID) + ) + + data, _ := api.ReadInstance(fmt.Sprintf("%d", instanceID)) + if data["vpc"] == nil { + response, err := api.sling.New().Put(path).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 200: + log.Printf("[DEBUG] VPC-Connect: VPC features enabled") + return nil + default: + return fmt.Errorf("enable VPC failed, status: %v, message: %s", + response.StatusCode, failed) + } + } + + log.Printf("[DEBUG] VPC-Connect: VPC features already enabled") + return nil +} diff --git a/api/vpc_gcp_peering.go b/api/vpc_gcp_peering.go new file mode 100644 index 00000000..bd0cbe7b --- /dev/null +++ b/api/vpc_gcp_peering.go @@ -0,0 +1,211 @@ +package api + +import ( + "fmt" + "log" + "strings" + "time" +) + +// waitForGcpPeeringStatus: waits for the VPC peering status to be ACTIVE or until timed out +func (api *API) waitForGcpPeeringStatus(path, peerID string, + attempt, sleep, timeout int) error { + + var ( + data map[string]interface{} + err error + ) + + for { + if attempt*sleep > timeout { + return fmt.Errorf("wait until GCP VPC peering status reached timeout of %d seconds", timeout) + } + + attempt, data, err = api.readVpcGcpPeeringWithRetry(path, attempt, sleep, timeout) + if err != nil { + return err + } + + rows := data["rows"].([]interface{}) + if len(rows) > 0 { + for _, row := range rows { + tempRow := row.(map[string]interface{}) + if tempRow["name"] != peerID { + continue + } + if tempRow["state"] == "ACTIVE" { + return nil + } + } + } + log.Printf("[INFO] go-api::vpc_gcp_peering::waitForGcpPeeringStatus Waiting for state = ACTIVE "+ + "attempt %d until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + } +} + +// RequestVpcGcpPeering: requests a VPC peering from an instance. +func (api *API) RequestVpcGcpPeering(instanceID int, params map[string]interface{}, + waitOnStatus bool, sleep, timeout int) (map[string]interface{}, error) { + + path := fmt.Sprintf("api/instances/%v/vpc-peering", instanceID) + attempt, data, err := api.requestVpcGcpPeeringWithRetry(path, params, waitOnStatus, 1, sleep, timeout) + if err != nil { + return nil, err + } + + if waitOnStatus { + log.Printf("[DEBUG] go-api::vpc_gcp_peering_withvpcid::request waiting for active state") + err = api.waitForGcpPeeringStatus(path, data["peering"].(string), attempt, sleep, timeout) + if err != nil { + return nil, err + } + } + + return data, nil +} + +// requestVpcGcpPeeringWithRetry: requests a VPC peering from a path with retry logic +func (api *API) requestVpcGcpPeeringWithRetry(path string, params map[string]interface{}, + waitOnStatus bool, attempt, sleep, timeout int) (int, map[string]interface{}, error) { + var ( + data map[string]interface{} + failed map[string]interface{} + ) + + log.Printf("[DEBUG] go-api::vpc_gcp_peering::request path: %s, params: %v", path, params) + response, err := api.sling.New().Post(path).BodyJSON(params).Receive(&data, &failed) + if err != nil { + return attempt, nil, err + } else if attempt*sleep > timeout { + return attempt, nil, + fmt.Errorf("request VPC peering failed, reached timeout of %d seconds", timeout) + } + + switch response.StatusCode { + case 200: + return attempt, data, nil + case 400: + if strings.Compare(failed["error"].(string), "Timeout talking to backend") == 0 { + log.Printf("[INFO] go-api::vpc_gcp_peering::request Timeout talking to backend "+ + "attempt %d until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.requestVpcGcpPeeringWithRetry(path, params, waitOnStatus, attempt, sleep, timeout) + } + } + return attempt, nil, fmt.Errorf("request VPC peering failed, status: %v, message: %s", + response.StatusCode, failed) +} + +// ReadVpcGcpPeering: reads the VPC peering from the API +func (api *API) ReadVpcGcpPeering(instanceID, sleep, timeout int) ( + map[string]interface{}, error) { + + path := fmt.Sprintf("/api/instances/%v/vpc-peering", instanceID) + _, data, err := api.readVpcGcpPeeringWithRetry(path, 1, sleep, timeout) + return data, err +} + +// readVpcGcpPeeringWithRetry: reads the VPC peering from the API with retry logic +func (api *API) readVpcGcpPeeringWithRetry(path string, attempt, sleep, timeout int) ( + int, map[string]interface{}, error) { + + var ( + data map[string]interface{} + failed map[string]interface{} + ) + + log.Printf("[DEBUG] go-api::vpc_gcp_peering::read path: %s", path) + response, err := api.sling.New().Get(path).Receive(&data, &failed) + if err != nil { + return attempt, nil, err + } else if attempt*sleep > timeout { + return attempt, nil, fmt.Errorf("read VPC peering reached timeout of %d seconds", timeout) + } + + switch response.StatusCode { + case 200: + return attempt, data, nil + case 400: + if strings.Compare(failed["error"].(string), "Timeout talking to backend") == 0 { + log.Printf("[INFO] go-api::vpc_gcp_peering::read Timeout talking to backend "+ + "attempt %d until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.readVpcGcpPeeringWithRetry(path, attempt, sleep, timeout) + } + } + return attempt, nil, fmt.Errorf("read VPC peering with retry failed, status: %v, message: %s", + response.StatusCode, failed) +} + +// UpdateVpcGcpPeering: updates a VPC peering from an instance. +func (api *API) UpdateVpcGcpPeering(instanceID int, sleep, timeout int) ( + map[string]interface{}, error) { + + // NOP just read out the VPC peering + return api.ReadVpcGcpPeering(instanceID, sleep, timeout) +} + +// RemoveVpcGcpPeering: removes a VPC peering from an instance. +func (api *API) RemoveVpcGcpPeering(instanceID int, peerID string) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%v/vpc-peering/%v", instanceID, peerID) + ) + + log.Printf("[DEBUG] go-api::vpc_gcp_peering::remove instance id: %v, peering id: %v", instanceID, peerID) + response, err := api.sling.New().Delete(path).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 204: + return nil + default: + return fmt.Errorf("remove VPC peering failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// ReadVpcGcpInfo: reads the VPC info from the API +func (api *API) ReadVpcGcpInfo(instanceID, sleep, timeout int) (map[string]interface{}, error) { + path := fmt.Sprintf("/api/instances/%v/vpc-peering/info", instanceID) + return api.readVpcGcpInfoWithRetry(path, 1, sleep, timeout) +} + +// readVpcGcpInfoWithRetry: reads the VPC info from the API with retry logic +func (api *API) readVpcGcpInfoWithRetry(path string, attempt, sleep, timeout int) ( + map[string]interface{}, error) { + + var ( + data map[string]interface{} + failed map[string]interface{} + ) + + log.Printf("[DEBUG] go-api::vpc_gcp_peering::info path: %s", path) + response, err := api.sling.New().Get(path).Receive(&data, &failed) + if err != nil { + return nil, err + } else if attempt*sleep > timeout { + return nil, fmt.Errorf("read VPC info, reached timeout of %d seconds", timeout) + } + + switch response.StatusCode { + case 200: + return data, nil + case 400: + if strings.Compare(failed["error"].(string), "Timeout talking to backend") == 0 { + log.Printf("[INFO] go-api::vpc_gcp_peering::info Timeout talking to backend "+ + "attempt %d until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.readVpcGcpInfoWithRetry(path, attempt, sleep, timeout) + } + } + return nil, fmt.Errorf("read VPC info failed, status: %v, message: %s", + response.StatusCode, failed) +} diff --git a/api/vpc_gcp_peering_withvpcid.go b/api/vpc_gcp_peering_withvpcid.go new file mode 100644 index 00000000..87bc91e4 --- /dev/null +++ b/api/vpc_gcp_peering_withvpcid.go @@ -0,0 +1,77 @@ +package api + +// VPC peering for GCP, using vpcID as identifier. + +import ( + "fmt" + "log" +) + +// RequestVpcGcpPeeringWithVpcId: requests a VPC peering from an instance. +func (api *API) RequestVpcGcpPeeringWithVpcId(vpcID string, params map[string]interface{}, + waitOnStatus bool, sleep, timeout int) (map[string]interface{}, error) { + + path := fmt.Sprintf("api/vpcs/%s/vpc-peering", vpcID) + attempt, data, err := api.requestVpcGcpPeeringWithRetry(path, params, waitOnStatus, 1, sleep, timeout) + if err != nil { + return nil, err + } + + if waitOnStatus { + log.Printf("[DEBUG] go-api::vpc_gcp_peering_withvpcid::request waiting for active state") + err = api.waitForGcpPeeringStatus(path, data["peering"].(string), attempt, sleep, timeout) + if err != nil { + return nil, err + } + } + + return data, nil +} + +func (api *API) ReadVpcGcpPeeringWithVpcId(vpcID string, sleep, timeout int) ( + map[string]interface{}, error) { + + path := fmt.Sprintf("/api/vpcs/%s/vpc-peering", vpcID) + _, data, err := api.readVpcGcpPeeringWithRetry(path, 1, sleep, timeout) + return data, err +} + +// UpdateVpcGcpPeeringWithVpcId: updates the VPC peering from the API +func (api *API) UpdateVpcGcpPeeringWithVpcId(vpcID string, sleep, timeout int) ( + map[string]interface{}, error) { + + // NOP just read out the VPC peering + return api.ReadVpcGcpPeeringWithVpcId(vpcID, sleep, timeout) +} + +// RemoveVpcGcpPeeringWithVpcId: removes the VPC peering from the API +func (api *API) RemoveVpcGcpPeeringWithVpcId(vpcID, peerID string) error { + var ( + failed map[string]interface{} + path = fmt.Sprintf("/api/vpcs/%s/vpc-peering/%s", vpcID, peerID) + ) + + log.Printf("[DEBUG] go-api::vpc_gcp_peering_withvpcid::remove vpc id: %s, peering id: %s", + vpcID, peerID) + response, err := api.sling.New().Delete(path).Receive(nil, &failed) + if err != nil { + return err + } + + switch response.StatusCode { + case 204: + return nil + default: + return fmt.Errorf("remove VPC peering failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// ReadVpcGcpInfoWithVpcId: reads the VPC info from the API +func (api *API) ReadVpcGcpInfoWithVpcId(vpcID string, sleep, timeout int) ( + map[string]interface{}, error) { + + path := fmt.Sprintf("/api/vpcs/%s/vpc-peering/info", vpcID) + _, data, err := api.readVpcGcpPeeringWithRetry(path, 1, sleep, timeout) + return data, err +} diff --git a/api/vpc_peering.go b/api/vpc_peering.go new file mode 100644 index 00000000..7450eadc --- /dev/null +++ b/api/vpc_peering.go @@ -0,0 +1,182 @@ +package api + +import ( + "fmt" + "log" + "strings" + "time" +) + +func (api *API) AcceptVpcPeering(instanceID int, peeringID string, sleep, timeout int) (map[string]interface{}, error) { + attempt, err := api.waitForPeeringStatus(instanceID, peeringID, 1, sleep, timeout) + log.Printf("[DEBUG] go-api::vpc_peering::accept attempt: %d, sleep: %d, timeout: %d", attempt, sleep, timeout) + if err != nil { + return nil, err + } + path := fmt.Sprintf("/api/instances/%v/vpc-peering/request/%v", instanceID, peeringID) + return api.retryAcceptVpcPeering(path, attempt, sleep, timeout) +} + +func (api *API) ReadVpcInfo(instanceID int) (map[string]interface{}, error) { + path := fmt.Sprintf("/api/instances/%v/vpc-peering/info", instanceID) + // Initiale values, 5 attempts and 20 second sleep + return api.readVpcInfoWithRetry(path, 5, 20) +} + +func (api *API) ReadVpcPeeringRequest(instanceID int, peeringID string) (map[string]interface{}, error) { + data := make(map[string]interface{}) + failed := make(map[string]interface{}) + log.Printf("[DEBUG] go-api::vpc_peering::request instance id: %v, peering id: %v", instanceID, peeringID) + path := fmt.Sprintf("/api/instances/%v/vpc-peering/request/%v", instanceID, peeringID) + response, err := api.sling.New().Get(path).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::vpc_peering::request data: %v", data) + + if err != nil { + return nil, err + } else if response.StatusCode != 200 { + return nil, fmt.Errorf("readRequest failed, status: %v, message: %s", response.StatusCode, failed) + } + + return data, nil +} + +func (api *API) RemoveVpcPeering(instanceID int, peeringID string, sleep, timeout int) error { + path := fmt.Sprintf("/api/instances/%v/vpc-peering/%v", instanceID, peeringID) + return api.retryRemoveVpcPeering(path, 1, sleep, timeout) +} + +func (api *API) retryAcceptVpcPeering(path string, attempt, sleep, timeout int) (map[string]interface{}, error) { + log.Printf("[DEBUG] go-api::vpc_peering::retryRemoveVpcPeering path: %s, "+ + "attempt: %d, sleep: %d, timeout: %d", path, attempt, sleep, timeout) + data := make(map[string]interface{}) + failed := make(map[string]interface{}) + response, err := api.sling.New().Put(path).Receive(&data, &failed) + + if err != nil { + return nil, err + } else if attempt*sleep > timeout { + return nil, fmt.Errorf("accept VPC peering failed, reached timeout of %d seconds", timeout) + } + + switch response.StatusCode { + case 200: + return data, nil + case 400: + switch { + case failed["error_code"] == nil: + break + case failed["error_code"].(float64) == 40001: + log.Printf("[DEBUG] go-api::vpc_peering::accept firewall not finished configuring will retry "+ + "accept VPC peering, attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.retryAcceptVpcPeering(path, attempt, sleep, timeout) + } + } + + return nil, fmt.Errorf("accept VPC peering failed, status: %v, message: %s", response.StatusCode, failed) +} + +func (api *API) readVpcInfoWithRetry(path string, attempts, sleep int) (map[string]interface{}, error) { + log.Printf("[DEBUG] go-api::vpc_peering::readVpcInfoWithRetry path: %s, "+ + "attempts: %d, sleep: %d", path, attempts, sleep) + data := make(map[string]interface{}) + failed := make(map[string]interface{}) + response, err := api.sling.New().Get(path).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::vpc_peering::info data: %v", data) + + if err != nil { + return nil, err + } + + switch response.StatusCode { + case 200: + return data, nil + case 400: + if strings.Compare(failed["error"].(string), "Timeout talking to backend") == 0 { + if attempts--; attempts > 0 { + log.Printf("[INFO] go-api::vpc_peering::info Timeout talking to backend "+ + "attempts left %d and retry in %d seconds", attempts, sleep) + time.Sleep(time.Duration(sleep) * time.Second) + return api.readVpcInfoWithRetry(path, attempts, 2*sleep) + } + return nil, fmt.Errorf("readInfo failed, status: %v, message: %s", response.StatusCode, failed) + } + } + + return nil, fmt.Errorf("readInfo failed, status: %v, message: %s", response.StatusCode, failed) +} + +func (api *API) retryRemoveVpcPeering(path string, attempt, sleep, timeout int) error { + log.Printf("[DEBUG] go-api::vpc_peering::retryRemoveVpcPeering path: %s, "+ + "attempt: %d, sleep: %d, timeout: %d", path, attempt, sleep, timeout) + failed := make(map[string]interface{}) + response, err := api.sling.New().Delete(path).Receive(nil, &failed) + + if err != nil { + return err + } else if attempt*sleep > timeout { + return fmt.Errorf("remove VPC peering failed, reached timeout of %d seconds", timeout) + } + + switch response.StatusCode { + case 204: + return nil + case 400: + switch { + case failed["error_code"] == nil: + break + case failed["error_code"].(float64) == 40001: + log.Printf("[DEBUG] go-api::vpc_peering::remove firewall not finished configuring will retry "+ + "removing VPC peering, attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.retryRemoveVpcPeering(path, attempt, sleep, timeout) + } + } + + return fmt.Errorf("remove VPC peering failed, status: %v, message: %s", response.StatusCode, failed) +} + +func (api *API) waitForPeeringStatus(instanceID int, peeringID string, attempt, sleep, timeout int) (int, error) { + time.Sleep(10 * time.Second) + path := fmt.Sprintf("/api/instances/%v/vpc-peering/status/%v", instanceID, peeringID) + return api.waitForPeeringStatusWithRetry(path, peeringID, attempt, sleep, timeout) +} + +func (api *API) waitForPeeringStatusWithRetry(path, peeringID string, attempt, sleep, timeout int) (int, error) { + log.Printf("[DEBUG] go-api::vpc_peering::waitForPeeringStatusWithRetry path: %s "+ + "attempt: %d, sleep: %d, timeout: %d", path, attempt, sleep, timeout) + data := make(map[string]interface{}) + failed := make(map[string]interface{}) + response, err := api.sling.New().Path(path).Receive(&data, &failed) + + if err != nil { + return attempt, err + } else if attempt*sleep > timeout { + return attempt, fmt.Errorf("accept VPC peering failed, reached timeout of %d seconds", timeout) + } + + switch response.StatusCode { + case 200: + switch data["status"] { + case "active", "pending-acceptance": + return attempt, nil + case "deleted": + return attempt, fmt.Errorf("peering: %s has been deleted", peeringID) + } + case 400: + switch { + case failed["error_code"] == nil: + break + case failed["error_code"].(float64) == 40003: + log.Printf("[DEBUG] go-api::vpc_peering::waitForPeeringStatusWithRetry %s, attempt: %d, until timeout: %d", + failed["message"].(string), attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.waitForPeeringStatusWithRetry(path, peeringID, attempt, sleep, timeout) + } + } + + return attempt, fmt.Errorf("accept VPC peering failed, status: %v, message: %v", response.StatusCode, failed) +} diff --git a/api/vpc_peering_withvpcid.go b/api/vpc_peering_withvpcid.go new file mode 100644 index 00000000..a09dc5ed --- /dev/null +++ b/api/vpc_peering_withvpcid.go @@ -0,0 +1,52 @@ +package api + +// VPC peering for AWS, using vpcID as identifier. + +import ( + "fmt" + "log" + "time" +) + +func (api *API) AcceptVpcPeeringWithVpcId(vpcID, peeringID string, sleep, timeout int) (map[string]interface{}, error) { + attempt, err := api.waitForPeeringStatusWithVpcID(vpcID, peeringID, 1, sleep, timeout) + if err != nil { + return nil, err + } + path := fmt.Sprintf("/api/vpcs/%s/vpc-peering/request/%s", vpcID, peeringID) + return api.retryAcceptVpcPeering(path, attempt, sleep, timeout) +} + +func (api *API) ReadVpcInfoWithVpcId(vpcID string) (map[string]interface{}, error) { + path := fmt.Sprintf("/api/vpcs/%s/vpc-peering/info", vpcID) + // Initiale values, 5 attempts and 20 second sleep + return api.readVpcInfoWithRetry(path, 5, 20) +} + +func (api *API) ReadVpcPeeringRequestWithVpcId(vpcID, peeringID string) (map[string]interface{}, error) { + data := make(map[string]interface{}) + failed := make(map[string]interface{}) + log.Printf("[DEBUG] go-api::vpc_peering_withvpcid::request vpc id: %v, peering id: %v", vpcID, peeringID) + path := fmt.Sprintf("/api/vpcs/%s/vpc-peering/request/%s", vpcID, peeringID) + response, err := api.sling.New().Get(path).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::vpc_peering_withvpcid::request data: %v", data) + + if err != nil { + return nil, err + } else if response.StatusCode != 200 { + return nil, fmt.Errorf("ReadRequest failed, status: %v, message: %s", response.StatusCode, failed) + } + + return data, nil +} + +func (api *API) RemoveVpcPeeringWithVpcId(vpcID, peeringID string, sleep, timeout int) error { + path := fmt.Sprintf("/api/vpcs/%s/vpc-peering/%s", vpcID, peeringID) + return api.retryRemoveVpcPeering(path, 1, sleep, timeout) +} + +func (api *API) waitForPeeringStatusWithVpcID(vpcID, peeringID string, attempt, sleep, timeout int) (int, error) { + time.Sleep(10 * time.Second) + path := fmt.Sprintf("/api/vpcs/%s/vpc-peering/status/%s", vpcID, peeringID) + return api.waitForPeeringStatusWithRetry(path, peeringID, attempt, sleep, timeout) +} diff --git a/api/webhook.go b/api/webhook.go new file mode 100644 index 00000000..70a4de5b --- /dev/null +++ b/api/webhook.go @@ -0,0 +1,213 @@ +package api + +import ( + "fmt" + "log" + "strconv" + "strings" + "time" +) + +// CreateWebhook - create a webhook for a vhost and a specific qeueu +func (api *API) CreateWebhook(instanceID int, params map[string]interface{}, + sleep, timeout int) (map[string]interface{}, error) { + + return api.createWebhookWithRetry(instanceID, params, 1, sleep, timeout) +} + +// createWebhookWithRetry: create webhook with retry if backend is busy. +func (api *API) createWebhookWithRetry(instanceID int, params map[string]interface{}, + attempt, sleep, timeout int) (map[string]interface{}, error) { + + var ( + data = make(map[string]interface{}) + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/webhooks", instanceID) + ) + + log.Printf("[DEBUG] go-api::webhook#create path: %s, params: %v", path, params) + response, err := api.sling.New().Post(path).BodyJSON(params).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::webhook#create response data: %v", data) + + if err != nil { + return nil, err + } else if attempt*sleep > timeout { + return nil, fmt.Errorf("create webhook reached timeout of %d seconds", timeout) + } + + switch response.StatusCode { + case 201: + if v, ok := data["id"]; ok { + data["id"] = strconv.FormatFloat(v.(float64), 'f', 0, 64) + } else { + msg := fmt.Sprintf("go-api::webhook#create Invalid webhook identifier: %v", data["id"]) + log.Printf("[ERROR] %s", msg) + return nil, fmt.Errorf(msg) + } + return data, nil + case 400: + if strings.Compare(failed["error"].(string), "Timeout talking to backend") == 0 { + log.Printf("[INFO] go-api::webhook#create Timeout talking to backend "+ + "attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.createWebhookWithRetry(instanceID, params, attempt, sleep, timeout) + } + return nil, fmt.Errorf("create webhook failed, status: %v, message: %s", 400, failed) + default: + return nil, + fmt.Errorf("create webhook with retry failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// ReadWebhook - retrieves a specific webhook for an instance +func (api *API) ReadWebhook(instanceID int, webhookID string, sleep, timeout int) ( + map[string]interface{}, error) { + + path := fmt.Sprintf("/api/instances/%d/webhooks/%s", instanceID, webhookID) + return api.readWebhookWithRetry(path, 1, sleep, timeout) +} + +// readWebhookWithRetry: read webhook with retry if backend is busy. +func (api *API) readWebhookWithRetry(path string, attempt, sleep, timeout int) ( + map[string]interface{}, error) { + + var ( + data map[string]interface{} + failed map[string]interface{} + ) + + log.Printf("[DEBUG] go-api::webhook#read path: %s", path) + response, err := api.sling.New().Get(path).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::webhook#read response data: %v", data) + + if err != nil { + return nil, err + } else if attempt*sleep > timeout { + return nil, fmt.Errorf("read webhook reached timeout of %d seconds", timeout) + } + + switch response.StatusCode { + case 200: + return data, nil + case 400: + if strings.Compare(failed["error"].(string), "Timeout talking to backend") == 0 { + log.Printf("[INFO] go-api::webhook#read Timeout talking to backend "+ + "attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.readWebhookWithRetry(path, attempt, sleep, timeout) + } + return nil, fmt.Errorf("read webhook failed, status: %v, message: %s", 400, failed) + default: + return nil, fmt.Errorf("read webhook with retry failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// ListWebhooks - list all webhooks for an instance. +func (api *API) ListWebhooks(instanceID int) (map[string]interface{}, error) { + var ( + data map[string]interface{} + failed map[string]interface{} + path = fmt.Sprintf("/api/instances/%d/webhooks", instanceID) + ) + + log.Printf("[DEBUG] go-api::webhook#list path: %s", path) + response, err := api.sling.New().Path(path).Receive(&data, &failed) + + if err != nil { + return nil, err + } + if response.StatusCode != 200 { + return nil, fmt.Errorf("list webhooks failed, status: %v, message: %s", + response.StatusCode, failed) + } + + return data, err +} + +// UpdateWebhook - updates a specific webhook for an instance +func (api *API) UpdateWebhook(instanceID int, webhookID string, params map[string]interface{}, + sleep, timeout int) error { + + path := fmt.Sprintf("/api/instances/%d/webhooks/%s", instanceID, webhookID) + return api.updateWebhookWithRetry(path, params, 1, sleep, timeout) +} + +// updateWebhookWithRetry: update webhook with retry if backend is busy. +func (api *API) updateWebhookWithRetry(path string, params map[string]interface{}, + attempt, sleep, timeout int) error { + + var ( + data = make(map[string]interface{}) + failed map[string]interface{} + ) + + log.Printf("[DEBUG] go-api::webhook#update path: %s, params: %v", path, params) + response, err := api.sling.New().Put(path).BodyJSON(params).Receive(&data, &failed) + log.Printf("[DEBUG] go-api::webhook#update response data: %v", data) + + if err != nil { + return err + } else if attempt*sleep > timeout { + return fmt.Errorf("update webhook reached timeout of %d seconds", timeout) + } + + switch response.StatusCode { + case 201: + return nil + case 400: + if strings.Compare(failed["error"].(string), "Timeout talking to backend") == 0 { + log.Printf("[INFO] go-api::webhook#update Timeout talking to backend "+ + "attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.updateWebhookWithRetry(path, params, attempt, sleep, timeout) + } + return fmt.Errorf("update webhook failed, status: %v, message: %s", 400, failed) + default: + return fmt.Errorf("update webhook with retry failed, status: %v, message: %s", + response.StatusCode, failed) + } +} + +// DeleteWebhook - removes a specific webhook for an instance +func (api *API) DeleteWebhook(instanceID int, webhookID string, sleep, timeout int) error { + path := fmt.Sprintf("/api/instances/%d/webhooks/%s", instanceID, webhookID) + return api.deleteWebhookWithRetry(path, 1, sleep, timeout) +} + +// deleteWebhookWithRetry: delete webhook with retry if backend is busy. +func (api *API) deleteWebhookWithRetry(path string, attempt, sleep, timeout int) error { + var ( + failed map[string]interface{} + ) + + log.Printf("[DEBUG] go-api::webhook#delete path: %s", path) + response, err := api.sling.New().Delete(path).Receive(nil, &failed) + + if err != nil { + return err + } else if attempt*sleep > timeout { + return fmt.Errorf("delete webhook reached timeout of %d seconds", timeout) + } + + switch response.StatusCode { + case 204: + return nil + case 400: + if strings.Compare(failed["error"].(string), "Timeout talking to backend") == 0 { + log.Printf("[INFO] go-api::webhook#delete Timeout talking to backend "+ + "attempt: %d, until timeout: %d", attempt, (timeout - (attempt * sleep))) + attempt++ + time.Sleep(time.Duration(sleep) * time.Second) + return api.deleteWebhookWithRetry(path, attempt, sleep, timeout) + } + return fmt.Errorf("delete webhook failed, status: %v, message: %s", 400, failed) + default: + return fmt.Errorf("delete webhook with retry failed, status: %v, message: %s", + response.StatusCode, failed) + } +}