diff --git a/resources/environments/environment_action_util.go b/resources/environments/environment_action_util.go index f261e399..cc8c8388 100644 --- a/resources/environments/environment_action_util.go +++ b/resources/environments/environment_action_util.go @@ -20,6 +20,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/cdp" + environmentsclient "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" @@ -106,3 +107,31 @@ func waitForCreateEnvironmentWithDiagnosticHandle(ctx context.Context, client *c } return descEnvResp.GetPayload().Environment, nil } + +func waitForStartEnvironmentWithDiagnosticHandle(ctx context.Context, client *environmentsclient.Environments, id string, envName string, resp *resource.UpdateResponse, options *utils.PollingOptions, + stateSaverCb func(*environmentsmodels.Environment)) (*environmentsmodels.Environment, error) { + if err := waitForEnvironmentToBeAvailable(id, timeoutOneHour, callFailureThreshold, client, ctx, options, stateSaverCb); err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "starting Environment failed") + return nil, err + } + + environmentName := envName + descParams := operations.NewDescribeEnvironmentParamsWithContext(ctx) + descParams.WithInput(&environmentsmodels.DescribeEnvironmentRequest{ + EnvironmentName: &environmentName, + }) + descEnvResp, err := client.Operations.DescribeEnvironment(descParams) + if err != nil { + if isEnvNotFoundError(err) { + resp.Diagnostics.AddWarning("Resource not found on provider", "Environment not found, removing from state.") + tflog.Warn(ctx, "Environment not found, removing from state", map[string]interface{}{ + "id": id, + }) + resp.State.RemoveResource(ctx) + return nil, err + } + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "starting Environment failed") + return nil, err + } + return descEnvResp.GetPayload().Environment, nil +} diff --git a/resources/environments/polling.go b/resources/environments/polling.go index 4caf89a3..86e7aeb1 100644 --- a/resources/environments/polling.go +++ b/resources/environments/polling.go @@ -113,7 +113,12 @@ func waitForEnvironmentToBeAvailable(environmentName string, fallbackTimeout tim "ENVIRONMENT_RESOURCE_ENCRYPTION_INITIALIZATION_IN_PROGRESS", "ENVIRONMENT_VALIDATION_IN_PROGRESS", "ENVIRONMENT_INITIALIZATION_IN_PROGRESS", - "FREEIPA_CREATION_IN_PROGRESS"}, + "FREEIPA_CREATION_IN_PROGRESS", + "START_DATALAKE_STARTED", + "START_DATAHUB_STARTED", + "START_SYNCHRONIZE_USERS_STARTED", + "START_FREEIPA_STARTED", + "ENV_STOPPED"}, Target: []string{"AVAILABLE"}, Delay: 5 * time.Second, Timeout: *timeout, @@ -151,6 +156,56 @@ func waitForEnvironmentToBeAvailable(environmentName string, fallbackTimeout tim return err } +func waitForEnvironmentToBeStopped(environmentName 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{ + "STOP_DATAHUB_STARTED", + "STOP_DATALAKE_STARTED", + "STOP_FREEIPA_STARTED", + "VERTICAL_SCALE_ON_FREEIPA_IN_PROGRESS", + }, + Target: []string{"ENV_STOPPED"}, + Delay: 5 * time.Second, + Timeout: *timeout, + PollInterval: 10 * time.Second, + Refresh: func() (interface{}, string, error) { + tflog.Debug(ctx, fmt.Sprintf("About to describe environment %s", environmentName)) + params := operations.NewDescribeEnvironmentParamsWithContext(ctx) + params.WithInput(&environmentsmodels.DescribeEnvironmentRequest{EnvironmentName: &environmentName}) + resp, err := client.Operations.DescribeEnvironment(params) + if err != nil { + if isEnvNotFoundError(err) { + tflog.Debug(ctx, fmt.Sprintf("Recoverable error describing environment: %s", err)) + callFailedCount = 0 + return nil, "", nil + } + callFailedCount++ + if callFailedCount <= callFailureThreshold { + tflog.Warn(ctx, fmt.Sprintf("Error describing environment 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 describing environment (due to: %s) and call failure threshold limit exceeded.", err)) + return nil, "", err + } + callFailedCount = 0 + tflog.Info(ctx, fmt.Sprintf("Described environment's status: %s", *resp.GetPayload().Environment.Status)) + return checkResponseStatusForError(resp) + }, + } + _, err = stateConf.WaitForStateContext(ctx) + + return err +} + func checkResponseStatusForError(resp *operations.DescribeEnvironmentOK) (interface{}, string, error) { if utils.ContainsAsSubstring([]string{"FAILED", "ERROR"}, *resp.GetPayload().Environment.Status) { return nil, "", fmt.Errorf("unexpected Enviornment status: %s. Reason: %s", *resp.GetPayload().Environment.Status, resp.GetPayload().Environment.StatusReason) diff --git a/resources/environments/resource_aws_environment.go b/resources/environments/resource_aws_environment.go index 33e1773b..516e1194 100644 --- a/resources/environments/resource_aws_environment.go +++ b/resources/environments/resource_aws_environment.go @@ -21,6 +21,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/cdp" + environmentsclient "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" @@ -137,7 +138,7 @@ func (r *awsEnvironmentResource) Update(ctx context.Context, req resource.Update var plan awsEnvironmentResourceModel var state awsEnvironmentResourceModel planDiags := req.Plan.Get(ctx, &plan) - stateDiags := req.State.Get(ctx, &state) + var stateDiags = req.State.Get(ctx, &state) resp.Diagnostics.Append(planDiags...) resp.Diagnostics.Append(stateDiags...) if resp.Diagnostics.HasError() { @@ -145,97 +146,12 @@ func (r *awsEnvironmentResource) Update(ctx context.Context, req resource.Update return } - if plan.CredentialName.ValueString() != state.CredentialName.ValueString() { - params := operations.NewChangeEnvironmentCredentialParamsWithContext(ctx) - params.WithInput(&environmentsmodels.ChangeEnvironmentCredentialRequest{ - CredentialName: plan.CredentialName.ValueStringPointer(), - EnvironmentName: state.EnvironmentName.ValueStringPointer(), - }) - _, err := r.client.Environments.Operations.ChangeEnvironmentCredential(params) - if err != nil { - utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "change AWS Environment credential") - return - } - stateDiags = resp.State.Set(ctx, state) - resp.Diagnostics.Append(stateDiags...) - if resp.Diagnostics.HasError() { - return - } - } + updateEnvironment(ctx, &plan, &state, r.client.Environments, resp) + updateFreeIpa(ctx, &plan, &state, r.client.Environments, resp) - if state.EncryptionKeyArn != plan.EncryptionKeyArn { - if err := updateAwsDiskEncryptionParameters(ctx, r.client.Environments, plan); err != nil { - utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "update disk encryption parameters") - return - } - state.EncryptionKeyArn = plan.EncryptionKeyArn - stateDiags = resp.State.Set(ctx, state) - resp.Diagnostics.Append(stateDiags...) - if resp.Diagnostics.HasError() { - return - } - } - if plan.Authentication != nil && !reflect.DeepEqual(plan.Authentication, state.Authentication) { - if err := updateSshKey(ctx, r.client.Environments, plan.Authentication, plan.EnvironmentName.ValueStringPointer()); err != nil { - utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "update SSH key") - return - } - state.Authentication = plan.Authentication - stateDiags = resp.State.Set(ctx, state) - resp.Diagnostics.Append(stateDiags...) - if resp.Diagnostics.HasError() { - return - } - } - if !reflect.DeepEqual(utils.FromSetValueToStringList(plan.SubnetIds), utils.FromSetValueToStringList(state.SubnetIds)) || - !reflect.DeepEqual(plan.EndpointAccessGatewaySubnetIds, state.EndpointAccessGatewaySubnetIds) { - if err := updateSubnet(ctx, r.client.Environments, plan); err != nil { - utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "update subnet") - return - } - state.SubnetIds = plan.SubnetIds - state.EndpointAccessGatewaySubnetIds = plan.EndpointAccessGatewaySubnetIds - stateDiags = resp.State.Set(ctx, state) - resp.Diagnostics.Append(stateDiags...) - if resp.Diagnostics.HasError() { - return - } - } - if plan.SecurityAccess != nil && !reflect.DeepEqual(plan.SecurityAccess, state.SecurityAccess) { - if err := updateSecurityAccess(ctx, r.client.Environments, plan); err != nil { - utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "update security access") - return - } - state.SecurityAccess = plan.SecurityAccess - stateDiags = resp.State.Set(ctx, state) - resp.Diagnostics.Append(stateDiags...) - if resp.Diagnostics.HasError() { - return - } - } - if !plan.Tags.IsNull() && !reflect.DeepEqual(plan.Tags, state.Tags) { - if err := updateTags(ctx, r.client.Environments, plan); err != nil { - utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "update tags") - return - } - state.Tags = plan.Tags - stateDiags = resp.State.Set(ctx, state) - resp.Diagnostics.Append(stateDiags...) - if resp.Diagnostics.HasError() { - return - } - } - if plan.ProxyConfigName != state.ProxyConfigName { - if err := updateProxyConfig(ctx, r.client.Environments, plan); err != nil { - utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "update proxy config") - return - } - state.ProxyConfigName = plan.ProxyConfigName - stateDiags = resp.State.Set(ctx, state) - resp.Diagnostics.Append(stateDiags...) - if resp.Diagnostics.HasError() { - return - } + stateDiags = resp.State.Set(ctx, state) + if resp.Diagnostics.HasError() { + return } resp.State.Set(ctx, state) } @@ -344,3 +260,99 @@ func toAwsEnvironmentResource(ctx context.Context, env *environmentsmodels.Envir model.TunnelType = types.StringValue(string(env.TunnelType)) model.WorkloadAnalytics = types.BoolValue(env.WorkloadAnalytics) } + +func updateEnvironment(ctx context.Context, plan *awsEnvironmentResourceModel, state *awsEnvironmentResourceModel, client *environmentsclient.Environments, resp *resource.UpdateResponse) *resource.UpdateResponse { + if plan.CredentialName.ValueString() != state.CredentialName.ValueString() { + params := operations.NewChangeEnvironmentCredentialParamsWithContext(ctx) + params.WithInput(&environmentsmodels.ChangeEnvironmentCredentialRequest{ + CredentialName: plan.CredentialName.ValueStringPointer(), + EnvironmentName: state.EnvironmentName.ValueStringPointer(), + }) + _, err := client.Operations.ChangeEnvironmentCredential(params) + if err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "change AWS Environment credential") + return resp + } + } + + if state.EncryptionKeyArn != plan.EncryptionKeyArn { + if err := updateAwsDiskEncryptionParameters(ctx, client, *plan); err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "update disk encryption parameters") + return resp + } + state.EncryptionKeyArn = plan.EncryptionKeyArn + } + + if plan.Authentication != nil && !reflect.DeepEqual(plan.Authentication, state.Authentication) { + if err := updateSshKey(ctx, client, plan.Authentication, plan.EnvironmentName.ValueStringPointer()); err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "update SSH key") + return resp + } + state.Authentication = plan.Authentication + } + + if !reflect.DeepEqual(utils.FromSetValueToStringList(plan.SubnetIds), utils.FromSetValueToStringList(state.SubnetIds)) || + !reflect.DeepEqual(plan.EndpointAccessGatewaySubnetIds, state.EndpointAccessGatewaySubnetIds) { + if err := updateSubnet(ctx, client, *plan); err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "update subnet") + return resp + } + state.SubnetIds = plan.SubnetIds + state.EndpointAccessGatewaySubnetIds = plan.EndpointAccessGatewaySubnetIds + } + + if plan.SecurityAccess != nil && !reflect.DeepEqual(plan.SecurityAccess, state.SecurityAccess) { + if err := updateSecurityAccess(ctx, client, *plan); err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "update security access") + return resp + } + state.SecurityAccess = plan.SecurityAccess + } + + if !plan.Tags.IsNull() && !reflect.DeepEqual(plan.Tags, state.Tags) { + if err := updateTags(ctx, client, *plan); err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "update tags") + return resp + } + state.Tags = plan.Tags + } + + if plan.ProxyConfigName != state.ProxyConfigName { + if err := updateProxyConfig(ctx, client, *plan); err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "update proxy config") + return resp + } + state.ProxyConfigName = plan.ProxyConfigName + } + return resp +} + +func startEnvironment(ctx context.Context, state *awsEnvironmentResourceModel, resp *resource.UpdateResponse, client *environmentsclient.Environments) error { + if !(state.PollingOptions != nil && state.PollingOptions.Async.ValueBool()) { + stateSaver := func(env *environmentsmodels.Environment) { + toAwsEnvironmentResource(ctx, utils.LogEnvironmentSilently(ctx, env, describeLogPrefix), state, state.PollingOptions, &resp.Diagnostics) + } + _, err := waitForStartEnvironmentWithDiagnosticHandle(ctx, client, state.ID.ValueString(), state.EnvironmentName.ValueString(), resp, state.PollingOptions, stateSaver) + if err != nil { + return err + } + } + return nil +} + +func stopAndWaitForEnvironment(ctx context.Context, environment string, pollingOptions *utils.PollingOptions, resp *resource.UpdateResponse, client *environmentsclient.Environments) error { + params := operations.NewStopEnvironmentParamsWithContext(ctx) + params.WithInput(&environmentsmodels.StopEnvironmentRequest{ + EnvironmentName: &environment, + }) + _, err := client.Operations.StopEnvironment(params) + if err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "stop Environment") + return err + } + if err := waitForEnvironmentToBeStopped(environment, timeoutOneHour, callFailureThreshold, client, ctx, pollingOptions); err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "create Environment failed") + return err + } + return nil +} diff --git a/resources/environments/update_freeipa_service.go b/resources/environments/update_freeipa_service.go new file mode 100644 index 00000000..e130cffb --- /dev/null +++ b/resources/environments/update_freeipa_service.go @@ -0,0 +1,118 @@ +package environments + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "reflect" + + environmentsclient "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" +) + +func updateFreeIpaRecipes(ctx context.Context, client *environmentsclient.Environments, planRecipes types.Set, stateRecipes types.Set, environment *string) error { + recipesToBeAdded := make([]string, 0) + recipesToBeRemoved := make([]string, 0) + + for _, planRecipe := range utils.FromSetValueToStringList(planRecipes) { + shouldBeAdded := true + for _, stateRecipe := range utils.FromSetValueToStringList(stateRecipes) { + if planRecipe == stateRecipe { + shouldBeAdded = false + break + } + } + if shouldBeAdded { + recipesToBeAdded = append(recipesToBeAdded, planRecipe) + } + } + + for _, stateRecipe := range utils.FromSetValueToStringList(stateRecipes) { + shouldBeRemoved := true + for _, planRecipe := range utils.FromSetValueToStringList(planRecipes) { + if stateRecipe == planRecipe { + shouldBeRemoved = false + break + } + } + if shouldBeRemoved { + recipesToBeRemoved = append(recipesToBeRemoved, stateRecipe) + } + } + + fmt.Println("recipesToBeAdded: ", recipesToBeAdded) + fmt.Println("recipesToBeRemoved: ", recipesToBeRemoved) + + paramsToAttach := operations.NewAttachFreeIpaRecipesParamsWithContext(ctx) + paramsToAttach.WithInput(&environmentsmodels.AttachFreeIpaRecipesRequest{ + Environment: environment, + Recipes: recipesToBeAdded, + }) + _, err := client.Operations.AttachFreeIpaRecipes(paramsToAttach) + + paramsToDetach := operations.NewDetachFreeIpaRecipesParamsWithContext(ctx) + paramsToDetach.WithInput(&environmentsmodels.DetachFreeIpaRecipesRequest{ + Environment: environment, + Recipes: recipesToBeRemoved, + }) + _, err = client.Operations.DetachFreeIpaRecipes(paramsToDetach) + return err +} + +func updateFreeIpa(ctx context.Context, plan *awsEnvironmentResourceModel, state *awsEnvironmentResourceModel, client *environmentsclient.Environments, resp *resource.UpdateResponse) *resource.UpdateResponse { + var freeIpaPlanDetails FreeIpaDetails + plan.FreeIpa.As(ctx, &freeIpaPlanDetails, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true}) + + var freeIpaStateDetails FreeIpaDetails + state.FreeIpa.As(ctx, &freeIpaStateDetails, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true}) + + isStopped := false + + if !reflect.DeepEqual(freeIpaStateDetails.Recipes, freeIpaPlanDetails.Recipes) { + if err := updateFreeIpaRecipes(ctx, client, freeIpaPlanDetails.Recipes, freeIpaStateDetails.Recipes, state.EnvironmentName.ValueStringPointer()); err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "attaching FreeIPA recipes") + return resp + } + } + + if !reflect.DeepEqual(freeIpaPlanDetails.InstanceType, freeIpaStateDetails.InstanceType) { + err := stopAndWaitForEnvironment(ctx, plan.EnvironmentName.ValueString(), plan.PollingOptions, resp, client) + if err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "stopping environment") + return resp + } + isStopped = true + params := operations.NewStartFreeIpaVerticalScalingParamsWithContext(ctx) + params.WithInput( + &environmentsmodels.StartFreeIpaVerticalScalingRequest{ + Environment: state.EnvironmentName.ValueStringPointer(), + InstanceTemplate: &environmentsmodels.InstanceTemplate{ + InstanceType: freeIpaPlanDetails.InstanceType.ValueString(), + }, + }, + ) + _, err = client.Operations.StartFreeIpaVerticalScaling(params) + if err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "free IPA vertical scale") + return resp + } + if err := waitForEnvironmentToBeStopped(state.EnvironmentName.ValueString(), timeoutOneHour, callFailureThreshold, client, ctx, state.PollingOptions); err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "create Environment failed") + return resp + } + } + + if isStopped { + err := startEnvironment(ctx, state, resp, client) + if err != nil { + utils.AddEnvironmentDiagnosticsError(err, &resp.Diagnostics, "starting environment") + return resp + } + } + + return resp +}