diff --git a/hcx/activation.go b/hcx/activation.go index d2e41eb..b77153d 100644 --- a/hcx/activation.go +++ b/hcx/activation.go @@ -100,7 +100,6 @@ func GetActivate(c *Client) (ActivateBody, error) { // DeleteActivate sends a request to remove the activation configuration using the provided body and returns the // resulting ActivateBody object. Returns an error if the request fails or the response cannot be parsed. - func DeleteActivate(c *Client, body ActivateBody) (ActivateBody, error) { resp := ActivateBody{} diff --git a/hcx/constants/constants.go b/hcx/constants/constants.go index b6ffc53..a451634 100644 --- a/hcx/constants/constants.go +++ b/hcx/constants/constants.go @@ -24,6 +24,7 @@ const ( // VMC VmcMaxRetries = 12 VmcRetryInterval = 10 * time.Second + VmcMaxRetryInterval = 5 * time.Minute VmcActivationActiveStatus = "ACTIVE" VmcActivationFailedStatus = "ACTIVATION_FAILED" VmcDeactivationInactiveStatus = "DE-ACTIVATED" diff --git a/hcx/resource_vmc.go b/hcx/resource_vmc.go index eb6c203..8641cee 100644 --- a/hcx/resource_vmc.go +++ b/hcx/resource_vmc.go @@ -6,7 +6,7 @@ package hcx import ( "context" - "log" + "math/rand" "time" "github.com/vmware/terraform-provider-hcx/hcx/constants" @@ -58,157 +58,198 @@ func resourceVmc() *schema.Resource { // resourceVmcCreate creates the VMware Cloud on AWS configuration. func resourceVmcCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - client := m.(*Client) - token := client.Token - sddcName := d.Get("sddc_name").(string) - sddcID := d.Get("sddc_id").(string) - - // Authenticate with VMware Cloud Services - accessToken, err := VmcAuthenticate(token) - if err != nil { - return diag.FromErr(err) - } - - err = CloudAuthenticate(client, accessToken) - if err != nil { - return diag.FromErr(err) + // Authenticate and fetch the SDDC details. + sddc, diags := authenticateAndFetchSDDC(ctx, d, client) + if diags != nil { + return diags } - var sddc SDDC - if sddcID != "" { - sddc, err = GetSddcByID(client, sddcID) - } else { - sddc, err = GetSddcByName(client, sddcName) - } - - if err != nil { - return diag.FromErr(err) - } - - // Check if already activated. + // Validate if the SDDC is already activated. if sddc.DeploymentStatus == constants.VmcActivationActiveStatus { - return diag.Errorf("Already activated") + return diag.Errorf("sddc is already activated") } - // Activate HCX. - _, err = ActivateHcxOnSDDC(client, sddc.ID) + // Trigger the activation of HCX on the specified SDDC. + _, err := ActivateHcxOnSDDC(client, sddc.ID) if err != nil { - return diag.FromErr(err) + return diag.Errorf("failed to activate hcx on the sddc: %s", err.Error()) } - // Wait for task to be completed. - errcount := 0 - for { - if sddcID != "" { - sddc, err = GetSddcByID(client, sddcID) - } else { - sddc, err = GetSddcByName(client, sddcName) - } - if err != nil { - // Attempt to bypass recurring situation where the HCX API - // returns status 502 with a proxy server error, and an HTML response - // instead of JSON. - errcount++ - hclog.Default().Info("[INFO] - resourceVmcCreate() - Error retrieving SDDC status: ", "error", err.Error(), "Errcount:", errcount) - if errcount > 12 { + // Poll the activation status until the process completes successfully or fails. + for retries := 0; retries < constants.VmcMaxRetries; retries++ { + // Fetch the updated SDDC details. + sddc, diags = authenticateAndFetchSDDC(ctx, d, client) + if diags != nil { + hclog.Default().Info("[INFO] - resourceVmcCreate() - error retrieving SDDC status; retrying...", + "sddc_id", sddc.ID, "sddc_name", sddc.Name, "error", diags[0].Summary, "RetryCount", retries) + + // Exit if the retry limit is exceeded. + if retries == constants.VmcMaxRetries-1 { + return diags + } + + // Backoff before retrying. + waitTime := calculateBackoff(retries) + if err := waitWithContext(ctx, waitTime); err != nil { // Respect context cancellations. return diag.FromErr(err) } + continue } - if sddc.DeploymentStatus == constants.VmcActivationActiveStatus { - break - } + // Check the current deployment status of the SDDC. + switch sddc.DeploymentStatus { + case constants.VmcActivationActiveStatus: + // Activation successful. Refresh the resource state by calling "read". + return resourceVmcRead(ctx, d, m) + + case constants.VmcActivationFailedStatus: + // Explicit activation failure. Return an appropriate error message. + return diag.Errorf("activation failed with status: %s", constants.VmcActivationFailedStatus) - if sddc.DeploymentStatus == constants.VmcActivationFailedStatus { - return diag.Errorf("Activation failed") + default: + hclog.Default().Warn("unknown SDDC status during polling", "status", sddc.DeploymentStatus) } - time.Sleep(constants.VmcRetryInterval) + // Wait before the next polling attempt. + if err := waitWithContext(ctx, constants.VmcRetryInterval); err != nil { + return diag.FromErr(err) + } } - return resourceVmcRead(ctx, d, m) + // If retry limit is reached without resolution, return an error. + return diag.Errorf("maximum retries reached while activating the SDDC") } // resourceVmcRead retrieves the VMware Cloud on AWS configuration. func resourceVmcRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - var diags diag.Diagnostics - client := m.(*Client) - token := client.Token - sddcName := d.Get("sddc_name").(string) - sddcID := d.Get("sddc_id").(string) - - log.Printf("******************************************************************\n") - log.Printf("token: %s, sddc_name: %s, sddc: %s \n", token, sddcName, sddcID) - log.Printf("******************************************************************\n") - - if sddcName == "" && sddcID == "" { - return diag.Errorf("SDDC name or Id must be specified") + // Authenticate and fetch the SDDC details. + sddc, diags := authenticateAndFetchSDDC(ctx, d, client) + if diags != nil { + return diags } - // Authenticate with VMware Cloud Services - accessToken, err := VmcAuthenticate(token) - if err != nil { - return diag.FromErr(err) + // Set resource data fields with retrieved SDDC details. + if err := d.Set("cloud_url", sddc.CloudURL); err != nil { + return diag.Errorf("failed to set 'cloud_url': %s", err.Error()) } - - err = CloudAuthenticate(client, accessToken) - if err != nil { - return diag.FromErr(err) - } - - log.Printf("****************") - log.Printf("[Client inside]: %+v ", client) - log.Printf("****************") - - var sddc SDDC - if sddcID != "" { - sddc, err = GetSddcByID(client, sddcID) - } else { - sddc, err = GetSddcByName(client, sddcName) + if err := d.Set("cloud_name", sddc.CloudName); err != nil { + return diag.Errorf("failed to set 'cloud_name': %s", err.Error()) } - if err != nil { - return diag.FromErr(err) + if err := d.Set("cloud_type", sddc.CloudType); err != nil { + return diag.Errorf("failed to set 'cloud_type': %s", err.Error()) } - d.SetId(sddc.ID) - d.Set("cloud_url", sddc.CloudURL) - d.Set("cloud_name", sddc.CloudName) - d.Set("cloud_type", sddc.CloudType) - return diags + // Return diagnostics response. + return nil } // resourceVmcUpdate updates the VMware Cloud on AWS resource configuration. func resourceVmcUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + // Validate immutable fields to prevent unsupported updates. + if d.HasChange("sddc_id") || d.HasChange("sddc_name") { + return diag.Errorf("opdating 'sddc_id' or 'sddc_name' is not supported") + } + // Return the current state using Read. return resourceVmcRead(ctx, d, m) } // resourceVmcDelete removes the VMware Cloud on AWS configuration and clears the state of the resource in the schema. func resourceVmcDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - var diags diag.Diagnostics - client := m.(*Client) + // Authenticate and fetch the SDDC details. + sddc, diags := authenticateAndFetchSDDC(ctx, d, client) + if diags != nil { + return diags + } + + // Trigger the deactivation of HCX on the specified SDDC. + _, err := DeactivateHcxOnSDDC(client, sddc.ID) + if err != nil { + return diag.Errorf("failed to deactivate hcx on the sddc: %s", err.Error()) + } + + // Poll the deactivation status until the process completes successfully or fails. + for retries := 0; retries < constants.VmcMaxRetries; retries++ { + // Fetch the updated status of the SDDC. + sddc, diags = authenticateAndFetchSDDC(ctx, d, client) + if diags != nil { + hclog.Default().Info("[INFO] - resourceVmcDelete() - error retrieving SDDC status; retrying...", + "sddc_id", sddc.ID, "sddc_name", sddc.Name, "error", diags[0].Summary, "RetryCount", retries) + + // Exit if the retry limit is exceeded. + if retries == constants.VmcMaxRetries-1 { + return diags + } + + // Backoff before retrying. + waitTime := calculateBackoff(retries) + if err := waitWithContext(ctx, waitTime); err != nil { + return diag.FromErr(err) + } + continue + } + + // Evaluate the current deployment status of the SDDC. + switch sddc.DeploymentStatus { + case constants.VmcDeactivationInactiveStatus: + // Deactivation successful. Exit the deletion process. + return nil + + case constants.VmcDeactivationFailedStatus: + // Explicit deactivation failure. Return an appropriate error message. + return diag.Errorf("deactivation failed with status: %s", constants.VmcDeactivationFailedStatus) + + case "": + // SDDC resource already missing or deleted. + hclog.Default().Info("[INFO] - resourceVmcDelete() - SDDC already missing or deleted.") + return nil + + default: + hclog.Default().Warn("unknown SDDC status during polling", "status", sddc.DeploymentStatus) + } + + // Wait before polling again. + if err := waitWithContext(ctx, constants.VmcRetryInterval); err != nil { + return diag.FromErr(err) + } + } + + // If retry limit is reached without resolution, return an error. + return diag.Errorf("maximum retries reached while deleting the SDDC") +} + +// authenticateAndFetchSDDC authenticates with VMware Cloud and HCX APIs, and retrieves SDDC details by name or ID. +func authenticateAndFetchSDDC(ctx context.Context, d *schema.ResourceData, client *Client) (SDDC, diag.Diagnostics) { + // Extract input parameters. token := client.Token sddcName := d.Get("sddc_name").(string) sddcID := d.Get("sddc_id").(string) - // Authenticate with VMware Cloud Services + // Validate inputs. + if sddcName == "" && sddcID == "" { + return SDDC{}, diag.Errorf("neither 'sddc_name' nor 'sddc_id' was specified") + } + + // Authenticate with VMware Cloud Services. accessToken, err := VmcAuthenticate(token) if err != nil { - return diag.FromErr(err) + return SDDC{}, diag.Errorf("failed to authenticate with VMware Cloud Services: %s", err.Error()) } + // Authenticate the HCX API. err = CloudAuthenticate(client, accessToken) if err != nil { - return diag.FromErr(err) + return SDDC{}, diag.Errorf("failed to authenticate hcx api: %s", err.Error()) } + // Retrieve the SDDC details. var sddc SDDC if sddcID != "" { sddc, err = GetSddcByID(client, sddcID) @@ -216,45 +257,29 @@ func resourceVmcDelete(ctx context.Context, d *schema.ResourceData, m interface{ sddc, err = GetSddcByName(client, sddcName) } if err != nil { - return diag.FromErr(err) - } - - // Deactivate HCX - _, err = DeactivateHcxOnSDDC(client, sddc.ID) - if err != nil { - return diag.FromErr(err) + return SDDC{}, diag.Errorf("failed to retrieve sddc details: %s", err.Error()) } - // Wait for task to be completed - errcount := 0 - for { - var sddc SDDC - if sddcID != "" { - sddc, err = GetSddcByID(client, sddcID) - } else { - sddc, err = GetSddcByName(client, sddcName) - } - if err != nil { - // Attempt to bypass recurring situation where the HCX API - // returns status 502 with a proxy server error, and an HTML response - // instead of JSON. - errcount++ - hclog.Default().Info("[INFO] - resourceVmcDelete() - Error retrieving SDDC status: ", "error", err.Error(), "Errcount:", errcount) - if errcount > constants.VmcMaxRetries { - return diag.FromErr(err) - } - } - - if sddc.DeploymentStatus == constants.VmcDeactivationInactiveStatus { - break - } - - if sddc.DeploymentStatus == constants.VmcDeactivationFailedStatus { - return diag.Errorf("Deactivation failed") - } + return sddc, nil +} - time.Sleep(constants.VmcRetryInterval) +// calculateBackoff computes the exponential backoff duration with capped interval and jitter. +func calculateBackoff(retries int) time.Duration { + base := constants.VmcRetryInterval * time.Duration(1< constants.VmcMaxRetryInterval { + base = constants.VmcMaxRetryInterval } + // Add jitter to reduce the chance of simultaneous retries across instances. + jitter := time.Duration(rand.Int63n(int64(base / 2))) + return base + jitter +} - return diags +// waitWithContext waits for the backoff duration or respects context cancellations. +func waitWithContext(ctx context.Context, backoff time.Duration) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(backoff): + return nil + } }