Files
tf_provider/internal/provider/edge_resource.go
T

675 lines
20 KiB
Go

package provider
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
"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/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ resource.Resource = &EdgeResource{}
var _ resource.ResourceWithImportState = &EdgeResource{}
func NewEdgeResource() resource.Resource {
return &EdgeResource{}
}
type EdgeResource struct {
client *NubesClient
}
type EdgeResourceModel struct {
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
Description types.String `tfsdk:"description"`
VdcUID types.String `tfsdk:"vdc_uid"`
EdgeType types.String `tfsdk:"edge_type"`
EdgeCount types.Int64 `tfsdk:"edge_count"`
EnableAdvanced types.Bool `tfsdk:"enable_advanced"`
ExternalNetwork types.String `tfsdk:"external_network"`
Status types.String `tfsdk:"status"`
}
func (r *EdgeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_edge"
}
func (r *EdgeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Nubes Edge Gateway resource",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Edge identifier (UUID)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"display_name": schema.StringAttribute{
MarkdownDescription: "Edge display name",
Required: true,
},
"description": schema.StringAttribute{
MarkdownDescription: "Edge description",
Optional: true,
},
"vdc_uid": schema.StringAttribute{
MarkdownDescription: "VDC UUID (parameter ID 8)",
Required: true,
},
"edge_type": schema.StringAttribute{
MarkdownDescription: "Edge type: vdc (parameter ID 621)",
Optional: true,
Computed: true,
},
"edge_count": schema.Int64Attribute{
MarkdownDescription: "Number of Edge Gateways (parameter ID 341)",
Optional: true,
Computed: true,
},
"enable_advanced": schema.BoolAttribute{
MarkdownDescription: "Enable advanced features (parameter ID 340)",
Optional: true,
Computed: true,
},
"external_network": schema.StringAttribute{
MarkdownDescription: "External network name (parameter ID 367)",
Optional: true,
},
"status": schema.StringAttribute{
MarkdownDescription: "Current status of the Edge",
Computed: true,
},
},
}
}
func (r *EdgeResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*NubesClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *NubesClient, got: %T", req.ProviderData),
)
return
}
r.client = client
}
func (r *EdgeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data EdgeResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Set defaults
if data.EdgeType.IsNull() || data.EdgeType.IsUnknown() {
data.EdgeType = types.StringValue("vdc")
}
if data.EdgeCount.IsNull() || data.EdgeCount.IsUnknown() {
data.EdgeCount = types.Int64Value(1)
}
if data.EnableAdvanced.IsNull() || data.EnableAdvanced.IsUnknown() {
data.EnableAdvanced = types.BoolValue(false)
}
// Step 1: Create instance
createReq := CreateInstanceRequest{
ServiceId: 22, // Edge service ID
DisplayName: data.DisplayName.ValueString(),
Descr: data.Description.ValueString(),
}
jsonData, err := json.Marshal(createReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instances", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create instance: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Create instance failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
location := httpResp.Header.Get("Location")
if location == "" {
resp.Diagnostics.AddError("API Error", "No Location header in response")
return
}
instanceId := location[2:]
data.ID = types.StringValue(instanceId)
// Step 2: Create operation
operationReq := CreateOperationRequest{
InstanceUid: instanceId,
Operation: "create",
}
jsonData, err = json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err = http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err = r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Create operation failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
location = httpResp.Header.Get("Location")
if location == "" {
resp.Diagnostics.AddError("API Error", "No Location header in operation response")
return
}
operationId := location[2:]
// Step 3: Submit operation parameters and run
if err := r.submitOperationParams(ctx, operationId, data); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to submit operation parameters: %s", err))
return
}
// Step 4: Wait for operation completion and instance running status
if err := r.waitForOperationAndInstanceStatus(ctx, operationId, instanceId, 10*time.Minute); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Instance failed to reach running status: %s", err))
return
}
readData, err := r.readInstance(ctx, instanceId)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance after creation: %s", err))
return
}
data.Status = types.StringValue(readData.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *EdgeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data EdgeResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
instanceResp, err := r.readInstance(ctx, data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance: %s", err))
return
}
data.Status = types.StringValue(instanceResp.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *EdgeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data EdgeResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
operationReq := CreateOperationRequest{
InstanceUid: data.ID.ValueString(),
Operation: "modify",
}
jsonData, err := json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update instance: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Modify operation failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
location := httpResp.Header.Get("Location")
if location != "" {
operationId := location[2:]
if err := r.submitOperationParams(ctx, operationId, data); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to submit operation parameters: %s", err))
return
}
}
time.Sleep(5 * time.Second)
readData, err := r.readInstance(ctx, data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance after update: %s", err))
return
}
data.Status = types.StringValue(readData.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *EdgeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data EdgeResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
operationReq := CreateOperationRequest{
InstanceUid: data.ID.ValueString(),
Operation: "delete",
}
jsonData, err := json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete instance: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated && httpResp.StatusCode != http.StatusNotFound {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Delete operation failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
location := httpResp.Header.Get("Location")
if location != "" {
operationId := location[2:]
if err := r.submitOperationParams(ctx, operationId, data); err != nil {
resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Unable to submit operation parameters: %s", err))
}
}
}
func (r *EdgeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
func (r *EdgeResource) readInstance(ctx context.Context, id string) (*InstanceResponse, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", r.client.ApiEndpoint+"/instances/"+id, nil)
if err != nil {
return nil, err
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return nil, err
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, err
}
if httpResp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("read request failed with status %d: %s", httpResp.StatusCode, string(body))
}
var instanceResp InstanceResponse
if err := json.Unmarshal(body, &instanceResp); err != nil {
return nil, err
}
return &instanceResp, nil
}
func (r *EdgeResource) submitOperationParams(ctx context.Context, operationUid string, data EdgeResourceModel) error {
// Get operation details
httpReq, err := http.NewRequestWithContext(ctx, "GET",
r.client.ApiEndpoint+"/instanceOperations/"+operationUid+"?fields=cfsParams", nil)
if err != nil {
return fmt.Errorf("unable to create request: %s", err)
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("unable to get operation: %s", err)
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return fmt.Errorf("unable to read response: %s", err)
}
if httpResp.StatusCode != http.StatusOK {
return fmt.Errorf("get operation failed with status %d: %s", httpResp.StatusCode, string(body))
}
var getResp GetOperationResponse
if err := json.Unmarshal(body, &getResp); err != nil {
return fmt.Errorf("unable to unmarshal response: %s", err)
}
operationResp := getResp.InstanceOperation
// Submit each parameter
for _, param := range operationResp.CfsParams {
valToSend := ""
// Map Edge parameters by ID
switch param.SvcOperationCfsParamId {
case 8: // vdcUid
if !data.VdcUID.IsNull() && !data.VdcUID.IsUnknown() {
valToSend = data.VdcUID.ValueString()
}
case 340: // enableAdvanced
if !data.EnableAdvanced.IsNull() && !data.EnableAdvanced.IsUnknown() {
valToSend = fmt.Sprintf("%t", data.EnableAdvanced.ValueBool())
}
case 341: // edgeCount
if !data.EdgeCount.IsNull() && !data.EdgeCount.IsUnknown() {
valToSend = fmt.Sprintf("%d", data.EdgeCount.ValueInt64())
}
case 367: // externalNetwork
if !data.ExternalNetwork.IsNull() && !data.ExternalNetwork.IsUnknown() {
valToSend = data.ExternalNetwork.ValueString()
}
case 621: // edgeType
if !data.EdgeType.IsNull() && !data.EdgeType.IsUnknown() {
valToSend = data.EdgeType.ValueString()
}
case 622: // unknown optional parameter
// Leave empty or use default
default:
// Use existing or default value for unknown parameters
if param.ParamValue != nil {
valToSend = *param.ParamValue
} else if param.DefaultValue != nil {
valToSend = *param.DefaultValue
}
}
// Fix specific data type formatting
if valToSend == "" || valToSend == "\"\"" {
if param.DataType == "map" || param.DataType == "json" {
valToSend = "{}"
} else if param.DataType == "array" || param.DataType == "list" {
valToSend = "[]"
}
}
paramReq := CreateCfsParamRequest{
InstanceOperationUid: operationUid,
SvcOperationCfsParamId: param.SvcOperationCfsParamId,
ParamValue: valToSend,
}
jsonData, err := json.Marshal(paramReq)
if err != nil {
return fmt.Errorf("unable to marshal param request: %s", err)
}
httpReq, err = http.NewRequestWithContext(ctx, "POST",
r.client.ApiEndpoint+"/instanceOperationCfsParams", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("unable to create param request: %s", err)
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err = r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("unable to submit parameter: %s", err)
}
respBody, _ := io.ReadAll(httpResp.Body)
httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated &&
httpResp.StatusCode != http.StatusOK &&
httpResp.StatusCode != http.StatusNoContent {
return fmt.Errorf("submit parameter id %d failed with status %d: %s",
param.SvcOperationCfsParamId, httpResp.StatusCode, string(respBody))
}
}
// Run the operation
runReq, err := http.NewRequestWithContext(ctx, "POST",
r.client.ApiEndpoint+"/instanceOperations/"+operationUid+"/run", bytes.NewBuffer([]byte("{}")))
if err != nil {
return fmt.Errorf("unable to create run request: %s", err)
}
runReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
runReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
runResp, err := r.client.HttpClient.Do(runReq)
if err != nil {
return fmt.Errorf("unable to run operation: %s", err)
}
defer runResp.Body.Close()
if runResp.StatusCode != http.StatusOK &&
runResp.StatusCode != http.StatusNoContent &&
runResp.StatusCode != http.StatusCreated {
runBody, _ := io.ReadAll(runResp.Body)
return fmt.Errorf("run operation failed with status %d: %s",
runResp.StatusCode, string(runBody))
}
return nil
}
func (r *EdgeResource) waitForOperationAndInstanceStatus(ctx context.Context, operationId string, instanceId string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
log.Printf("[DEBUG] Starting two-stage polling: operationId=%s, instanceId=%s, timeout=%v", operationId, instanceId, timeout)
// Шаг 1: Ждём завершения операции
operationLoop:
for {
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled")
case <-ticker.C:
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for operation to complete")
}
log.Printf("[DEBUG] Polling operation status: operationId=%s", operationId)
// Проверяем статус операции
httpReq, err := http.NewRequestWithContext(ctx, "GET",
r.client.ApiEndpoint+"/instanceOperations/"+operationId, nil)
if err != nil {
return fmt.Errorf("failed to create request: %s", err)
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to check operation status: %s", err)
}
body, _ := io.ReadAll(httpResp.Body)
httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
return fmt.Errorf("get operation failed with status %d", httpResp.StatusCode)
}
var opResp struct {
InstanceOperation struct {
IsInProgress bool `json:"isInProgress"`
IsPending bool `json:"isPending"`
} `json:"instanceOperation"`
}
if err := json.Unmarshal(body, &opResp); err != nil {
return fmt.Errorf("failed to parse operation response: %s", err)
}
log.Printf("[DEBUG] Operation status: isInProgress=%v, isPending=%v", opResp.InstanceOperation.IsInProgress, opResp.InstanceOperation.IsPending)
// Операция завершена когда isInProgress=false И isPending=false
if !opResp.InstanceOperation.IsInProgress && !opResp.InstanceOperation.IsPending {
log.Printf("[DEBUG] Operation completed, moving to instance status check")
break operationLoop
}
}
}
// Шаг 2: Проверяем статус instance
ticker2 := time.NewTicker(5 * time.Second)
defer ticker2.Stop()
log.Printf("[DEBUG] Starting instance status polling")
for {
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled")
case <-ticker2.C:
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for instance to become running")
}
log.Printf("[DEBUG] Polling instance status: instanceId=%s", instanceId)
instance, err := r.readInstance(ctx, instanceId)
if err != nil {
return fmt.Errorf("failed to check instance status: %s", err)
}
if instance.Status == "running" {
return nil
}
if instance.Status == "error" || instance.Status == "failed" {
return fmt.Errorf("instance entered error state: %s", instance.Status)
}
// Continue waiting for other statuses
}
}
}