diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b421875..9accf7f3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -82,7 +82,7 @@ jobs: run: | go test -count=1 -parallel=4 -timeout 10m -json -v ./... 2>&1 | tee TestResults-${{ matrix.os }}_${{ matrix.go-version }}.log - name: Upload test log - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: TestResults-${{ matrix.os }}_${{ matrix.go-version }}.log diff --git a/docs/resources/dw_aws_cluster.md b/docs/resources/dw_aws_cluster.md index 37dec949..719740f5 100644 --- a/docs/resources/dw_aws_cluster.md +++ b/docs/resources/dw_aws_cluster.md @@ -79,6 +79,7 @@ output "name" { - `database_backup_retention_days` (Number) The number of days to retain database backups. - `instance_settings` (Attributes) (see [below for nested schema](#nestedatt--instance_settings)) - `node_role_cdw_managed_policy_arn` (String) The managed policy ARN to be attached to the created node instance role. +- `polling_options` (Attributes) Polling related configuration options that could specify various values that will be used during CDP resource creation. (see [below for nested schema](#nestedatt--polling_options)) ### Read-Only @@ -86,6 +87,7 @@ output "name" { - `id` (String) The ID of this resource. - `last_updated` (String) Timestamp of the last Terraform update of the order. - `name` (String) The name of the cluster matches the environment name. +- `status` (String) The status of the cluster. ### Nested Schema for `network_settings` @@ -124,3 +126,13 @@ Optional: - `enable_spot_instances` (Boolean) Whether to use spot instances for worker nodes. + +### Nested Schema for `polling_options` + +Optional: + +- `async` (Boolean) Boolean value that specifies if Terraform should wait for resource creation/deletion. +- `call_failure_threshold` (Number) Threshold value that specifies how many times should a single call failure happen before giving up the polling. +- `polling_timeout` (Number) Timeout value in minutes that specifies for how long should the polling go for resource creation/deletion. + + diff --git a/docs/resources/environments_user_sync.md b/docs/resources/environments_user_sync.md index 367b5077..a6e7540e 100644 --- a/docs/resources/environments_user_sync.md +++ b/docs/resources/environments_user_sync.md @@ -33,6 +33,9 @@ This approach allows a fine-grain control of the sync operation. resource "cdp_environments_user_sync" "example-user_sync" { environment_names = ["example-cdp-environment-1", "example-cdp-environment-2"] + polling_options = { + async = true + } } ``` @@ -42,7 +45,17 @@ resource "cdp_environments_user_sync" "example-user_sync" { ### Optional - `environment_names` (Set of String) List of environments to be synced. If not present, all environments will be synced. +- `polling_options` (Attributes) Polling related configuration options that could specify various values that will be used during CDP resource creation. (see [below for nested schema](#nestedatt--polling_options)) ### Read-Only -- `id` (String) The ID of this resource. \ No newline at end of file +- `id` (String) The ID of this resource. + + +### Nested Schema for `polling_options` + +Optional: + +- `async` (Boolean) Boolean value that specifies if Terraform should wait for resource creation/deletion. +- `call_failure_threshold` (Number) Threshold value that specifies how many times should a single call failure happen before giving up the polling. +- `polling_timeout` (Number) Timeout value in minutes that specifies for how long should the polling go for resource creation/deletion. \ No newline at end of file diff --git a/examples/resources/cdp_environments_user_sync/resource.tf b/examples/resources/cdp_environments_user_sync/resource.tf index 54b749b6..795f184f 100644 --- a/examples/resources/cdp_environments_user_sync/resource.tf +++ b/examples/resources/cdp_environments_user_sync/resource.tf @@ -10,4 +10,7 @@ resource "cdp_environments_user_sync" "example-user_sync" { environment_names = ["example-cdp-environment-1", "example-cdp-environment-2"] + polling_options = { + async = true + } } diff --git a/resources/environments/resource_user_sync.go b/resources/environments/resource_user_sync.go index 30866535..db5538cc 100644 --- a/resources/environments/resource_user_sync.go +++ b/resources/environments/resource_user_sync.go @@ -12,8 +12,11 @@ package environments import ( "context" + "fmt" + "time" "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/cdp" + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/environments/client" "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/environments/client/operations" environmentsmodels "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/environments/models" "github.com/cloudera/terraform-provider-cdp/utils" @@ -21,10 +24,14 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" ) var ( @@ -45,6 +52,33 @@ var userSyncSchema = schema.Schema{ ElementType: types.StringType, Optional: true, }, + "polling_options": schema.SingleNestedAttribute{ + MarkdownDescription: "Polling related configuration options that could specify various values that will be used during CDP resource creation.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "async": schema.BoolAttribute{ + MarkdownDescription: "Boolean value that specifies if Terraform should wait for resource creation/deletion.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + "polling_timeout": schema.Int64Attribute{ + MarkdownDescription: "Timeout value in minutes that specifies for how long should the polling go for resource creation/deletion.", + Default: int64default.StaticInt64(90), + Computed: true, + Optional: true, + }, + "call_failure_threshold": schema.Int64Attribute{ + MarkdownDescription: "Threshold value that specifies how many times should a single call failure happen before giving up the polling.", + Default: int64default.StaticInt64(3), + Computed: true, + Optional: true, + }, + }, + }, }, } @@ -61,9 +95,9 @@ func (r *userSyncResource) Metadata(ctx context.Context, req resource.MetadataRe } type userSyncResourceModel struct { - ID types.String `tfsdk:"id"` - - EnvironmentNames types.Set `tfsdk:"environment_names"` + ID types.String `tfsdk:"id"` + EnvironmentNames types.Set `tfsdk:"environment_names"` + PollingOptions *utils.PollingOptions `tfsdk:"polling_options"` } func toSyncAllUsersRequest(ctx context.Context, model *userSyncResourceModel, diag *diag.Diagnostics) *environmentsmodels.SyncAllUsersRequest { @@ -93,7 +127,7 @@ func (r *userSyncResource) Create(ctx context.Context, req resource.CreateReques params := operations.NewSyncAllUsersParamsWithContext(ctx) params.WithInput(toSyncAllUsersRequest(ctx, &state, &resp.Diagnostics)) - _, err := client.Operations.SyncAllUsers(params) + res, err := client.Operations.SyncAllUsers(params) if err != nil { if isSyncAllUsersNotFoundError(err) { resp.Diagnostics.AddError( @@ -113,6 +147,74 @@ func (r *userSyncResource) Create(ctx context.Context, req resource.CreateReques if resp.Diagnostics.HasError() { return } + + opID := res.Payload.OperationID + tflog.Debug(ctx, fmt.Sprintf("User sync operation ID: %s", *opID)) + if !(state.PollingOptions != nil && state.PollingOptions.Async.ValueBool()) { + tflog.Debug(ctx, "User sync polling starts") + err = waitForUserSync(*opID, time.Hour*1, callFailureThreshold, r.client.Environments, ctx, state.PollingOptions) + if err != nil { + return + } + } +} + +func waitForUserSync(opID string, fallbackTimeout time.Duration, callFailureThresholdDefault int, client *client.Environments, ctx context.Context, pollingOptions *utils.PollingOptions) error { + timeout, err := utils.CalculateTimeoutOrDefault(ctx, pollingOptions, fallbackTimeout) + if err != nil { + return err + } + callFailureThreshold, failureThresholdError := utils.CalculateCallFailureThresholdOrDefault(ctx, pollingOptions, callFailureThresholdDefault) + if failureThresholdError != nil { + return failureThresholdError + } + callFailedCount := 0 + stateConf := &retry.StateChangeConf{ + Pending: []string{"NEVER_RUN", + "REQUESTED", + "REJECTED", + "RUNNING", + "COMPLETED", + "FAILED", + "TIMEDOUT"}, + Target: []string{"COMPLETED"}, + Delay: 5 * time.Second, + Timeout: *timeout, + PollInterval: 10 * time.Second, + Refresh: func() (interface{}, string, error) { + tflog.Debug(ctx, fmt.Sprintf("About to get sync status for operationID %s", opID)) + params := operations.NewSyncStatusParamsWithContext(ctx) + params.WithInput(&environmentsmodels.SyncStatusRequest{OperationID: &opID}) + resp, err := client.Operations.SyncStatus(params) + if err != nil { + if isEnvNotFoundError(err) { + tflog.Debug(ctx, fmt.Sprintf("Recoverable error getting user sync status: %s", err)) + callFailedCount = 0 + return nil, "", nil + } + callFailedCount++ + if callFailedCount <= callFailureThreshold { + tflog.Warn(ctx, fmt.Sprintf("Error getting user sync status with call failure due to [%s] but threshold limit is not reached yet (%d out of %d).", err.Error(), callFailedCount, callFailureThreshold)) + return nil, "", nil + } + tflog.Error(ctx, fmt.Sprintf("Error getting user sync status (due to: %s) and call failure threshold limit exceeded.", err)) + return nil, "", err + } + callFailedCount = 0 + tflog.Info(ctx, fmt.Sprintf("User sync status: %s", resp.GetPayload().Status)) + return checkUserSyncResponseStatusForError(resp) + }, + } + _, err = stateConf.WaitForStateContext(ctx) + + return err +} + +func checkUserSyncResponseStatusForError(resp *operations.SyncStatusOK) (interface{}, string, error) { + if utils.ContainsAsSubstring([]string{"FAILED", "ERROR"}, string(resp.GetPayload().Status)) { + return nil, "", fmt.Errorf("unexpected user sync status status: %s. ", resp.GetPayload().Status) + } + return resp, string(resp.GetPayload().Status), nil } func isSyncAllUsersNotFoundError(err error) bool {