From 43a26cb469eb7824ed9a58bcdec7950a9ffb2220 Mon Sep 17 00:00:00 2001 From: ducvm29 Date: Wed, 31 Jul 2024 15:12:41 +0700 Subject: [PATCH] Implement muxing & add DFKE support --- fptcloud/dfke/dfke_service.go | 81 +++++ fptcloud/dfke/resource_dfke.go | 560 +++++++++++++++++++++++++++++++++ fptcloud/provider_tf6.go | 156 +++++++++ go.mod | 6 +- go.sum | 34 ++ main.go | 33 +- 6 files changed, 863 insertions(+), 7 deletions(-) create mode 100644 fptcloud/dfke/dfke_service.go create mode 100644 fptcloud/dfke/resource_dfke.go create mode 100644 fptcloud/provider_tf6.go diff --git a/fptcloud/dfke/dfke_service.go b/fptcloud/dfke/dfke_service.go new file mode 100644 index 0000000..9671b74 --- /dev/null +++ b/fptcloud/dfke/dfke_service.go @@ -0,0 +1,81 @@ +package fptcloud_dfke + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "terraform-provider-fptcloud/commons" +) + +type dfkeApiClient struct { + *commons.Client + edgeClient *commons.Client +} + +func newDfkeApiClient(c *commons.Client) (*dfkeApiClient, error) { + serviceToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U" + edgeClient, err := commons.NewClientWithURL( + serviceToken, + c.BaseURL.String(), + c.Region, + c.TenantName, + ) + + if err != nil { + return nil, err + } + + return &dfkeApiClient{ + Client: edgeClient, + edgeClient: edgeClient, + }, nil +} + +type EdgeGateway struct { + Id string `json:"id"` + VpcId string `json:"vpc_id"` + EdgeGatewayId string `json:"edge_gateway_id"` +} + +type edgeResponse struct { + EdgeGateway EdgeGateway `json:"edgeGateway"` +} + +func (a *dfkeApiClient) FindEdgeById(vpcId string, id string) (*EdgeGateway, error) { + path := fmt.Sprintf("internal/vpc/%s/find_edge_by_id/%s/false", vpcId, id) + r, err := a.internalFindEdge(path) + if err != nil { + return nil, err + } + + return r, nil +} + +func (a *dfkeApiClient) FindEdgeByEdgeGatewayId(vpcId string, edgeId string) (*EdgeGateway, error) { + if !strings.HasPrefix(edgeId, "urn:vcloud:gateway") { + return nil, errors.New("edge gateway id must be prefixed with \"urn:vcloud:gateway\"") + } + path := fmt.Sprintf("internal/vpc/%s/find_edge_by_id/%s/true", vpcId, edgeId) + r, err := a.internalFindEdge(path) + if err != nil { + return nil, err + } + + return r, nil +} + +func (a *dfkeApiClient) internalFindEdge(endpoint string) (*EdgeGateway, error) { + r, err := a.edgeClient.SendGetRequest(endpoint) + if err != nil { + return nil, err + } + + var edge edgeResponse + err = json.Unmarshal(r, &edge) + if err != nil { + return nil, err + } + + return &edge.EdgeGateway, nil +} diff --git a/fptcloud/dfke/resource_dfke.go b/fptcloud/dfke/resource_dfke.go new file mode 100644 index 0000000..d470057 --- /dev/null +++ b/fptcloud/dfke/resource_dfke.go @@ -0,0 +1,560 @@ +package fptcloud_dfke + +import ( + "context" + "encoding/json" + "fmt" + diag2 "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "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" + "github.com/hashicorp/terraform-plugin-log/tflog" + "terraform-provider-fptcloud/commons" +) + +var ( + _ resource.Resource = &resourceDedicatedKubernetesEngine{} + _ resource.ResourceWithConfigure = &resourceDedicatedKubernetesEngine{} + _ resource.ResourceWithImportState = &resourceDedicatedKubernetesEngine{} + + forceNewPlanModifiersString = []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + } + + forceNewPlanModifiersInt = []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + } +) + +type resourceDedicatedKubernetesEngine struct { + client *commons.Client + dfkeClient *dfkeApiClient +} + +func (r *resourceDedicatedKubernetesEngine) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var state dedicatedKubernetesEngine + diags := request.Plan.Get(ctx, &state) + + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + var f dedicatedKubernetesEngineJson + r.remap(&state, &f) + + f.CustomScript = "" + f.EnableCustomScript = false + f.PublicKey = "" + f.UpstreamDNS = "" + f.RegionId = "saigon-vn" + + client := r.client + a, err := client.SendPostRequest(fmt.Sprintf("xplat/fke/vpc/%s/kubernetes", state.vpcId()), f) + + if err != nil { + response.Diagnostics.Append(diag2.NewErrorDiagnostic("Error calling API", err.Error())) + return + } + + errorResponse := r.checkForError(a) + if errorResponse != nil { + response.Diagnostics.Append(errorResponse) + return + } + + var createResponse dedicatedKubernetesEngineCreateResponse + if err = json.Unmarshal(a, &createResponse); err != nil { + response.Diagnostics.Append(diag2.NewErrorDiagnostic("Error unmarshalling response", err.Error())) + return + } + + tflog.Info(ctx, "Created cluster with id "+createResponse.Cluster.ID) + + if err = r.internalRead(ctx, createResponse.Cluster.ID, &state); err != nil { + response.Diagnostics.Append(diag2.NewErrorDiagnostic("Error reading cluster state", err.Error())) + return + } + + diags = response.State.Set(ctx, &state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } +} + +func (r *resourceDedicatedKubernetesEngine) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var state dedicatedKubernetesEngine + diags := request.State.Get(ctx, &state) + + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + err := r.internalRead(ctx, state.Id.ValueString(), &state) + if err != nil { + response.Diagnostics.Append(diag2.NewErrorDiagnostic("Error calling API", err.Error())) + return + } + + diags = response.State.Set(ctx, &state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } +} + +func (r *resourceDedicatedKubernetesEngine) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + //TODO implement me + panic("implement me") +} + +func (r *resourceDedicatedKubernetesEngine) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var state dedicatedKubernetesEngine + diags := request.State.Get(ctx, &state) + + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + _, err := r.client.SendDeleteRequest(fmt.Sprintf("xplat/fke/vpc/%s/cluster/%s/delete", state.vpcId(), state.Id)) + if err != nil { + response.Diagnostics.Append(diag2.NewErrorDiagnostic("Error calling API", err.Error())) + return + } +} + +func (r *resourceDedicatedKubernetesEngine) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + tflog.Info(ctx, "Importing cluster ID "+request.ID) + var state dedicatedKubernetesEngine + + state.Id = types.StringValue(request.ID) + err := r.internalRead(ctx, request.ID, &state) + if err != nil { + response.Diagnostics.Append(diag2.NewErrorDiagnostic("Error calling API", err.Error())) + return + } + + diags := response.State.Set(ctx, &state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } +} + +func NewResourceDedicatedKubernetesEngine() resource.Resource { + return &resourceDedicatedKubernetesEngine{} +} + +func (r *resourceDedicatedKubernetesEngine) Metadata(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = request.ProviderTypeName + "_dedicated_kubernetes_engine_v1" +} + +func (r *resourceDedicatedKubernetesEngine) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Description: "Manage dedicated FKE clusters.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "cluster_id": schema.StringAttribute{ + Computed: true, + }, + "cluster_name": schema.StringAttribute{ + Required: true, + }, + "k8s_version": schema.StringAttribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersString, + }, + "master_type": schema.StringAttribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersString, + }, + "master_count": schema.Int64Attribute{ + Required: true, + }, + "master_disk_size": schema.Int64Attribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersInt, + }, + "worker_type": schema.StringAttribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersString, + }, + "worker_disk_size": schema.Int64Attribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersInt, + }, + "network_id": schema.StringAttribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersString, + }, + "lb_size": schema.StringAttribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersString, + }, + "pod_network": schema.StringAttribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersString, + }, + "service_network": schema.StringAttribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersString, + }, + "network_node_prefix": schema.Int64Attribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersInt, + }, + "max_pod_per_node": schema.Int64Attribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersInt, + }, + "nfs_status": schema.StringAttribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersString, + }, + "nfs_disk_size": schema.Int64Attribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersInt, + }, + //"public_key": schema.StringAttribute{ + // Required:true, + // PlanModifiers: forceNewPlanModifiersString, + //}, + "storage_policy": schema.StringAttribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersString, + }, + "edge_id": schema.StringAttribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersString, + }, + //"upstream_dns": schema.Int64Attribute{ + // Required:true, + // PlanModifiers: forceNewPlanModifiersInt, + //}, + "scale_min": schema.Int64Attribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersInt, + }, + "scale_max": schema.Int64Attribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersInt, + }, + "node_dns": schema.StringAttribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersString, + }, + "ip_public_firewall": schema.StringAttribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersString, + }, + "ip_private_firewall": schema.StringAttribute{ + Required: true, + PlanModifiers: forceNewPlanModifiersString, + }, + }, + } +} + +func (r *resourceDedicatedKubernetesEngine) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { + if request.ProviderData == nil { + return + } + + client, ok := request.ProviderData.(*commons.Client) + if !ok { + response.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *commons.Client, got: %T. Please report this issue to the provider developers.", request.ProviderData), + ) + + return + } + + r.client = client + a, err := newDfkeApiClient(client) + if err != nil { + response.Diagnostics.AddError( + "Error configuring API client", + fmt.Sprintf("%s", err.Error()), + ) + + return + } + + r.dfkeClient = a +} + +func (r *resourceDedicatedKubernetesEngine) internalRead(ctx context.Context, clusterId string, state *dedicatedKubernetesEngine) error { + vpcId := state.VpcId.ValueString() + tflog.Info(ctx, "Reading state of cluster ID "+clusterId+", VPC ID "+vpcId) + + a, err := r.client.SendGetRequest(fmt.Sprintf("xplat/fke/vpc/%s/cluster/%s?page=1&page_size=25", vpcId, clusterId)) + + if err != nil { + return err + } + + var d dedicatedKubernetesEngineReadResponse + err = json.Unmarshal(a, &d) + data := d.Cluster + if err != nil { + return err + } + + var awx dedicatedKubernetesEngineParams + err = json.Unmarshal([]byte(d.Cluster.AwxParams), &awx) + + if err != nil { + return err + } + + // resolve edge ID + edge, err := r.dfkeClient.FindEdgeByEdgeGatewayId(vpcId, data.EdgeID) + if err != nil { + return err + } + + state.ClusterId = types.StringValue(data.ClusterID) + state.ClusterName = types.StringValue(data.Name) + state.Version = types.StringValue(awx.K8SVersion) + state.MasterType = types.StringValue(awx.MasterType) + state.MasterCount = types.Int64Value(int64(awx.MasterCount)) + state.MasterDiskSize = types.Int64Value(int64(awx.MasterDiskSize)) + state.WorkerType = types.StringValue(awx.WorkerType) + state.WorkerDiskSize = types.Int64Value(int64(awx.WorkerDiskSize)) + state.NetworkID = types.StringValue(data.NetworkID) + state.LbSize = types.StringValue(awx.LbSize) + state.PodNetwork = types.StringValue(awx.PodNetwork + "/" + awx.PodPrefix) + state.ServiceNetwork = types.StringValue(awx.ServiceNetwork + "/" + awx.ServicePrefix) + state.NetworkNodePrefix = types.Int64Value(int64(awx.NetworkNodePrefix)) + state.MaxPodPerNode = types.Int64Value(int64(awx.K8SMaxPod)) + state.NfsStatus = types.StringValue(awx.NfsStatus) + state.NfsDiskSize = types.Int64Value(int64(awx.NfsDiskSize)) + state.StoragePolicy = types.StringValue(awx.StorageProfile) + state.EdgeID = types.StringValue(edge.Id) + state.ScaleMin = types.Int64Value(int64(awx.ScaleMinSize)) + state.ScaleMax = types.Int64Value(int64(awx.ScaleMaxSize)) + state.NodeDNS = types.StringValue(awx.NodeDNS) + state.IPPublicFirewall = types.StringValue(awx.IPPublicFirewall) + state.IPPrivateFirewall = types.StringValue(awx.IPPrivateFirewall) + state.VpcId = types.StringValue(data.VpcID) + //state.CustomScript = awx.CustomScript + //state.EnableCustomScript = awx.EnableCustomScript + + return nil +} + +func (r *resourceDedicatedKubernetesEngine) checkForError(a []byte) *diag2.ErrorDiagnostic { + var re map[string]interface{} + err := json.Unmarshal(a, &re) + if err != nil { + res := diag2.NewErrorDiagnostic("Error unmarshalling response", err.Error()) + return &res + } + + if _, ok := re["error"]; ok { + res := diag2.NewErrorDiagnostic("Response contained an error field", "Response body was "+string(a)) + return &res + } + + return nil +} + +func (r *resourceDedicatedKubernetesEngine) remap(from *dedicatedKubernetesEngine, to *dedicatedKubernetesEngineJson) { + to.ClusterName = from.ClusterName.ValueString() + to.ClusterId = from.ClusterId.ValueString() + to.Id = from.Id.ValueString() + to.Version = from.Version.ValueString() + to.MasterType = from.MasterType.ValueString() + to.MasterCount = from.MasterCount.ValueInt64() + to.MasterDiskSize = from.MasterDiskSize.ValueInt64() + to.WorkerType = from.WorkerType.ValueString() + to.WorkerDiskSize = from.WorkerDiskSize.ValueInt64() + to.NetworkID = from.NetworkID.ValueString() + to.LbSize = from.LbSize.ValueString() + to.PodNetwork = from.PodNetwork.ValueString() + to.ServiceNetwork = from.ServiceNetwork.ValueString() + to.NetworkNodePrefix = from.NetworkNodePrefix.ValueInt64() + to.MaxPodPerNode = from.MaxPodPerNode.ValueInt64() + to.NfsStatus = from.NfsStatus.ValueString() + to.NfsDiskSize = from.NfsDiskSize.ValueInt64() + to.StoragePolicy = from.StoragePolicy.ValueString() + to.EdgeID = from.EdgeID.ValueString() + to.ScaleMin = from.ScaleMin.ValueInt64() + to.ScaleMax = from.ScaleMax.ValueInt64() + to.NodeDNS = from.NodeDNS.ValueString() + to.IPPublicFirewall = from.IPPublicFirewall.ValueString() + to.IPPrivateFirewall = from.IPPrivateFirewall.ValueString() +} + +func (e *dedicatedKubernetesEngine) vpcId() string { + return e.VpcId.ValueString() +} + +type dedicatedKubernetesEngine struct { + ClusterName types.String `tfsdk:"cluster_name" json:"cluster_name"` + ClusterId types.String `tfsdk:"cluster_id" json:"cluster_id,omitempty"` + Id types.String `tfsdk:"id" json:"id"` + Version types.String `tfsdk:"k8s_version" json:"k8s_version"` + MasterType types.String `tfsdk:"master_type"` // tfsdk:"master_type" + MasterCount types.Int64 `tfsdk:"master_count" json:"master_count"` + MasterDiskSize types.Int64 `tfsdk:"master_disk_size" json:"master_disk_size"` + WorkerType types.String `tfsdk:"worker_type" json:"worker_type"` + WorkerDiskSize types.Int64 `tfsdk:"worker_disk_size" json:"worker_disk_size"` + NetworkID types.String `tfsdk:"network_id" json:"network_id"` + LbSize types.String `tfsdk:"lb_size" json:"lb_size"` + + PodNetwork types.String `tfsdk:"pod_network" json:"pod_network"` + ServiceNetwork types.String `tfsdk:"service_network" json:"service_network"` + + NetworkNodePrefix types.Int64 `tfsdk:"network_node_prefix" json:"network_node_prefix"` + + MaxPodPerNode types.Int64 `tfsdk:"max_pod_per_node" json:"max_pod_per_node"` + NfsStatus types.String `tfsdk:"nfs_status" json:"nfs_status"` + NfsDiskSize types.Int64 `tfsdk:"nfs_disk_size" json:"nfs_disk_size"` + + StoragePolicy types.String `tfsdk:"storage_policy" json:"storage_policy"` + EdgeID types.String `tfsdk:"edge_id"` + + ScaleMin types.Int64 `tfsdk:"scale_min" json:"scale_min"` + ScaleMax types.Int64 `tfsdk:"scale_max" json:"scale_max"` + + NodeDNS types.String `tfsdk:"node_dns" json:"node_dns"` + IPPublicFirewall types.String `tfsdk:"ip_public_firewall" json:"ip_public_firewall"` + IPPrivateFirewall types.String `tfsdk:"ip_private_firewall" json:"ip_private_firewall"` + VpcId types.String `tfsdk:"vpc_id" json:"vpc_id"` +} + +type dedicatedKubernetesEngineJson struct { + ClusterName string `json:"cluster_name"` + ClusterId string `json:"cluster_id,omitempty"` + Id string `json:"id,omitempty"` + Version string `json:"k8s_version"` + IpPublic string `json:"ip_public"` + MasterType string `json:"master_type"` + MasterCount int64 `json:"master_count"` + MasterDiskSize int64 `json:"master_disk_size"` + WorkerType string `json:"worker_type"` + WorkerDiskSize int64 `json:"worker_disk_size"` + NetworkID string `json:"network_id"` + LbSize string `json:"lb_size"` + + PodNetwork string `json:"pod_network"` + ServiceNetwork string `json:"service_network"` + + NetworkNodePrefix int64 `json:"network_node_prefix"` + + MaxPodPerNode int64 `json:"max_pod_per_node"` + NfsStatus string `json:"nfs_status"` + NfsDiskSize int64 `json:"nfs_disk_size"` + + StoragePolicy string `json:"storage_policy"` + EdgeID string `json:"edge_id"` + + ScaleMin int64 `json:"scale_min"` + ScaleMax int64 `json:"scale_max"` + + NodeDNS string `json:"node_dns"` + IPPublicFirewall string `json:"ip_public_firewall"` + IPPrivateFirewall string `json:"ip_private_firewall"` + + CustomScript string `json:"custom_script"` + EnableCustomScript bool `json:"enable_custom_script"` + PublicKey string `json:"public_key"` + UpstreamDNS string `json:"upstream_dns"` + + RegionId string `json:"region_id"` +} + +type dedicatedKubernetesEngineData struct { + ID string `json:"id"` + ClusterID string `json:"cluster_id"` + VpcID string `json:"vpc_id"` + EdgeID string `json:"edge_id"` + Name string `json:"name"` + AwxParams string `json:"awx_params"` + Status string `json:"status"` + NetworkID string `json:"network_id"` + NfsDiskSize int `json:"nfs_disk_size"` + NfsStatus string `json:"nfs_status"` + ErrorMessage interface{} `json:"error_message"` + IsRunning bool `json:"is_running"` + AutoScale string `json:"auto_scale"` + ScaleMin int `json:"scale_min"` + ScaleMax int `json:"scale_max"` + Templates string `json:"templates"` + NetworkName string `json:"network_name"` +} + +type dedicatedKubernetesEngineParams struct { + VcdURL string `json:"vcd_url"` + PublicDomain string `json:"public_domain"` + ClusterID string `json:"cluster_id"` + ClusterName string `json:"cluster_name"` + OrgName string `json:"org_name"` + VdcName string `json:"vdc_name"` + MasterType string `json:"master_type"` + MasterOs string `json:"master_os"` + MasterCPU int `json:"master_cpu"` + MasterRAM int `json:"master_ram"` + MasterCount int `json:"master_count"` + MasterDiskSize int `json:"master_disk_size"` + WorkerOs string `json:"worker_os"` + WorkerCPU int `json:"worker_cpu"` + WorkerRAM int `json:"worker_ram"` + WorkerCount int `json:"worker_count"` + WorkerType string `json:"worker_type"` + WorkerDiskSize int `json:"worker_disk_size"` + VMPass string `json:"vm_pass"` + StorageProfile string `json:"storage_profile"` + VMNetwork string `json:"vm_network"` + EdgeGatewayID string `json:"edge_gateway_id"` + K8SVersion string `json:"k8s_version"` + PodNetwork string `json:"pod_network"` + PodPrefix string `json:"pod_prefix"` + ServiceNetwork string `json:"service_network"` + ServicePrefix string `json:"service_prefix"` + NetworkNodePrefix int `json:"network_node_prefix"` + K8SMaxPod int `json:"k8s_max_pod"` + IPPublic string `json:"ip_public"` + IDServiceEngineGroup string `json:"id_service_engine_group"` + VirtualIPAddress interface{} `json:"virtual_ip_address"` + NfsStatus string `json:"nfs_status"` + NfsDiskSize int `json:"nfs_disk_size"` + LbSize string `json:"lb_size"` + DashboardLink string `json:"dashboard_link"` + APILink string `json:"api_link"` + UserName string `json:"user_name"` + AwxJobType string `json:"awx_job_type"` + AutoScaleStatus string `json:"auto_scale_status"` + ScaleMinSize int `json:"scale_min_size"` + ScaleMaxSize int `json:"scale_max_size"` + VpcID string `json:"vpc_id"` + NodeDNS string `json:"node_dns"` + CallbackURL string `json:"callback_url"` + CallbackAction string `json:"callback_action"` + AccessToken string `json:"access_token"` + IPPublicFirewall string `json:"ip_public_firewall"` + IPPrivateFirewall string `json:"ip_private_firewall"` + CustomScript string `json:"custom_script"` + EnableCustomScript bool `json:"enable_custom_script"` + VcdProvider string `json:"vcd_provider"` + VcdPod string `json:"vcd_pod"` + RequestUserID string `json:"request_user_id"` +} + +type dedicatedKubernetesEngineCreateResponse struct { + Cluster dedicatedKubernetesEngineData `json:"cluster"` +} + +type dedicatedKubernetesEngineReadResponse struct { + Cluster dedicatedKubernetesEngineData `json:"cluster"` +} diff --git a/fptcloud/provider_tf6.go b/fptcloud/provider_tf6.go new file mode 100644 index 0000000..f1081a5 --- /dev/null +++ b/fptcloud/provider_tf6.go @@ -0,0 +1,156 @@ +package fptcloud + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "os" + common "terraform-provider-fptcloud/commons" + fptcloud_dfke "terraform-provider-fptcloud/fptcloud/dfke" +) + +var ( + _ provider.Provider = &xplatProvider{} +) + +type xplatProviderModel struct { + Region types.String `tfsdk:"region"` + Token types.String `tfsdk:"token"` + TenantName types.String `tfsdk:"tenant_name"` + ApiEndpoint types.String `tfsdk:"api_endpoint"` +} + +type xplatProvider struct { + // version is set to the provider version on release, "dev" when the + // provider is built and ran locally, and "test" when running acceptance + // testing. + version string +} + +func NewXplatProvider(version string) func() provider.Provider { + return func() provider.Provider { + return &xplatProvider{ + version: version, + } + } +} + +func (x *xplatProvider) Metadata(ctx context.Context, request provider.MetadataRequest, response *provider.MetadataResponse) { + response.TypeName = "fptcloud" + response.Version = x.version +} + +func (x *xplatProvider) Schema(ctx context.Context, request provider.SchemaRequest, response *provider.SchemaResponse) { + response.Schema = schema.Schema{ + Description: "Resources provider from xPlat", + Attributes: map[string]schema.Attribute{ + "region": schema.StringAttribute{ + Description: "The FPTCloud region to connect to.", + Optional: true, + }, + + "token": schema.StringAttribute{ + Description: "Authentication token to use as an alternative to username/password.", + Optional: true, + }, + + "tenant_name": schema.StringAttribute{ + Description: "Tenant name", + Optional: true, + }, + + "api_endpoint": schema.StringAttribute{ + Description: "The URL to use", + Optional: true, + }, + }, + } +} + +func (x *xplatProvider) Configure(ctx context.Context, request provider.ConfigureRequest, response *provider.ConfigureResponse) { + tflog.Info(ctx, "Configuring FPTCloud client") + var config xplatProviderModel + + diags := request.Config.Get(ctx, &config) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + token := os.Getenv("FPTCLOUD_TOKEN") + region := os.Getenv("FPTCLOUD_REGION") + tenant_name := os.Getenv("FPTCLOUD_TENANT_NAME") + api_endpoint := os.Getenv("FPTCLOUD_API_URL") + + if !config.Token.IsNull() { + token = config.Token.ValueString() + } + + if !config.Region.IsNull() { + region = config.Region.ValueString() + } + + if !config.TenantName.IsNull() { + tenant_name = config.TenantName.ValueString() + } + + if !config.ApiEndpoint.IsNull() { + api_endpoint = config.ApiEndpoint.ValueString() + } + + if api_endpoint == "" { + api_endpoint = ProdAPI + } + + if token == "" { + response.Diagnostics.AddAttributeError( + path.Root("token"), + "Missing token", + "Token must be specified to authenticate to provision resources", + ) + } + + if response.Diagnostics.HasError() { + return + } + + ctx = tflog.SetField(ctx, "token", token) + ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "token") + tflog.Debug(ctx, "Creating FPTCloud client") + + client, err := common.NewClientWithURL(token, api_endpoint, region, tenant_name) + + if err != nil { + response.Diagnostics.AddError("Error creating client", err.Error()) + return + } + + userAgent := &common.Component{ + Name: "terraform-provider-fptcloud", + Version: ProviderVersion, + } + client.SetUserAgent(userAgent) + + response.DataSourceData = client + response.ResourceData = client + + tflog.Info(ctx, "Configured FPTCloud client", map[string]any{ + "success": true, + "api_endpoint": api_endpoint, + "tenant_name": tenant_name, + }) +} + +func (x *xplatProvider) DataSources(ctx context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{} +} + +func (x *xplatProvider) Resources(ctx context.Context) []func() resource.Resource { + return []func() resource.Resource{ + fptcloud_dfke.NewResourceDedicatedKubernetesEngine, + } +} diff --git a/go.mod b/go.mod index 4945419..10f824d 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,10 @@ module terraform-provider-fptcloud go 1.21 require ( + github.com/hashicorp/terraform-plugin-framework v1.10.0 + github.com/hashicorp/terraform-plugin-go v0.23.0 + github.com/hashicorp/terraform-plugin-log v0.9.0 + github.com/hashicorp/terraform-plugin-mux v0.16.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 github.com/stretchr/testify v1.8.2 ) @@ -30,8 +34,6 @@ require ( github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.20.0 // indirect github.com/hashicorp/terraform-json v0.21.0 // indirect - github.com/hashicorp/terraform-plugin-go v0.23.0 // indirect - github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect diff --git a/go.sum b/go.sum index 30796d2..f61789a 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ProtonMail/go-crypto v1.1.0-alpha.0 h1:nHGfwXmFvJrSR9xu8qL7BkO4DqTHXE9N5vPhgY2I+j0= github.com/ProtonMail/go-crypto v1.1.0-alpha.0/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= @@ -10,14 +14,26 @@ github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= +github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -57,10 +73,14 @@ github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8J github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw= github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRyRNd+zTI05U= github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk= +github.com/hashicorp/terraform-plugin-framework v1.10.0 h1:xXhICE2Fns1RYZxEQebwkB2+kXouLC932Li9qelozrc= +github.com/hashicorp/terraform-plugin-framework v1.10.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM= github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= +github.com/hashicorp/terraform-plugin-mux v0.16.0 h1:RCzXHGDYwUwwqfYYWJKBFaS3fQsWn/ZECEiW7p2023I= +github.com/hashicorp/terraform-plugin-mux v0.16.0/go.mod h1:PF79mAsPc8CpusXPfEVa4X8PtkB+ngWoiUClMrNZlYo= github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 h1:qHprzXy/As0rxedphECBEQAh3R4yp6pKksKHcqZx5G8= github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0/go.mod h1:H+8tjs9TjV2w57QFVSMBQacf8k/E1XwLXGCARgViC6A= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= @@ -69,8 +89,12 @@ github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= @@ -99,11 +123,17 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= +github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -119,6 +149,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= @@ -187,6 +219,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index af017c8..22bf931 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,18 @@ package main import ( + "context" "flag" - "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5/tf5server" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" + "log" "terraform-provider-fptcloud/fptcloud" ) func main() { + ctx := context.Background() var debugMode bool = true flag.BoolVar( @@ -17,12 +23,29 @@ func main() { ) flag.Parse() - opts := &plugin.ServeOpts{ProviderFunc: fptcloud.Provider} + providers := []func() tfprotov5.ProviderServer{ + providerserver.NewProtocol5(fptcloud.NewXplatProvider("dev")()), + fptcloud.Provider().GRPCProvider, + } + + muxServer, err := tf5muxserver.NewMuxServer(ctx, providers...) + if err != nil { + log.Fatal(err) + } + + var serveOpts []tf5server.ServeOpt if debugMode { - opts.Debug = true - opts.ProviderAddr = "github.com/terraform-providers/fptcloud" + serveOpts = append(serveOpts, tf5server.WithManagedDebug()) } - plugin.Serve(opts) + err = tf5server.Serve( + "github.com/terraform-providers/fptcloud", + muxServer.ProviderServer, + serveOpts..., + ) + + if err != nil { + log.Fatal(err) + } }