package utilities import ( "context" "fmt" "strconv" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" "repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi/bservice" "repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/client" "repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/service/cloudapi/bservice/models" "repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/service/cloudapi/ic" "repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/status" ) // BServiceResourceCheckPresence checks if BService with serviceID exists func BServiceResourceCheckPresence(ctx context.Context, serviceID uint64, c *client.Client) (*bservice.RecordBasicService, error) { tflog.Info(ctx, fmt.Sprintf("BServiceResourceCheckPresence: Get info about service with ID - %v", serviceID)) record, err := c.CloudAPI().BService().Get(ctx, bservice.GetRequest{ServiceID: serviceID}) if err != nil { return nil, fmt.Errorf("BServiceResourceCheckPresence: cannot get info about resource with error: %w", err) } tflog.Info(ctx, "BServiceResourceCheckPresence: response from CloudAPI().BService().Get", map[string]any{"service_id": serviceID, "response": record}) return record, err } // BServiceReadStatus loads BService resource by its id, gets it current status. Performs restore and enable if needed for // Deleted status. // In case of failure returns errors. func BSerivceReadStatus(ctx context.Context, state *models.RecordBasicServiceResourceModel, c *client.Client) diag.Diagnostics { tflog.Info(ctx, "BSerivceReadStatus: Read status resource with ID", map[string]any{"resource_id": state.ID.ValueString()}) diags := diag.Diagnostics{} resourceId, err := strconv.ParseUint(state.ID.ValueString(), 10, 64) if err != nil { diags.AddError("BSerivceReadStatus: Cannot parse resource ID from state", err.Error()) return diags } record, err := BServiceResourceCheckPresence(ctx, resourceId, c) if err != nil { diags.AddError("BSerivceReadStatus: Unable to Read bservice before status check", err.Error()) return diags } // check resource status switch record.Status { case status.Disabled: tflog.Info(ctx, "The BService is in status Disabled, troubles may occur with update. Please, enable BService first.") case status.Modeled: diags.AddError("The basic service is in status: %s, please, contact support for more information", record.Status) case status.Disabling: tflog.Info(ctx, fmt.Sprintf("The basic service is in status: %s, troubles can occur with the update.", record.Status)) case status.Deleted: restore := state.Restore.ValueBool() if state.Restore.IsNull() { restore = true } // default true if restore { // attempt to restore bservice tflog.Info(ctx, "BServiceReadStatus: BService with status.Deleted is being read, attempt to restore it", map[string]any{ "BService": state.ID.ValueString(), "status": record.Status}) diags.Append(restoreBservice(ctx, resourceId, c)...) if diags.HasError() { tflog.Error(ctx, "BServiceReadStatus: cannot restore BService") return diags } tflog.Info(ctx, "BServiceReadStatus: BService restored successfully", map[string]any{"service_id": resourceId}) } else { tflog.Info(ctx, "BServiceReadStatus: BService is i status Deleted but restore is not specified") } case status.Destroyed: diags.AddError( "BSerivceReadStatus: BService is in status Destroyed", fmt.Sprintf("the resource with bservice_id %d cannot be read or updated because it has been destroyed", resourceId), ) return diags case status.Destroying: diags.AddError( "BSerivceReadStatus: BService is in progress with status Destroying", fmt.Sprintf("the resource with bservice_id %d cannot be read or updated because it is currently being destroyed", resourceId), ) return diags } return nil } func BServiceResourceCreate(ctx context.Context, plan *models.RecordBasicServiceResourceModel, c *client.Client) (*uint64, diag.Diagnostics) { tflog.Info(ctx, "Start BServiceResourceCreate", map[string]any{"service_id": plan.ServiceId.ValueInt64()}) diags := diag.Diagnostics{} err := ic.ExistRG(ctx, uint64(plan.RGID.ValueInt64()), c) if err != nil { diags.AddError(fmt.Sprintf("resourceBasicServiceCreate: can't create basic service because RGID %d is not allowed or does not exist", plan.RGID.ValueInt64()), err.Error()) return nil, diags } req := bservice.CreateRequest{} req.Name = plan.Name.ValueString() req.RGID = uint64(plan.RGID.ValueInt64()) if !plan.SSHKey.IsNull() { req.SSHKey = plan.SSHKey.ValueString() } if !plan.SSHUser.IsNull() { req.SSHUser = plan.SSHUser.ValueString() } // Make request and get response serviceId, err := c.CloudAPI().BService().Create(ctx, req) if err != nil { tflog.Error(ctx, "Error response for create bservice", map[string]any{"error": err.Error()}) diags.AddError("Unable to Create bservice", err.Error()) return nil, diags } plan.ID = types.StringValue(strconv.Itoa(int(serviceId))) enable := plan.Enable.ValueBool() if enable { tflog.Info(ctx, "resourceBasicServiceCreate: before calling CloudAPI().BService().Enable", map[string]any{"service_id": serviceId}) res, err := c.CloudAPI().BService().Enable(ctx, bservice.EnableRequest{ServiceID: serviceId}) if err != nil { diags.AddWarning( "resourceBasicServiceCreate: cannot enable BService", err.Error(), ) return &serviceId, diags } tflog.Info(ctx, "resourceBasicServiceCreate: response from CloudAPI().BService().Enable", map[string]any{"service_id": serviceId, "response": res}) } if plan.Start.ValueBool() { if !enable { diags.AddWarning( "can not start bservice that is not enabled. Set enable = true and start = true to enable and start bservice", fmt.Sprintf("service_id: %v", serviceId), ) return &serviceId, diags } tflog.Info(ctx, "resourceBasicServiceCreate: before calling CloudAPI().BService().Start", map[string]any{"service_id": serviceId}) _, err := c.CloudAPI().BService().Start(ctx, bservice.StartRequest{ ServiceID: serviceId, }) if err != nil { diags.AddWarning( "resourceBasicServiceCreate: cannot start BService", err.Error(), ) return &serviceId, diags } } tflog.Info(ctx, "End resourceBasicServiceCreate", map[string]any{"service_id": serviceId}) return &serviceId, diags } // EnableDisableBService performs BService Enable/Disable request. // Returns error in case of failures. func EnableDisableBService(ctx context.Context, plan *models.RecordBasicServiceResourceModel, c *client.Client) diag.Diagnostics { tflog.Info(ctx, "Start EnableDisableBService", map[string]any{"service_id": plan.ID.ValueString()}) diags := diag.Diagnostics{} serviceID, err := strconv.Atoi(plan.ID.ValueString()) if err != nil { diags.AddError("EnableDisableBService: Cannot parse ID from state", err.Error()) return diags } if plan.Enable.ValueBool() { tflog.Info(ctx, "EnableDisableBService: before calling CloudAPI().BService().Enable", map[string]any{"service_id": serviceID}) res, err := c.CloudAPI().BService().Enable(ctx, bservice.EnableRequest{ServiceID: uint64(serviceID)}) if err != nil { diags.AddError( "EnableDisableBService: cannot enable BService", err.Error(), ) return diags } tflog.Info(ctx, "EnableDisableBService: response from CloudAPI().BService().Enable", map[string]any{"service_id": serviceID, "response": res}) return nil } else { tflog.Info(ctx, "EnableDisableBService: before calling CloudAPI().BService().Disable", map[string]any{"service_id": serviceID}) res, err := c.CloudAPI().BService().Disable(ctx, bservice.DisableRequest{ServiceID: uint64(serviceID)}) if err != nil { diags.AddError( "EnableDisableBService: cannot disable BService", err.Error(), ) return diags } tflog.Info(ctx, "EnableDisableBService: response from CloudAPI().BService().Disable", map[string]any{"service_id": serviceID, "response": res}) } return nil } // StartStopBService performs BService Start/Stop request. // Returns error in case of failures. func StartStopBService(ctx context.Context, plan *models.RecordBasicServiceResourceModel, c *client.Client) diag.Diagnostics { tflog.Info(ctx, "Start StartStopBService", map[string]any{"service_id": plan.ID.ValueString()}) diags := diag.Diagnostics{} serviceID, err := strconv.Atoi(plan.ID.ValueString()) if err != nil { diags.AddError("StartStopBService: Cannot parse ID from state", err.Error()) return diags } if plan.Start.ValueBool() { tflog.Info(ctx, "StartStopBService: before calling CloudAPI().BService().Start", map[string]any{"service_id": serviceID}) res, err := c.CloudAPI().BService().Start(ctx, bservice.StartRequest{ServiceID: uint64(serviceID)}) if err != nil { diags.AddError( "StartStopBService: cannot start BService", err.Error(), ) return diags } tflog.Info(ctx, "StartStopBService: response from CloudAPI().BService().Start", map[string]any{"service_id": serviceID, "response": res}) return nil } else { tflog.Info(ctx, "StartStopBService: before calling CloudAPI().BService().Stop", map[string]any{"service_id": serviceID}) res, err := c.CloudAPI().BService().Stop(ctx, bservice.StopRequest{ServiceID: uint64(serviceID)}) if err != nil { diags.AddError( "StartStopBService: cannot stop BService", err.Error(), ) return diags } tflog.Info(ctx, "StartStopBService: response from CloudAPI().BService().Stop", map[string]any{"service_id": serviceID, "response": res}) } return nil } func SnapshotsBService(ctx context.Context, oldSnapshots basetypes.SetValue, newSnapshots basetypes.SetValue, serviceID uint64, c *client.Client) diag.Diagnostics { diags := diag.Diagnostics{} // Handle snapshot changes in the plan tflog.Info(ctx, "Start SnapshotsBService", map[string]any{"service_id": serviceID}) addMap, rollbackMap, deleteMap := differenceSnapshots(oldSnapshots, newSnapshots) tflog.Debug(ctx, "SnapshotsBService: Snapshots to be deleted", map[string]any{"deleted_snapshots": deleteMap}) tflog.Debug(ctx, "SnapshotsBService: Snapshots to be added", map[string]any{"added_snapshots": addMap}) tflog.Debug(ctx, "SnapshotsBService: Snapshots to be updated", map[string]any{"updated_snapshots": rollbackMap}) if len(deleteMap) > 0 { for _, snapshot := range deleteMap { req := bservice.SnapshotDeleteRequest{ ServiceID: serviceID, Label: snapshot["label"].(types.String).ValueString(), } tflog.Info(ctx, "SnapshotsBService: before calling CloudAPI().BService().SnapshotDelete", map[string]any{"service_id": serviceID, "req": req}) _, err := c.CloudAPI().BService().SnapshotDelete(ctx, req) if err != nil { tflog.Error(ctx, "SnapshotsBService: Failed to delete snapshot") diags.AddError(fmt.Sprintf("SnapshotsBService: failed to delete snapshot %d from service %d", snapshot["label"].(types.String).ValueString(), serviceID), err.Error()) return diags } tflog.Info(ctx, "Deleted snapshot", map[string]any{"service_id": serviceID, "label": req.Label}) } } if len(addMap) > 0 { for _, snapshot := range addMap { req := bservice.SnapshotCreateRequest{ ServiceID: serviceID, Label: snapshot["label"].(types.String).ValueString(), } tflog.Info(ctx, "SnapshotsBService: before calling CloudAPI().BService().SnapshotCreate", map[string]any{"service_id": serviceID, "req": req}) _, err := c.CloudAPI().BService().SnapshotCreate(ctx, req) if err != nil { tflog.Error(ctx, "SnapshotsBService: Failed to create snapshot") diags.AddError(fmt.Sprintf("SnapshotsBService: failed to create snapshot %d for service %d", snapshot["label"].(types.String).ValueString(), serviceID), err.Error()) return diags } tflog.Info(ctx, "Created snapshot", map[string]any{"service_id": serviceID, "label": req.Label}) } } if len(rollbackMap) > 0 { for _, snapshot := range rollbackMap { if snapshot["rollback"].(types.Bool).ValueBool() { req := bservice.SnapshotRollbackRequest{ ServiceID: serviceID, Label: snapshot["label"].(types.String).ValueString(), } tflog.Info(ctx, "SnapshotsBService: before calling CloudAPI().BService().SnapshotRollback", map[string]any{"service_id": serviceID, "req": req}) _, err := c.CloudAPI().BService().SnapshotRollback(ctx, req) if err != nil { tflog.Error(ctx, "SnapshotsBService: Failed to rollback snapshot") diags.AddError(fmt.Sprintf("SnapshotsBService: failed to rollback snapshot %d from service %d", snapshot["label"].(types.String).ValueString(), serviceID), err.Error()) return diags } tflog.Info(ctx, "Rolled back snapshot", map[string]any{"service_id": serviceID, "label": req.Label}) } } } return nil } func differenceSnapshots(oldSet, newSet types.Set) (added, rollback, removed []map[string]attr.Value) { oldSlice := oldSet.Elements() newSlice := newSet.Elements() added = make([]map[string]attr.Value, 0) rollback = make([]map[string]attr.Value, 0) removed = make([]map[string]attr.Value, 0) for _, oldSnapshot := range oldSlice { oldMap := oldSnapshot.(types.Object).Attributes() found := false for _, newSnapshot := range newSlice { newMap := newSnapshot.(types.Object).Attributes() if newMap["label"] == oldMap["label"] { if newMap["rollback"] != oldMap["rollback"] { rollback = append(rollback, newMap) } found = true break } } if found { continue } removed = append(removed, oldMap) } for _, newSnapshot := range newSlice { newMap := newSnapshot.(types.Object).Attributes() found := false for _, oldSnapshot := range oldSlice { oldMap := oldSnapshot.(types.Object).Attributes() if newMap["label"] == oldMap["label"] { if newMap["rollback"] != oldMap["rollback"] { rollback = append(rollback, newMap) } found = true break } } if found { continue } added = append(added, newMap) } return } // restoreBservice performs BService Restore request. // Returns error in case of failures. func restoreBservice(ctx context.Context, serviceID uint64, c *client.Client) diag.Diagnostics { diags := diag.Diagnostics{} restoreReq := bservice.RestoreRequest{ ServiceID: serviceID, } tflog.Info(ctx, "restoreBservice: before calling CloudAPI().BService().Restore", map[string]any{"service_id": serviceID, "req": restoreReq}) res, err := c.CloudAPI().BService().Restore(ctx, restoreReq) if err != nil { diags.AddError( "restoreBservice: cannot restore BService", err.Error(), ) return diags } tflog.Info(ctx, "restoreBservice: response from CloudAPI().BService().Restore", map[string]any{"service_id": serviceID, "response": res}) return nil }