package vins import ( "context" "fmt" "strconv" "time" "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker/vins" "repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/client" "repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/constants" "repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/service/cloudbroker/vins/flattens" "repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/service/cloudbroker/vins/models" "repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/service/cloudbroker/vins/schemas" "repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/service/cloudbroker/vins/utilities" ) // Ensure the implementation satisfies the expected interfaces. var ( _ resource.Resource = &resourceVINS{} _ resource.ResourceWithImportState = &resourceVINS{} ) // NewResourceVINS is a helper function to simplify the provider implementation. func NewResourceVINS() resource.Resource { return &resourceVINS{} } // resourceVINS is the resource implementation. type resourceVINS struct { client *client.Client } // Create creates the resource and sets the initial Terraform state. func (r *resourceVINS) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // Get plan to create vins var plan models.ResourceVINSModel resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Create resourceVINS: Error receiving the plan") return } tflog.Info(ctx, "Create resourceVINS: got plan successfully", map[string]any{"name": plan.Name.ValueString()}) tflog.Info(ctx, "Create resourceVINS: start creating", map[string]any{"name": plan.Name.ValueString()}) // Set timeouts createTimeout, diags := plan.Timeouts.Create(ctx, constants.Timeout20m) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Create resourceVINS: Error set timeout") return } tflog.Info(ctx, "Create resourceVINS: set timeouts successfully", map[string]any{ "name": plan.Name.ValueString(), "createTimeout": createTimeout}) ctx, cancel := context.WithTimeout(ctx, createTimeout) defer cancel() // Check if input values are valid in the platform tflog.Info(ctx, "Create resourceVINS: starting input checks", map[string]any{"name": plan.Name.ValueString()}) resp.Diagnostics.Append(resourceVINSInputChecks(ctx, &plan, r.client)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Create resourceVINS: Error input checks") return } tflog.Info(ctx, "Create resourceVINS: input checks successful", map[string]any{"name": plan.Name.ValueString()}) var vinsId uint64 // Make create request and get response for creation in RG if !plan.RGID.IsUnknown() { vinsId, diags = utilities.CreateInRGResourceVINS(ctx, &plan, r.client) if diags.HasError() { resp.Diagnostics.Append(diags...) tflog.Error(ctx, "Create resourceVINS: Error response for create in RG of resource vins") return } } // Make create request and get response for creation in account if !plan.AccountID.IsUnknown() { vinsId, diags = utilities.CreateInAccountResourceVINS(ctx, &plan, r.client) if diags.HasError() { resp.Diagnostics.Append(diags...) tflog.Error(ctx, "Create resourceVINS: Error response for create in account of resource vins") return } } plan.Id = types.StringValue(strconv.Itoa(int(vinsId))) plan.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) tflog.Info(ctx, "Create resourceVINS: vins created", map[string]any{"vins_id": vinsId, "name": plan.Name.ValueString()}) // additional settings after vins creation: in case of failures, warnings are added to resp.Diagnostics, // because additional settings failure is not critical. If errors were added instead of warnings, terraform // framework would mark resource as tainted and delete it, which would be unwanted behaviour. // reserve ip for vins after creation, warnings added to resp.Diagnostics in case of failure. if !plan.IP.IsNull() { // IP is optional resp.Diagnostics.Append(utilities.IPCreateVINS(ctx, vinsId, &plan, r.client)...) } // add nat rules for vins after creation, warnings added to resp.Diagnostics in case of failure. if !plan.NatRule.IsNull() { // NatRule is optional resp.Diagnostics.Append(utilities.NATRuleCreateVINS(ctx, vinsId, &plan, r.client)...) } // update default qos for vins after creation, warnings added to resp.Diagnostics in case of failure. if !plan.DefaultQOS.IsUnknown() { // DefaultQOS is optional && computed resp.Diagnostics.Append(utilities.DefaultQosCreateVINS(ctx, vinsId, &plan, r.client)...) } tflog.Info(ctx, "Create resourceVINS: resource creation is completed", map[string]any{"vins_id": vinsId}) // Map response body to schema and populate Computed attribute values resp.Diagnostics.Append(flattens.VINSResource(ctx, &plan, r.client)...) if resp.Diagnostics.HasError() { return } // Set data last update plan.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) // Set state to fully populated data resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) if resp.Diagnostics.HasError() { return } } // Read refreshes the Terraform state with the latest data. func (r *resourceVINS) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // Get current state var state models.ResourceVINSModel resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Read resourceVINS: Error get state") return } tflog.Info(ctx, "Read resourceVINS: got state successfully", map[string]any{"vins_id": state.Id.ValueString()}) // Set timeouts readTimeout, diags := state.Timeouts.Read(ctx, constants.Timeout600s) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Read resourceVINS: Error set timeout") return } tflog.Info(ctx, "Read resourceVINS: set timeouts successfully", map[string]any{ "vins_id": state.Id.ValueString(), "readTimeout": readTimeout}) ctx, cancel := context.WithTimeout(ctx, readTimeout) defer cancel() // read status tflog.Info(ctx, "Read resourceVINS: before VINSReadStatus", map[string]any{"vins_id": state.Id.ValueString()}) resp.Diagnostics.Append(utilities.VINSReadStatus(ctx, &state, r.client)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Read resourceVINS: Error reading status") return } // Overwrite items with refreshed state resp.Diagnostics.Append(flattens.VINSResource(ctx, &state, r.client)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Read resourceVINS: Error flatten") return } // Set refreshed state resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Read resourceVINS: Error set state") return } tflog.Info(ctx, "End read resourceVINS") } // Update updates the resource and sets the updated Terraform state on success. func (r *resourceVINS) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // Retrieve values from plan var plan models.ResourceVINSModel resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Update resourceVINS: Error receiving the plan") return } tflog.Info(ctx, "Update resourceVINS: got plan successfully", map[string]any{"vins_id": plan.Id.ValueString()}) // Retrieve values from state var state models.ResourceVINSModel resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Update resourceVINS: Error receiving the state") return } tflog.Info(ctx, "Update resourceVINS: got state successfully", map[string]any{"vins_id": state.Id.ValueString()}) // Set timeouts updateTimeout, diags := plan.Timeouts.Update(ctx, constants.Timeout20m) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Error set timeout") return } tflog.Info(ctx, "Update resourceVINS: set timeouts successfully", map[string]any{ "vins_id": state.Id.ValueString(), "updateTimeout": updateTimeout}) ctx, cancel := context.WithTimeout(ctx, updateTimeout) defer cancel() // Checking for values in the platform tflog.Info(ctx, "Update resourceVINS: starting input checks", map[string]any{"vins_id": plan.Id.ValueString()}) resp.Diagnostics.Append(resourceVINSInputChecks(ctx, &plan, r.client)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Update resourceVINS: Error input checks") return } tflog.Info(ctx, "Update resourceVINS: input checks successful", map[string]any{"vins_id": state.Id.ValueString()}) vinsId, err := strconv.ParseUint(state.Id.ValueString(), 10, 64) if err != nil { resp.Diagnostics.AddError(fmt.Sprintf("Cannot parse vins ID %s from state", state.Id.ValueString()), err.Error()) return } // enable/disable vins if needed if !plan.Enable.Equal(state.Enable) && !plan.Enable.IsNull() { resp.Diagnostics.Append(utilities.EnableDisableUpdateVINS(ctx, vinsId, &plan, r.client)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Update resourceVINS: Error enabling/disabling vins") return } } // connect/disconnect extnet for vins if needed if !plan.ExtNet.Equal(state.ExtNet) { resp.Diagnostics.Append(utilities.ExtNetUpdateVINS(ctx, vinsId, &plan, &state, r.client)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Update resourceVINS: Error updating vins extnet") return } } // reserve/release ip for vins if needed if !plan.IP.Equal(state.IP) { resp.Diagnostics.Append(utilities.IPUpdateVINS(ctx, vinsId, &plan, &state, r.client)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Update resourceVINS: Error releasing/reserving vins ip") return } } // add/delete nat rules for vins if needed if !plan.NatRule.Equal(state.NatRule) { resp.Diagnostics.Append(utilities.NATRuleUpdateVINS(ctx, vinsId, &plan, &state, r.client)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Update resourceVINS: Error adding/deleting nat rules for vins") return } } // add/delete dns for vins if needed. Empty "dns" is allowed, it will update vnfs.dhcp.config.dns from current values to empty list if !plan.DNS.IsNull() && !plan.DNS.Equal(state.DNS) { resp.Diagnostics.Append(utilities.UpdateDNSlistVINS(ctx, vinsId, &plan, r.client)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Update resourceVINS: Error updating DNSList") return } } if !plan.DefaultQOS.IsUnknown() && !plan.DefaultQOS.Equal(state.DefaultQOS) { resp.Diagnostics.Append(utilities.UpdateDefaultQosVINS(ctx, vinsId, &plan, r.client)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Update resourceVINS: Error updating DefaultQos") return } } // restart vnf_dev for vins if needed if !plan.VnfdevRestart.Equal(state.VnfdevRestart) && !plan.VnfdevRestart.IsNull() { resp.Diagnostics.Append(utilities.VnfdevRestartUpdateVINS(ctx, vinsId, &state, r.client)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Update resourceVINS: Unable to restart vnf_def for VINS") return } } // redeploy vnf_dev for vins if needed if !plan.VnfdevRedeploy.Equal(state.VnfdevRedeploy) && !plan.VnfdevRedeploy.IsNull() { resp.Diagnostics.Append(utilities.VnfdevRedeployUpdateVINS(ctx, vinsId, &state, r.client)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Update resourceVINS: Unable to redeploy vnf_def for VINS") return } } // reset vnf_dev for vins if needed if !plan.VnfdevReset.Equal(state.VnfdevReset) && !plan.VnfdevReset.IsNull() { resp.Diagnostics.Append(utilities.VnfdevResetUpdateVINS(ctx, vinsId, &state, r.client)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Update resourceVINS: Unable to reset vnf_def for VINS") return } } // start/stop vnf_dev for vins if needed if !plan.VnfdevStart.Equal(state.VnfdevStart) && !plan.VnfdevStart.IsNull() { resp.Diagnostics.Append(utilities.VnfdevStartStopUpdateVINS(ctx, vinsId, &state, r.client)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Update resourceVINS: Unable to start/stop vnf_def for VINS") return } } tflog.Info(ctx, "Update resourceVINS: resource update is completed", map[string]any{"vins_id": plan.Id.ValueString()}) // Map response body to schema and populate Computed attribute values resp.Diagnostics.Append(flattens.VINSResource(ctx, &plan, r.client)...) if resp.Diagnostics.HasError() { return } // Set data last update plan.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) // Set state to fully populated data resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) if resp.Diagnostics.HasError() { return } } // Delete deletes the resource and removes the Terraform state on success. func (r *resourceVINS) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // Get current state var state models.ResourceVINSModel resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Delete resourceVINS: Error get state") return } tflog.Info(ctx, "Delete resourceVINS: got state successfully", map[string]any{"vins_id": state.Id.ValueString()}) // Set timeouts deleteTimeout, diags := state.Timeouts.Delete(ctx, constants.Timeout600s) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { tflog.Error(ctx, "Delete resourceVINS: Error set timeout") return } tflog.Info(ctx, "Delete resourceVINS: set timeouts successfully", map[string]any{ "vins_id": state.Id.ValueString(), "deleteTimeout": deleteTimeout}) ctx, cancel := context.WithTimeout(ctx, deleteTimeout) defer cancel() // Delete existing vins delReq := vins.DeleteRequest{ VINSID: uint64(state.VinsID.ValueInt64()), } if state.Force.IsNull() { delReq.Force = true // default value } else { delReq.Force = state.Force.ValueBool() } if state.Permanently.IsNull() { delReq.Permanently = true // default value } else { delReq.Permanently = state.Permanently.ValueBool() } tflog.Info(ctx, "Delete resourceVINS: calling cloudbroker().VINS().Delete", map[string]any{ "vins_id": state.Id.ValueString(), "req": delReq, }) _, err := r.client.CloudBroker().VINS().Delete(ctx, delReq) if err != nil { resp.Diagnostics.AddError("Delete resourceVINS: Error deleting", err.Error()) return } tflog.Info(ctx, "End delete resource vins", map[string]any{"vins_id": state.Id.ValueString()}) } // Schema defines the schema for the resource. func (r *resourceVINS) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: schemas.MakeSchemaResourceVINS(), Blocks: map[string]schema.Block{ "timeouts": timeouts.Block(ctx, timeouts.Opts{Create: true, Read: true, Update: true, Delete: true}), }, } } // Metadata returns the resource type name. func (r *resourceVINS) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_cb_vins" } // Configure adds the provider configured client to the resource. func (r *resourceVINS) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { tflog.Info(ctx, "Get Configure resourceVINS") r.client = client.Resource(ctx, &req, resp) tflog.Info(ctx, "Getting Configure resourceVINS successfully") } func (r *resourceVINS) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { // Retrieve import ID and save to id attribute resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) }