## Description
<!-- **Please link some issue here describing what you are trying to achieve.**
In case there is no issue present for your PR, please consider creating one.
At least please give us some description what you are trying to achieve and why your change is needed. -->
relates to #1234
## Checklist
- [ ] Issue was linked above
- [ ] Code format was applied: `make fmt`
- [ ] Examples were added / adjusted (see `examples/` directory)
- [x] Docs are up-to-date: `make generate-docs` (will be checked by CI)
- [ ] Unit tests got implemented or updated
- [ ] Acceptance tests got implemented or updated (see e.g. [here](f5f99d1709/stackit/internal/services/dns/dns_acc_test.go))
- [x] Unit tests are passing: `make test` (will be checked by CI)
- [x] No linter issues: `make lint` (will be checked by CI)
Reviewed-on: #58
Co-authored-by: Marcel S. Henselin <marcel.henselin@stackit.cloud>
Co-committed-by: Marcel S. Henselin <marcel.henselin@stackit.cloud>
556 lines
16 KiB
Go
556 lines
16 KiB
Go
// Copyright (c) STACKIT
|
|
|
|
package sqlserverflexalpha
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/identityschema"
|
|
|
|
sqlserverflexalpha2 "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/instance/resources_gen"
|
|
sqlserverflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/utils"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
|
|
|
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha"
|
|
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion"
|
|
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core"
|
|
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils"
|
|
wait "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/wait/sqlserverflexalpha"
|
|
|
|
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
|
)
|
|
|
|
// Ensure the implementation satisfies the expected interfaces.
|
|
var (
|
|
_ resource.Resource = &instanceResource{}
|
|
_ resource.ResourceWithConfigure = &instanceResource{}
|
|
_ resource.ResourceWithImportState = &instanceResource{}
|
|
_ resource.ResourceWithModifyPlan = &instanceResource{}
|
|
_ resource.ResourceWithIdentity = &instanceResource{}
|
|
)
|
|
|
|
// NewInstanceResource is a helper function to simplify the provider implementation.
|
|
func NewInstanceResource() resource.Resource {
|
|
return &instanceResource{}
|
|
}
|
|
|
|
//nolint:unused // TODO: remove if not needed later
|
|
var validNodeTypes []string = []string{
|
|
"Single",
|
|
"Replica",
|
|
}
|
|
|
|
// resourceModel describes the resource data model.
|
|
type resourceModel = sqlserverflexalpha2.InstanceModel
|
|
|
|
type InstanceResourceIdentityModel struct {
|
|
ProjectID types.String `tfsdk:"project_id"`
|
|
Region types.String `tfsdk:"region"`
|
|
InstanceID types.String `tfsdk:"instance_id"`
|
|
}
|
|
|
|
// instanceResource is the resource implementation.
|
|
type instanceResource struct {
|
|
client *sqlserverflexalpha.APIClient
|
|
providerData core.ProviderData
|
|
}
|
|
|
|
// Metadata returns the resource type name.
|
|
func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
|
resp.TypeName = req.ProviderTypeName + "_sqlserverflexalpha_instance"
|
|
}
|
|
|
|
// Configure adds the provider configured client to the resource.
|
|
func (r *instanceResource) Configure(
|
|
ctx context.Context,
|
|
req resource.ConfigureRequest,
|
|
resp *resource.ConfigureResponse,
|
|
) {
|
|
var ok bool
|
|
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
apiClient := sqlserverflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
r.client = apiClient
|
|
tflog.Info(ctx, "SQLServer Flex instance client configured")
|
|
}
|
|
|
|
// ModifyPlan implements resource.ResourceWithModifyPlan.
|
|
// Use the modifier to set the effective region in the current plan.
|
|
func (r *instanceResource) ModifyPlan(
|
|
ctx context.Context,
|
|
req resource.ModifyPlanRequest,
|
|
resp *resource.ModifyPlanResponse,
|
|
) { // nolint:gocritic // function signature required by Terraform
|
|
|
|
// skip initial empty configuration to avoid follow-up errors
|
|
if req.Config.Raw.IsNull() {
|
|
return
|
|
}
|
|
var configModel sqlserverflexalpha2.InstanceModel
|
|
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
if req.Plan.Raw.IsNull() {
|
|
return
|
|
}
|
|
var planModel sqlserverflexalpha2.InstanceModel
|
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
var identityModel InstanceResourceIdentityModel
|
|
identityModel.ProjectID = planModel.ProjectId
|
|
identityModel.Region = planModel.Region
|
|
if !planModel.InstanceId.IsNull() && !planModel.InstanceId.IsUnknown() {
|
|
identityModel.InstanceID = planModel.InstanceId
|
|
}
|
|
|
|
resp.Diagnostics.Append(resp.Identity.Set(ctx, identityModel)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
}
|
|
|
|
//go:embed planModifiers.yaml
|
|
var modifiersFileByte []byte
|
|
|
|
// Schema defines the schema for the resource.
|
|
func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
|
|
|
schema := sqlserverflexalpha2.InstanceResourceSchema(ctx)
|
|
|
|
fields, err := utils.ReadModifiersConfig(modifiersFileByte)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("error during read modifiers config file", err.Error())
|
|
return
|
|
}
|
|
|
|
err = utils.AddPlanModifiersToResourceSchema(fields, &schema)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("error adding plan modifiers", err.Error())
|
|
return
|
|
}
|
|
resp.Schema = schema
|
|
}
|
|
|
|
func (r *instanceResource) IdentitySchema(
|
|
_ context.Context,
|
|
_ resource.IdentitySchemaRequest,
|
|
resp *resource.IdentitySchemaResponse,
|
|
) {
|
|
resp.IdentitySchema = identityschema.Schema{
|
|
Attributes: map[string]identityschema.Attribute{
|
|
"project_id": identityschema.StringAttribute{
|
|
RequiredForImport: true, // must be set during import by the practitioner
|
|
},
|
|
"region": identityschema.StringAttribute{
|
|
RequiredForImport: true, // can be defaulted by the provider configuration
|
|
},
|
|
"instance_id": identityschema.StringAttribute{
|
|
RequiredForImport: true, // can be defaulted by the provider configuration
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Create creates the resource and sets the initial Terraform state.
|
|
func (r *instanceResource) Create(
|
|
ctx context.Context,
|
|
req resource.CreateRequest,
|
|
resp *resource.CreateResponse,
|
|
) { // nolint:gocritic // function signature required by Terraform
|
|
var model resourceModel
|
|
diags := req.Plan.Get(ctx, &model)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// Read identity data
|
|
var identityData InstanceResourceIdentityModel
|
|
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
ctx = core.InitProviderContext(ctx)
|
|
|
|
projectId := identityData.ProjectID.ValueString()
|
|
region := identityData.Region.ValueString()
|
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
ctx = tflog.SetField(ctx, "region", region)
|
|
|
|
// Generate API request body from model
|
|
payload, err := toCreatePayload(ctx, &model)
|
|
if err != nil {
|
|
core.LogAndAddError(
|
|
ctx,
|
|
&resp.Diagnostics,
|
|
"Error creating instance",
|
|
fmt.Sprintf("Creating API payload: %v", err),
|
|
)
|
|
return
|
|
}
|
|
// Create new instance
|
|
createResp, err := r.client.CreateInstanceRequest(
|
|
ctx,
|
|
projectId,
|
|
region,
|
|
).CreateInstanceRequestPayload(*payload).Execute()
|
|
if err != nil {
|
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err))
|
|
return
|
|
}
|
|
|
|
ctx = core.LogResponse(ctx)
|
|
|
|
instanceId := *createResp.Id
|
|
|
|
// Set data returned by API in identity
|
|
identity := InstanceResourceIdentityModel{
|
|
ProjectID: types.StringValue(projectId),
|
|
Region: types.StringValue(region),
|
|
InstanceID: types.StringValue(instanceId),
|
|
}
|
|
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
utils.SetAndLogStateFields(
|
|
ctx, &resp.Diagnostics, &resp.State, map[string]any{
|
|
"id": utils.BuildInternalTerraformId(projectId, region, instanceId),
|
|
"instance_id": instanceId,
|
|
},
|
|
)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// The creation waiter sometimes returns an error from the API: "instance with id xxx has unexpected status Failure"
|
|
// which can be avoided by sleeping before wait
|
|
waitResp, err := wait.CreateInstanceWaitHandler(
|
|
ctx,
|
|
r.client,
|
|
projectId,
|
|
instanceId,
|
|
region,
|
|
).SetSleepBeforeWait(
|
|
30 * time.Second,
|
|
).SetTimeout(
|
|
90 * time.Minute,
|
|
).WaitWithContext(ctx)
|
|
if err != nil {
|
|
core.LogAndAddError(
|
|
ctx,
|
|
&resp.Diagnostics,
|
|
"Error creating instance",
|
|
fmt.Sprintf("Instance creation waiting: %v", err),
|
|
)
|
|
return
|
|
}
|
|
|
|
if waitResp.FlavorId == nil {
|
|
core.LogAndAddError(
|
|
ctx,
|
|
&resp.Diagnostics,
|
|
"Error creating instance",
|
|
"Instance creation waiting: returned flavor id is nil",
|
|
)
|
|
return
|
|
}
|
|
|
|
// Map response body to schema
|
|
// err = mapFields(ctx, waitResp, &model, storage, encryption, network, region)
|
|
err = mapFields(ctx, waitResp, &model, resp.Diagnostics)
|
|
if err != nil {
|
|
core.LogAndAddError(
|
|
ctx,
|
|
&resp.Diagnostics,
|
|
"Error creating instance",
|
|
fmt.Sprintf("Processing API payload: %v", err),
|
|
)
|
|
return
|
|
}
|
|
// Set state to fully populated data
|
|
diags = resp.State.Set(ctx, model)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
tflog.Info(ctx, "SQLServer Flex instance created")
|
|
}
|
|
|
|
// Read refreshes the Terraform state with the latest data.
|
|
func (r *instanceResource) Read(
|
|
ctx context.Context,
|
|
req resource.ReadRequest,
|
|
resp *resource.ReadResponse,
|
|
) { // nolint:gocritic // function signature required by Terraform
|
|
var model resourceModel
|
|
diags := req.State.Get(ctx, &model)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// Read identity data
|
|
var identityData InstanceResourceIdentityModel
|
|
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
ctx = core.InitProviderContext(ctx)
|
|
|
|
projectId := model.ProjectId.ValueString()
|
|
instanceId := model.InstanceId.ValueString()
|
|
region := r.providerData.GetRegionWithOverride(model.Region)
|
|
|
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
ctx = tflog.SetField(ctx, "instance_id", instanceId)
|
|
ctx = tflog.SetField(ctx, "region", region)
|
|
|
|
instanceResp, err := r.client.GetInstanceRequest(ctx, projectId, region, instanceId).Execute()
|
|
if err != nil {
|
|
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
|
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
|
resp.State.RemoveResource(ctx)
|
|
return
|
|
}
|
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", err.Error())
|
|
return
|
|
}
|
|
|
|
ctx = core.LogResponse(ctx)
|
|
|
|
// Map response body to schema
|
|
err = mapFields(ctx, instanceResp, &model, resp.Diagnostics)
|
|
if err != nil {
|
|
core.LogAndAddError(
|
|
ctx,
|
|
&resp.Diagnostics,
|
|
"Error reading instance",
|
|
fmt.Sprintf("Processing API payload: %v", err),
|
|
)
|
|
return
|
|
}
|
|
// Set refreshed state
|
|
diags = resp.State.Set(ctx, model)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// Set data returned by API in identity
|
|
identity := InstanceResourceIdentityModel{
|
|
ProjectID: types.StringValue(projectId),
|
|
Region: types.StringValue(region),
|
|
InstanceID: types.StringValue(instanceId),
|
|
}
|
|
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
tflog.Info(ctx, "SQLServer Flex instance read")
|
|
}
|
|
|
|
// Update updates the resource and sets the updated Terraform state on success.
|
|
func (r *instanceResource) Update(
|
|
ctx context.Context,
|
|
req resource.UpdateRequest,
|
|
resp *resource.UpdateResponse,
|
|
) { // nolint:gocritic // function signature required by Terraform
|
|
// Retrieve values from plan
|
|
var model resourceModel
|
|
diags := req.Plan.Get(ctx, &model)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
ctx = core.InitProviderContext(ctx)
|
|
|
|
projectId := model.ProjectId.ValueString()
|
|
instanceId := model.InstanceId.ValueString()
|
|
region := model.Region.ValueString()
|
|
|
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
ctx = tflog.SetField(ctx, "instance_id", instanceId)
|
|
ctx = tflog.SetField(ctx, "region", region)
|
|
|
|
// Generate API request body from model
|
|
payload, err := toUpdatePayload(ctx, &model, resp)
|
|
if err != nil {
|
|
core.LogAndAddError(
|
|
ctx,
|
|
&resp.Diagnostics,
|
|
"Error updating instance",
|
|
fmt.Sprintf("Creating API payload: %v", err),
|
|
)
|
|
return
|
|
}
|
|
// Update existing instance
|
|
err = r.client.UpdateInstanceRequest(
|
|
ctx,
|
|
projectId,
|
|
region,
|
|
instanceId,
|
|
).UpdateInstanceRequestPayload(*payload).Execute()
|
|
if err != nil {
|
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", err.Error())
|
|
return
|
|
}
|
|
|
|
ctx = core.LogResponse(ctx)
|
|
|
|
waitResp, err := wait.UpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId, region).WaitWithContext(ctx)
|
|
if err != nil {
|
|
core.LogAndAddError(
|
|
ctx,
|
|
&resp.Diagnostics,
|
|
"Error updating instance",
|
|
fmt.Sprintf("Instance update waiting: %v", err),
|
|
)
|
|
return
|
|
}
|
|
|
|
// Map response body to schema
|
|
err = mapFields(ctx, waitResp, &model, resp.Diagnostics)
|
|
// err = mapFields(ctx, waitResp, &model, storage, encryption, network, region)
|
|
if err != nil {
|
|
core.LogAndAddError(
|
|
ctx,
|
|
&resp.Diagnostics,
|
|
"Error updating instance",
|
|
fmt.Sprintf("Processing API payload: %v", err),
|
|
)
|
|
return
|
|
}
|
|
diags = resp.State.Set(ctx, model)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
tflog.Info(ctx, "SQLServer Flex instance updated")
|
|
}
|
|
|
|
// Delete deletes the resource and removes the Terraform state on success.
|
|
func (r *instanceResource) Delete(
|
|
ctx context.Context,
|
|
req resource.DeleteRequest,
|
|
resp *resource.DeleteResponse,
|
|
) { // nolint:gocritic // function signature required by Terraform
|
|
// Retrieve values from state
|
|
var model resourceModel
|
|
diags := req.State.Get(ctx, &model)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
ctx = core.InitProviderContext(ctx)
|
|
|
|
projectId := model.ProjectId.ValueString()
|
|
instanceId := model.InstanceId.ValueString()
|
|
region := model.Region.ValueString()
|
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
ctx = tflog.SetField(ctx, "instance_id", instanceId)
|
|
ctx = tflog.SetField(ctx, "region", region)
|
|
|
|
// Delete existing instance
|
|
err := r.client.DeleteInstanceRequest(ctx, projectId, region, instanceId).Execute()
|
|
if err != nil {
|
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err))
|
|
return
|
|
}
|
|
|
|
ctx = core.LogResponse(ctx)
|
|
|
|
_, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId, region).WaitWithContext(ctx)
|
|
if err != nil {
|
|
core.LogAndAddError(
|
|
ctx,
|
|
&resp.Diagnostics,
|
|
"Error deleting instance",
|
|
fmt.Sprintf("Instance deletion waiting: %v", err),
|
|
)
|
|
return
|
|
}
|
|
tflog.Info(ctx, "SQLServer Flex instance deleted")
|
|
}
|
|
|
|
// ImportState imports a resource into the Terraform state on success.
|
|
// The expected format of the resource import identifier is: project_id,instance_id
|
|
func (r *instanceResource) ImportState(
|
|
ctx context.Context,
|
|
req resource.ImportStateRequest,
|
|
resp *resource.ImportStateResponse,
|
|
) {
|
|
if req.ID != "" {
|
|
idParts := strings.Split(req.ID, core.Separator)
|
|
|
|
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
|
|
core.LogAndAddError(
|
|
ctx, &resp.Diagnostics,
|
|
"Error importing instance",
|
|
fmt.Sprintf(
|
|
"Expected import identifier with format: [project_id],[region],[instance_id] Got: %q",
|
|
req.ID,
|
|
),
|
|
)
|
|
return
|
|
}
|
|
|
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
|
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...)
|
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...)
|
|
return
|
|
}
|
|
|
|
// If no ID is provided, attempt to read identity attributes from the import configuration
|
|
var identityData InstanceResourceIdentityModel
|
|
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
projectId := identityData.ProjectID.ValueString()
|
|
region := identityData.Region.ValueString()
|
|
instanceId := identityData.InstanceID.ValueString()
|
|
|
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
|
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...)
|
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), instanceId)...)
|
|
|
|
tflog.Info(ctx, "SQLServer Flex instance state imported")
|
|
}
|