Overhaul VirtFusion provider: 20 resources, 30 data sources, multipage pagination

Complete rewrite of the VirtFusion Terraform provider with full API coverage:

- 20 managed resources (server, build, SSH key, user, firewall, IP blocks, etc.)
- 30 data sources (hypervisors, packages, servers, IP blocks, self-service, etc.)
- New HTTP client with proper error handling, query parameter support, and
  automatic multipage pagination via GetAllPages (fetches all pages from
  Laravel-style paginated endpoints and merges into a single response)
- Fixed type mismatches against live API: ServerData.Suspended (int→bool),
  IPBlockData.Type (string→int), PackageData json tags (primaryStorage, etc.),
  ServerData nested CPU/Settings/Resources structure, HypervisorGroupResources
  array response
- Configurable results-per-page (default 300) on all list data sources
- Migrated CI from GitHub Actions to Gitea Actions
- Updated goreleaser config, go.mod dependencies, and examples

Verified against live VirtFusion instance at cp.vps.ezscale.tech:
all data sources return correct data with full pagination support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 02:01:16 -04:00
parent a3a16f46fa
commit 6b7430b67b
92 changed files with 18443 additions and 1488 deletions

View File

@@ -0,0 +1,105 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &DNSServiceDataSource{}
_ datasource.DataSourceWithConfigure = &DNSServiceDataSource{}
)
// NewDNSServiceDataSource returns a new DNS service data source.
func NewDNSServiceDataSource() datasource.DataSource {
return &DNSServiceDataSource{}
}
// DNSServiceDataSource defines the data source implementation.
type DNSServiceDataSource struct {
client *client.Client
}
// DNSServiceDataSourceModel describes the data source data model.
type DNSServiceDataSourceModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Type types.String `tfsdk:"type"`
}
func (d *DNSServiceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_dns_service"
}
func (d *DNSServiceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches a single VirtFusion DNS service by ID.",
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The DNS service ID.",
Required: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The DNS service name.",
Computed: true,
},
"type": schema.StringAttribute{
MarkdownDescription: "The DNS service type.",
Computed: true,
},
},
}
}
func (d *DNSServiceDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *DNSServiceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data DNSServiceDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/dns/services/%d", data.ID.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError("Error Reading DNS Service", err.Error())
return
}
var dnsResp client.DNSServiceResponse
if err := json.Unmarshal(rawResp, &dnsResp); err != nil {
resp.Diagnostics.AddError("Error Parsing DNS Service Response", err.Error())
return
}
data.ID = types.Int64Value(dnsResp.Data.ID)
data.Name = types.StringValue(dnsResp.Data.Name)
data.Type = types.StringValue(dnsResp.Data.Type)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,117 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &HypervisorDataSource{}
_ datasource.DataSourceWithConfigure = &HypervisorDataSource{}
)
// NewHypervisorDataSource returns a new hypervisor data source.
func NewHypervisorDataSource() datasource.DataSource {
return &HypervisorDataSource{}
}
// HypervisorDataSource defines the data source implementation.
type HypervisorDataSource struct {
client *client.Client
}
// HypervisorDataSourceModel describes the data source data model.
type HypervisorDataSourceModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Type types.String `tfsdk:"type"`
Hostname types.String `tfsdk:"hostname"`
Enabled types.Bool `tfsdk:"enabled"`
}
func (d *HypervisorDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_hypervisor"
}
func (d *HypervisorDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches a single VirtFusion hypervisor by ID.",
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The hypervisor ID.",
Required: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The hypervisor name.",
Computed: true,
},
"type": schema.StringAttribute{
MarkdownDescription: "The hypervisor type.",
Computed: true,
},
"hostname": schema.StringAttribute{
MarkdownDescription: "The hypervisor hostname.",
Computed: true,
},
"enabled": schema.BoolAttribute{
MarkdownDescription: "Whether the hypervisor is enabled.",
Computed: true,
},
},
}
}
func (d *HypervisorDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *HypervisorDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data HypervisorDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/compute/hypervisors/%d", data.ID.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError("Error Reading Hypervisor", err.Error())
return
}
var hypervisorResp client.HypervisorResponse
if err := json.Unmarshal(rawResp, &hypervisorResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Hypervisor Response", err.Error())
return
}
data.ID = types.Int64Value(hypervisorResp.Data.ID)
data.Name = types.StringValue(hypervisorResp.Data.Name)
data.Type = types.StringValue(hypervisorResp.Data.Type)
data.Hostname = types.StringValue(hypervisorResp.Data.Hostname)
data.Enabled = types.BoolValue(hypervisorResp.Data.Enabled)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,105 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &HypervisorGroupDataSource{}
_ datasource.DataSourceWithConfigure = &HypervisorGroupDataSource{}
)
// NewHypervisorGroupDataSource returns a new hypervisor group data source.
func NewHypervisorGroupDataSource() datasource.DataSource {
return &HypervisorGroupDataSource{}
}
// HypervisorGroupDataSource defines the data source implementation.
type HypervisorGroupDataSource struct {
client *client.Client
}
// HypervisorGroupDataSourceModel describes the data source data model.
type HypervisorGroupDataSourceModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Enabled types.Bool `tfsdk:"enabled"`
}
func (d *HypervisorGroupDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_hypervisor_group"
}
func (d *HypervisorGroupDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches a single VirtFusion hypervisor group by ID.",
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The hypervisor group ID.",
Required: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The hypervisor group name.",
Computed: true,
},
"enabled": schema.BoolAttribute{
MarkdownDescription: "Whether the hypervisor group is enabled.",
Computed: true,
},
},
}
}
func (d *HypervisorGroupDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *HypervisorGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data HypervisorGroupDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/compute/hypervisors/groups/%d", data.ID.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError("Error Reading Hypervisor Group", err.Error())
return
}
var groupResp client.HypervisorGroupResponse
if err := json.Unmarshal(rawResp, &groupResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Hypervisor Group Response", err.Error())
return
}
data.ID = types.Int64Value(groupResp.Data.ID)
data.Name = types.StringValue(groupResp.Data.Name)
data.Enabled = types.BoolValue(groupResp.Data.Enabled)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,119 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &HypervisorGroupResourcesDataSource{}
_ datasource.DataSourceWithConfigure = &HypervisorGroupResourcesDataSource{}
)
// NewHypervisorGroupResourcesDataSource returns a new hypervisor group resources data source.
func NewHypervisorGroupResourcesDataSource() datasource.DataSource {
return &HypervisorGroupResourcesDataSource{}
}
// HypervisorGroupResourcesDataSource defines the data source implementation.
type HypervisorGroupResourcesDataSource struct {
client *client.Client
}
// HypervisorGroupResourcesDataSourceModel describes the data source data model.
type HypervisorGroupResourcesDataSourceModel struct {
ID types.Int64 `tfsdk:"id"`
Results types.Int64 `tfsdk:"results"`
CPUCores types.Int64 `tfsdk:"cpu_cores"`
Memory types.Int64 `tfsdk:"memory"`
Storage types.Int64 `tfsdk:"storage"`
}
func (d *HypervisorGroupResourcesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_hypervisor_group_resources"
}
func (d *HypervisorGroupResourcesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches resource information for a VirtFusion hypervisor group.",
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The hypervisor group ID.",
Required: true,
},
"results": resultsSchemaAttribute(),
"cpu_cores": schema.Int64Attribute{
MarkdownDescription: "The number of CPU cores available in the group.",
Computed: true,
},
"memory": schema.Int64Attribute{
MarkdownDescription: "The amount of memory available in the group.",
Computed: true,
},
"storage": schema.Int64Attribute{
MarkdownDescription: "The amount of storage available in the group.",
Computed: true,
},
},
}
}
func (d *HypervisorGroupResourcesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *HypervisorGroupResourcesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data HypervisorGroupResourcesDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/compute/hypervisors/groups/%d/resources?%s", data.ID.ValueInt64(), resultsQueryParam(data.Results)))
if err != nil {
resp.Diagnostics.AddError("Error Reading Hypervisor Group Resources", err.Error())
return
}
var resourcesResp client.HypervisorGroupResourcesResponse
if err := json.Unmarshal(rawResp, &resourcesResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Hypervisor Group Resources Response", err.Error())
return
}
// Aggregate resource totals across all hypervisors in the group.
var totalCPU, totalMemory, totalStorage int64
for _, entry := range resourcesResp.Data {
totalCPU += entry.Resources.CPUCores.Max
totalMemory += entry.Resources.Memory.Max
totalStorage += entry.Resources.LocalStorage.Max
}
data.CPUCores = types.Int64Value(totalCPU)
data.Memory = types.Int64Value(totalMemory)
data.Storage = types.Int64Value(totalStorage)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,121 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &HypervisorGroupsDataSource{}
_ datasource.DataSourceWithConfigure = &HypervisorGroupsDataSource{}
)
// NewHypervisorGroupsDataSource returns a new hypervisor groups data source.
func NewHypervisorGroupsDataSource() datasource.DataSource {
return &HypervisorGroupsDataSource{}
}
// HypervisorGroupsDataSource defines the data source implementation.
type HypervisorGroupsDataSource struct {
client *client.Client
}
// HypervisorGroupsDataSourceModel describes the data source data model.
type HypervisorGroupsDataSourceModel struct {
Results types.Int64 `tfsdk:"results"`
Groups []HypervisorGroupItemModel `tfsdk:"groups"`
}
// HypervisorGroupItemModel describes a single hypervisor group in the list.
type HypervisorGroupItemModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Enabled types.Bool `tfsdk:"enabled"`
}
func (d *HypervisorGroupsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_hypervisor_groups"
}
func (d *HypervisorGroupsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches all VirtFusion hypervisor groups.",
Attributes: map[string]schema.Attribute{
"results": resultsSchemaAttribute(),
"groups": schema.ListNestedAttribute{
MarkdownDescription: "List of hypervisor groups.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The hypervisor group ID.",
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The hypervisor group name.",
Computed: true,
},
"enabled": schema.BoolAttribute{
MarkdownDescription: "Whether the hypervisor group is enabled.",
Computed: true,
},
},
},
},
},
}
}
func (d *HypervisorGroupsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *HypervisorGroupsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data HypervisorGroupsDataSourceModel
rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/compute/hypervisors/groups?%s", resultsQueryParam(data.Results)))
if err != nil {
resp.Diagnostics.AddError("Error Reading Hypervisor Groups", err.Error())
return
}
var listResp client.HypervisorGroupListResponse
if err := json.Unmarshal(rawResp, &listResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Hypervisor Groups Response", err.Error())
return
}
data.Groups = make([]HypervisorGroupItemModel, len(listResp.Data))
for i, g := range listResp.Data {
data.Groups[i] = HypervisorGroupItemModel{
ID: types.Int64Value(g.ID),
Name: types.StringValue(g.Name),
Enabled: types.BoolValue(g.Enabled),
}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,133 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &HypervisorsDataSource{}
_ datasource.DataSourceWithConfigure = &HypervisorsDataSource{}
)
// NewHypervisorsDataSource returns a new hypervisors data source.
func NewHypervisorsDataSource() datasource.DataSource {
return &HypervisorsDataSource{}
}
// HypervisorsDataSource defines the data source implementation.
type HypervisorsDataSource struct {
client *client.Client
}
// HypervisorsDataSourceModel describes the data source data model.
type HypervisorsDataSourceModel struct {
Results types.Int64 `tfsdk:"results"`
Hypervisors []HypervisorItemModel `tfsdk:"hypervisors"`
}
// HypervisorItemModel describes a single hypervisor in the list.
type HypervisorItemModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Type types.String `tfsdk:"type"`
Hostname types.String `tfsdk:"hostname"`
Enabled types.Bool `tfsdk:"enabled"`
}
func (d *HypervisorsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_hypervisors"
}
func (d *HypervisorsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches all VirtFusion hypervisors.",
Attributes: map[string]schema.Attribute{
"results": resultsSchemaAttribute(),
"hypervisors": schema.ListNestedAttribute{
MarkdownDescription: "List of hypervisors.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The hypervisor ID.",
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The hypervisor name.",
Computed: true,
},
"type": schema.StringAttribute{
MarkdownDescription: "The hypervisor type.",
Computed: true,
},
"hostname": schema.StringAttribute{
MarkdownDescription: "The hypervisor hostname.",
Computed: true,
},
"enabled": schema.BoolAttribute{
MarkdownDescription: "Whether the hypervisor is enabled.",
Computed: true,
},
},
},
},
},
}
}
func (d *HypervisorsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *HypervisorsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data HypervisorsDataSourceModel
rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/compute/hypervisors?%s", resultsQueryParam(data.Results)))
if err != nil {
resp.Diagnostics.AddError("Error Reading Hypervisors", err.Error())
return
}
var listResp client.HypervisorListResponse
if err := json.Unmarshal(rawResp, &listResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Hypervisors Response", err.Error())
return
}
data.Hypervisors = make([]HypervisorItemModel, len(listResp.Data))
for i, h := range listResp.Data {
data.Hypervisors[i] = HypervisorItemModel{
ID: types.Int64Value(h.ID),
Name: types.StringValue(h.Name),
Type: types.StringValue(h.Type),
Hostname: types.StringValue(h.Hostname),
Enabled: types.BoolValue(h.Enabled),
}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,123 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &IPBlockDataSource{}
_ datasource.DataSourceWithConfigure = &IPBlockDataSource{}
)
// NewIPBlockDataSource returns a new IP block data source.
func NewIPBlockDataSource() datasource.DataSource {
return &IPBlockDataSource{}
}
// IPBlockDataSource defines the data source implementation.
type IPBlockDataSource struct {
client *client.Client
}
// IPBlockDataSourceModel describes the data source data model.
type IPBlockDataSourceModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Type types.Int64 `tfsdk:"type"`
Gateway types.String `tfsdk:"gateway"`
Netmask types.String `tfsdk:"netmask"`
Enabled types.Bool `tfsdk:"enabled"`
}
func (d *IPBlockDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_ip_block"
}
func (d *IPBlockDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches a single VirtFusion IP block by ID.",
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The IP block ID.",
Required: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The IP block name.",
Computed: true,
},
"type": schema.Int64Attribute{
MarkdownDescription: "The IP block type (4 = IPv4, 6 = IPv6).",
Computed: true,
},
"gateway": schema.StringAttribute{
MarkdownDescription: "The IPv4 gateway address.",
Computed: true,
},
"netmask": schema.StringAttribute{
MarkdownDescription: "The IPv4 netmask.",
Computed: true,
},
"enabled": schema.BoolAttribute{
MarkdownDescription: "Whether the IP block is enabled.",
Computed: true,
},
},
}
}
func (d *IPBlockDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *IPBlockDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data IPBlockDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/connectivity/ipblocks/%d", data.ID.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError("Error Reading IP Block", err.Error())
return
}
var blockResp client.IPBlockResponse
if err := json.Unmarshal(rawResp, &blockResp); err != nil {
resp.Diagnostics.AddError("Error Parsing IP Block Response", err.Error())
return
}
data.ID = types.Int64Value(blockResp.Data.ID)
data.Name = types.StringValue(blockResp.Data.Name)
data.Type = types.Int64Value(blockResp.Data.Type)
data.Gateway = types.StringValue(blockResp.Data.IPv4.Gateway)
data.Netmask = types.StringValue(blockResp.Data.IPv4.Netmask)
data.Enabled = types.BoolValue(blockResp.Data.Enabled)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,139 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &IPBlocksDataSource{}
_ datasource.DataSourceWithConfigure = &IPBlocksDataSource{}
)
// NewIPBlocksDataSource returns a new IP blocks data source.
func NewIPBlocksDataSource() datasource.DataSource {
return &IPBlocksDataSource{}
}
// IPBlocksDataSource defines the data source implementation.
type IPBlocksDataSource struct {
client *client.Client
}
// IPBlocksDataSourceModel describes the data source data model.
type IPBlocksDataSourceModel struct {
Results types.Int64 `tfsdk:"results"`
IPBlocks []IPBlockItemModel `tfsdk:"ip_blocks"`
}
// IPBlockItemModel describes a single IP block in the list.
type IPBlockItemModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Type types.Int64 `tfsdk:"type"`
Gateway types.String `tfsdk:"gateway"`
Netmask types.String `tfsdk:"netmask"`
Enabled types.Bool `tfsdk:"enabled"`
}
func (d *IPBlocksDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_ip_blocks"
}
func (d *IPBlocksDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches all VirtFusion IP blocks.",
Attributes: map[string]schema.Attribute{
"results": resultsSchemaAttribute(),
"ip_blocks": schema.ListNestedAttribute{
MarkdownDescription: "List of IP blocks.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The IP block ID.",
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The IP block name.",
Computed: true,
},
"type": schema.Int64Attribute{
MarkdownDescription: "The IP block type (4 = IPv4, 6 = IPv6).",
Computed: true,
},
"gateway": schema.StringAttribute{
MarkdownDescription: "The IPv4 gateway address.",
Computed: true,
},
"netmask": schema.StringAttribute{
MarkdownDescription: "The IPv4 netmask.",
Computed: true,
},
"enabled": schema.BoolAttribute{
MarkdownDescription: "Whether the IP block is enabled.",
Computed: true,
},
},
},
},
},
}
}
func (d *IPBlocksDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *IPBlocksDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data IPBlocksDataSourceModel
rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/connectivity/ipblocks?%s", resultsQueryParam(data.Results)))
if err != nil {
resp.Diagnostics.AddError("Error Reading IP Blocks", err.Error())
return
}
var listResp client.IPBlockListResponse
if err := json.Unmarshal(rawResp, &listResp); err != nil {
resp.Diagnostics.AddError("Error Parsing IP Blocks Response", err.Error())
return
}
data.IPBlocks = make([]IPBlockItemModel, len(listResp.Data))
for i, b := range listResp.Data {
data.IPBlocks[i] = IPBlockItemModel{
ID: types.Int64Value(b.ID),
Name: types.StringValue(b.Name),
Type: types.Int64Value(b.Type),
Gateway: types.StringValue(b.IPv4.Gateway),
Netmask: types.StringValue(b.IPv4.Netmask),
Enabled: types.BoolValue(b.Enabled),
}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &ISODataSource{}
_ datasource.DataSourceWithConfigure = &ISODataSource{}
)
// NewISODataSource returns a new ISO data source.
func NewISODataSource() datasource.DataSource {
return &ISODataSource{}
}
// ISODataSource defines the data source implementation.
type ISODataSource struct {
client *client.Client
}
// ISODataSourceModel describes the data source data model.
type ISODataSourceModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
}
func (d *ISODataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_iso"
}
func (d *ISODataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches a single VirtFusion ISO by ID.",
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The ISO ID.",
Required: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The ISO name.",
Computed: true,
},
},
}
}
func (d *ISODataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *ISODataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data ISODataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/media/iso/%d", data.ID.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError("Error Reading ISO", err.Error())
return
}
var isoResp client.ISOResponse
if err := json.Unmarshal(rawResp, &isoResp); err != nil {
resp.Diagnostics.AddError("Error Parsing ISO Response", err.Error())
return
}
data.ID = types.Int64Value(isoResp.Data.ID)
data.Name = types.StringValue(isoResp.Data.Name)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,147 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &PackageDataSource{}
_ datasource.DataSourceWithConfigure = &PackageDataSource{}
)
// NewPackageDataSource returns a new package data source.
func NewPackageDataSource() datasource.DataSource {
return &PackageDataSource{}
}
// PackageDataSource defines the data source implementation.
type PackageDataSource struct {
client *client.Client
}
// PackageDataSourceModel describes the data source data model.
type PackageDataSourceModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Enabled types.Bool `tfsdk:"enabled"`
CPUCores types.Int64 `tfsdk:"cpu_cores"`
Memory types.Int64 `tfsdk:"memory"`
Storage types.Int64 `tfsdk:"storage"`
Traffic types.Int64 `tfsdk:"traffic"`
NetworkSpeedInbound types.Int64 `tfsdk:"network_speed_inbound"`
NetworkSpeedOutbound types.Int64 `tfsdk:"network_speed_outbound"`
Ipv4 types.Int64 `tfsdk:"ipv4"`
}
func (d *PackageDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_package"
}
func (d *PackageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches a single VirtFusion package by ID.",
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The package ID.",
Required: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The package name.",
Computed: true,
},
"enabled": schema.BoolAttribute{
MarkdownDescription: "Whether the package is enabled.",
Computed: true,
},
"cpu_cores": schema.Int64Attribute{
MarkdownDescription: "The number of CPU cores in the package.",
Computed: true,
},
"memory": schema.Int64Attribute{
MarkdownDescription: "The amount of memory in the package.",
Computed: true,
},
"storage": schema.Int64Attribute{
MarkdownDescription: "The amount of storage in the package.",
Computed: true,
},
"traffic": schema.Int64Attribute{
MarkdownDescription: "The traffic limit in the package.",
Computed: true,
},
"network_speed_inbound": schema.Int64Attribute{
MarkdownDescription: "The inbound network speed in the package.",
Computed: true,
},
"network_speed_outbound": schema.Int64Attribute{
MarkdownDescription: "The outbound network speed in the package.",
Computed: true,
},
"ipv4": schema.Int64Attribute{
MarkdownDescription: "The number of IPv4 addresses in the package.",
Computed: true,
},
},
}
}
func (d *PackageDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *PackageDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data PackageDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/packages/%d", data.ID.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError("Error Reading Package", err.Error())
return
}
var pkgResp client.PackageResponse
if err := json.Unmarshal(rawResp, &pkgResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Package Response", err.Error())
return
}
data.ID = types.Int64Value(pkgResp.Data.ID)
data.Name = types.StringValue(pkgResp.Data.Name)
data.Enabled = types.BoolValue(pkgResp.Data.Enabled)
data.CPUCores = types.Int64Value(pkgResp.Data.CPUCores)
data.Memory = types.Int64Value(pkgResp.Data.Memory)
data.Storage = types.Int64Value(pkgResp.Data.Storage)
data.Traffic = types.Int64Value(pkgResp.Data.Traffic)
data.NetworkSpeedInbound = types.Int64Value(pkgResp.Data.NetworkSpeedInbound)
data.NetworkSpeedOutbound = types.Int64Value(pkgResp.Data.NetworkSpeedOutbound)
data.Ipv4 = types.Int64Value(pkgResp.Data.Ipv4)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,124 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &PackageTemplatesDataSource{}
_ datasource.DataSourceWithConfigure = &PackageTemplatesDataSource{}
)
// NewPackageTemplatesDataSource returns a new package templates data source.
func NewPackageTemplatesDataSource() datasource.DataSource {
return &PackageTemplatesDataSource{}
}
// PackageTemplatesDataSource defines the data source implementation.
type PackageTemplatesDataSource struct {
client *client.Client
}
// PackageTemplatesDataSourceModel describes the data source data model.
type PackageTemplatesDataSourceModel struct {
PackageID types.Int64 `tfsdk:"package_id"`
Results types.Int64 `tfsdk:"results"`
Templates []PackageTemplateItemModel `tfsdk:"templates"`
}
// PackageTemplateItemModel describes a single template in the list.
type PackageTemplateItemModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
}
func (d *PackageTemplatesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_package_templates"
}
func (d *PackageTemplatesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches templates available for a VirtFusion server package.",
Attributes: map[string]schema.Attribute{
"package_id": schema.Int64Attribute{
MarkdownDescription: "The package ID to fetch templates for.",
Required: true,
},
"results": resultsSchemaAttribute(),
"templates": schema.ListNestedAttribute{
MarkdownDescription: "List of templates available for the package.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The template ID.",
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The template name.",
Computed: true,
},
},
},
},
},
}
}
func (d *PackageTemplatesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *PackageTemplatesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data PackageTemplatesDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/media/templates/fromServerPackageSpec/%d?%s", data.PackageID.ValueInt64(), resultsQueryParam(data.Results)))
if err != nil {
resp.Diagnostics.AddError("Error Reading Package Templates", err.Error())
return
}
var templateResp client.TemplateResponse
if err := json.Unmarshal(rawResp, &templateResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Package Templates Response", err.Error())
return
}
data.Templates = make([]PackageTemplateItemModel, len(templateResp.Data))
for i, t := range templateResp.Data {
data.Templates[i] = PackageTemplateItemModel{
ID: types.Int64Value(t.ID),
Name: types.StringValue(t.Name),
}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,163 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &PackagesDataSource{}
_ datasource.DataSourceWithConfigure = &PackagesDataSource{}
)
// NewPackagesDataSource returns a new packages data source.
func NewPackagesDataSource() datasource.DataSource {
return &PackagesDataSource{}
}
// PackagesDataSource defines the data source implementation.
type PackagesDataSource struct {
client *client.Client
}
// PackagesDataSourceModel describes the data source data model.
type PackagesDataSourceModel struct {
Results types.Int64 `tfsdk:"results"`
Packages []PackageItemModel `tfsdk:"packages"`
}
// PackageItemModel describes a single package in the list.
type PackageItemModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Enabled types.Bool `tfsdk:"enabled"`
CPUCores types.Int64 `tfsdk:"cpu_cores"`
Memory types.Int64 `tfsdk:"memory"`
Storage types.Int64 `tfsdk:"storage"`
Traffic types.Int64 `tfsdk:"traffic"`
NetworkSpeedInbound types.Int64 `tfsdk:"network_speed_inbound"`
NetworkSpeedOutbound types.Int64 `tfsdk:"network_speed_outbound"`
Ipv4 types.Int64 `tfsdk:"ipv4"`
}
func (d *PackagesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_packages"
}
func (d *PackagesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches all VirtFusion packages.",
Attributes: map[string]schema.Attribute{
"results": resultsSchemaAttribute(),
"packages": schema.ListNestedAttribute{
MarkdownDescription: "List of packages.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The package ID.",
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The package name.",
Computed: true,
},
"enabled": schema.BoolAttribute{
MarkdownDescription: "Whether the package is enabled.",
Computed: true,
},
"cpu_cores": schema.Int64Attribute{
MarkdownDescription: "The number of CPU cores in the package.",
Computed: true,
},
"memory": schema.Int64Attribute{
MarkdownDescription: "The amount of memory in the package.",
Computed: true,
},
"storage": schema.Int64Attribute{
MarkdownDescription: "The amount of storage in the package.",
Computed: true,
},
"traffic": schema.Int64Attribute{
MarkdownDescription: "The traffic limit in the package.",
Computed: true,
},
"network_speed_inbound": schema.Int64Attribute{
MarkdownDescription: "The inbound network speed in the package.",
Computed: true,
},
"network_speed_outbound": schema.Int64Attribute{
MarkdownDescription: "The outbound network speed in the package.",
Computed: true,
},
"ipv4": schema.Int64Attribute{
MarkdownDescription: "The number of IPv4 addresses in the package.",
Computed: true,
},
},
},
},
},
}
}
func (d *PackagesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *PackagesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data PackagesDataSourceModel
rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/packages?%s", resultsQueryParam(data.Results)))
if err != nil {
resp.Diagnostics.AddError("Error Reading Packages", err.Error())
return
}
var listResp client.PackageListResponse
if err := json.Unmarshal(rawResp, &listResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Packages Response", err.Error())
return
}
data.Packages = make([]PackageItemModel, len(listResp.Data))
for i, p := range listResp.Data {
data.Packages[i] = PackageItemModel{
ID: types.Int64Value(p.ID),
Name: types.StringValue(p.Name),
Enabled: types.BoolValue(p.Enabled),
CPUCores: types.Int64Value(p.CPUCores),
Memory: types.Int64Value(p.Memory),
Storage: types.Int64Value(p.Storage),
Traffic: types.Int64Value(p.Traffic),
NetworkSpeedInbound: types.Int64Value(p.NetworkSpeedInbound),
NetworkSpeedOutbound: types.Int64Value(p.NetworkSpeedOutbound),
Ipv4: types.Int64Value(p.Ipv4),
}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,111 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &QueueItemDataSource{}
_ datasource.DataSourceWithConfigure = &QueueItemDataSource{}
)
// NewQueueItemDataSource returns a new queue item data source.
func NewQueueItemDataSource() datasource.DataSource {
return &QueueItemDataSource{}
}
// QueueItemDataSource defines the data source implementation.
type QueueItemDataSource struct {
client *client.Client
}
// QueueItemDataSourceModel describes the data source data model.
type QueueItemDataSourceModel struct {
ID types.Int64 `tfsdk:"id"`
Status types.String `tfsdk:"status"`
Action types.String `tfsdk:"action"`
CreatedAt types.String `tfsdk:"created_at"`
}
func (d *QueueItemDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_queue_item"
}
func (d *QueueItemDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches a single VirtFusion queue item by ID.",
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The queue item ID.",
Required: true,
},
"status": schema.StringAttribute{
MarkdownDescription: "The queue item status.",
Computed: true,
},
"action": schema.StringAttribute{
MarkdownDescription: "The queue item action.",
Computed: true,
},
"created_at": schema.StringAttribute{
MarkdownDescription: "The creation timestamp.",
Computed: true,
},
},
}
}
func (d *QueueItemDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *QueueItemDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data QueueItemDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/queue/%d", data.ID.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError("Error Reading Queue Item", err.Error())
return
}
var queueResp client.QueueResponse
if err := json.Unmarshal(rawResp, &queueResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Queue Item Response", err.Error())
return
}
data.ID = types.Int64Value(queueResp.Data.ID)
data.Status = types.StringValue(queueResp.Data.Status)
data.Action = types.StringValue(queueResp.Data.Action)
data.CreatedAt = types.StringValue(queueResp.Data.CreatedAt)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,121 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &SelfServiceCurrenciesDataSource{}
_ datasource.DataSourceWithConfigure = &SelfServiceCurrenciesDataSource{}
)
// NewSelfServiceCurrenciesDataSource returns a new self-service currencies data source.
func NewSelfServiceCurrenciesDataSource() datasource.DataSource {
return &SelfServiceCurrenciesDataSource{}
}
// SelfServiceCurrenciesDataSource defines the data source implementation.
type SelfServiceCurrenciesDataSource struct {
client *client.Client
}
// SelfServiceCurrenciesDataSourceModel describes the data source data model.
type SelfServiceCurrenciesDataSourceModel struct {
Results types.Int64 `tfsdk:"results"`
Currencies []CurrencyItemModel `tfsdk:"currencies"`
}
// CurrencyItemModel describes a single currency in the list.
type CurrencyItemModel struct {
ID types.Int64 `tfsdk:"id"`
Code types.String `tfsdk:"code"`
Name types.String `tfsdk:"name"`
}
func (d *SelfServiceCurrenciesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_self_service_currencies"
}
func (d *SelfServiceCurrenciesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches all available VirtFusion self-service currencies.",
Attributes: map[string]schema.Attribute{
"results": resultsSchemaAttribute(),
"currencies": schema.ListNestedAttribute{
MarkdownDescription: "List of available currencies.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The currency ID.",
Computed: true,
},
"code": schema.StringAttribute{
MarkdownDescription: "The currency code.",
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The currency name.",
Computed: true,
},
},
},
},
},
}
}
func (d *SelfServiceCurrenciesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *SelfServiceCurrenciesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data SelfServiceCurrenciesDataSourceModel
rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/selfService/currencies?%s", resultsQueryParam(data.Results)))
if err != nil {
resp.Diagnostics.AddError("Error Reading Self-Service Currencies", err.Error())
return
}
var currencyResp client.CurrencyResponse
if err := json.Unmarshal(rawResp, &currencyResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Self-Service Currencies Response", err.Error())
return
}
data.Currencies = make([]CurrencyItemModel, len(currencyResp.Data))
for i, c := range currencyResp.Data {
data.Currencies[i] = CurrencyItemModel{
ID: types.Int64Value(c.ID),
Code: types.StringValue(c.Code),
Name: types.StringValue(c.Name),
}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &SelfServiceHourlyStatsDataSource{}
_ datasource.DataSourceWithConfigure = &SelfServiceHourlyStatsDataSource{}
)
// NewSelfServiceHourlyStatsDataSource returns a new self-service hourly stats data source.
func NewSelfServiceHourlyStatsDataSource() datasource.DataSource {
return &SelfServiceHourlyStatsDataSource{}
}
// SelfServiceHourlyStatsDataSource defines the data source implementation.
type SelfServiceHourlyStatsDataSource struct {
client *client.Client
}
// SelfServiceHourlyStatsDataSourceModel describes the data source data model.
type SelfServiceHourlyStatsDataSourceModel struct {
UserID types.Int64 `tfsdk:"user_id"`
GroupID types.Int64 `tfsdk:"group_id"`
StatsJSON types.String `tfsdk:"stats_json"`
}
func (d *SelfServiceHourlyStatsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_self_service_hourly_stats"
}
func (d *SelfServiceHourlyStatsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches VirtFusion self-service hourly stats for a user and group.",
Attributes: map[string]schema.Attribute{
"user_id": schema.Int64Attribute{
MarkdownDescription: "The user ID to fetch hourly stats for.",
Required: true,
},
"group_id": schema.Int64Attribute{
MarkdownDescription: "The group ID to fetch hourly stats for.",
Required: true,
},
"stats_json": schema.StringAttribute{
MarkdownDescription: "The raw JSON response containing hourly stats.",
Computed: true,
},
},
}
}
func (d *SelfServiceHourlyStatsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *SelfServiceHourlyStatsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data SelfServiceHourlyStatsDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/selfService/hourlyStats/byUser/%d/group/%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError("Error Reading Self-Service Hourly Stats", err.Error())
return
}
data.StatsJSON = types.StringValue(string(rawResp))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &SelfServiceReportDataSource{}
_ datasource.DataSourceWithConfigure = &SelfServiceReportDataSource{}
)
// NewSelfServiceReportDataSource returns a new self-service report data source.
func NewSelfServiceReportDataSource() datasource.DataSource {
return &SelfServiceReportDataSource{}
}
// SelfServiceReportDataSource defines the data source implementation.
type SelfServiceReportDataSource struct {
client *client.Client
}
// SelfServiceReportDataSourceModel describes the data source data model.
type SelfServiceReportDataSourceModel struct {
UserID types.Int64 `tfsdk:"user_id"`
GroupID types.Int64 `tfsdk:"group_id"`
ReportJSON types.String `tfsdk:"report_json"`
}
func (d *SelfServiceReportDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_self_service_report"
}
func (d *SelfServiceReportDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches a VirtFusion self-service report for a user and group.",
Attributes: map[string]schema.Attribute{
"user_id": schema.Int64Attribute{
MarkdownDescription: "The user ID to fetch the report for.",
Required: true,
},
"group_id": schema.Int64Attribute{
MarkdownDescription: "The group ID to fetch the report for.",
Required: true,
},
"report_json": schema.StringAttribute{
MarkdownDescription: "The raw JSON response containing the report.",
Computed: true,
},
},
}
}
func (d *SelfServiceReportDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *SelfServiceReportDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data SelfServiceReportDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/selfService/report/byUser/%d/group/%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError("Error Reading Self-Service Report", err.Error())
return
}
data.ReportJSON = types.StringValue(string(rawResp))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,111 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &SelfServiceResourcePackDataSource{}
_ datasource.DataSourceWithConfigure = &SelfServiceResourcePackDataSource{}
)
// NewSelfServiceResourcePackDataSource returns a new self-service resource pack data source.
func NewSelfServiceResourcePackDataSource() datasource.DataSource {
return &SelfServiceResourcePackDataSource{}
}
// SelfServiceResourcePackDataSource defines the data source implementation.
type SelfServiceResourcePackDataSource struct {
client *client.Client
}
// SelfServiceResourcePackDataSourceModel describes the data source data model.
type SelfServiceResourcePackDataSourceModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
UserID types.Int64 `tfsdk:"user_id"`
PackID types.Int64 `tfsdk:"pack_id"`
}
func (d *SelfServiceResourcePackDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_self_service_resource_pack"
}
func (d *SelfServiceResourcePackDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches a single VirtFusion self-service resource pack by ID.",
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The resource pack ID.",
Required: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The resource pack name.",
Computed: true,
},
"user_id": schema.Int64Attribute{
MarkdownDescription: "The user ID associated with the resource pack.",
Computed: true,
},
"pack_id": schema.Int64Attribute{
MarkdownDescription: "The pack ID associated with the resource pack.",
Computed: true,
},
},
}
}
func (d *SelfServiceResourcePackDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *SelfServiceResourcePackDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data SelfServiceResourcePackDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/selfService/resourcePack/%d", data.ID.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError("Error Reading Self-Service Resource Pack", err.Error())
return
}
var packResp client.SelfServiceResourcePackResponse
if err := json.Unmarshal(rawResp, &packResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Self-Service Resource Pack Response", err.Error())
return
}
data.ID = types.Int64Value(packResp.Data.ID)
data.Name = types.StringValue(packResp.Data.Name)
data.UserID = types.Int64Value(packResp.Data.UserID)
data.PackID = types.Int64Value(packResp.Data.PackID)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &SelfServiceUsageDataSource{}
_ datasource.DataSourceWithConfigure = &SelfServiceUsageDataSource{}
)
// NewSelfServiceUsageDataSource returns a new self-service usage data source.
func NewSelfServiceUsageDataSource() datasource.DataSource {
return &SelfServiceUsageDataSource{}
}
// SelfServiceUsageDataSource defines the data source implementation.
type SelfServiceUsageDataSource struct {
client *client.Client
}
// SelfServiceUsageDataSourceModel describes the data source data model.
type SelfServiceUsageDataSourceModel struct {
UserID types.Int64 `tfsdk:"user_id"`
GroupID types.Int64 `tfsdk:"group_id"`
UsageJSON types.String `tfsdk:"usage_json"`
}
func (d *SelfServiceUsageDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_self_service_usage"
}
func (d *SelfServiceUsageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches VirtFusion self-service usage data for a user and group.",
Attributes: map[string]schema.Attribute{
"user_id": schema.Int64Attribute{
MarkdownDescription: "The user ID to fetch usage data for.",
Required: true,
},
"group_id": schema.Int64Attribute{
MarkdownDescription: "The group ID to fetch usage data for.",
Required: true,
},
"usage_json": schema.StringAttribute{
MarkdownDescription: "The raw JSON response containing usage data.",
Computed: true,
},
},
}
}
func (d *SelfServiceUsageDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *SelfServiceUsageDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data SelfServiceUsageDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/selfService/usage/byUser/%d/group/%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError("Error Reading Self-Service Usage", err.Error())
return
}
data.UsageJSON = types.StringValue(string(rawResp))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,156 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &ServerDataSource{}
var _ datasource.DataSourceWithConfigure = &ServerDataSource{}
// NewServerDataSource returns a new data source for reading a single server.
func NewServerDataSource() datasource.DataSource {
return &ServerDataSource{}
}
// ServerDataSource defines the data source implementation.
type ServerDataSource struct {
client *client.Client
}
// ServerDataSourceModel describes the data source data model.
type ServerDataSourceModel struct {
ID types.Int64 `tfsdk:"id"`
UUID types.String `tfsdk:"uuid"`
Name types.String `tfsdk:"name"`
Hostname types.String `tfsdk:"hostname"`
OwnerID types.Int64 `tfsdk:"owner_id"`
HypervisorID types.Int64 `tfsdk:"hypervisor_id"`
Suspended types.Bool `tfsdk:"suspended"`
CPUCores types.Int64 `tfsdk:"cpu_cores"`
Memory types.Int64 `tfsdk:"memory"`
Storage types.Int64 `tfsdk:"storage"`
}
func (d *ServerDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server"
}
func (d *ServerDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Use this data source to read a single VirtFusion server by ID.",
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The server ID.",
Required: true,
},
"uuid": schema.StringAttribute{
MarkdownDescription: "The server UUID.",
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The server display name.",
Computed: true,
},
"hostname": schema.StringAttribute{
MarkdownDescription: "The server hostname.",
Computed: true,
},
"owner_id": schema.Int64Attribute{
MarkdownDescription: "The owner (user) ID who owns the server.",
Computed: true,
},
"hypervisor_id": schema.Int64Attribute{
MarkdownDescription: "The hypervisor ID where the server is hosted.",
Computed: true,
},
"suspended": schema.BoolAttribute{
MarkdownDescription: "Whether the server is suspended.",
Computed: true,
},
"cpu_cores": schema.Int64Attribute{
MarkdownDescription: "The number of CPU cores.",
Computed: true,
},
"memory": schema.Int64Attribute{
MarkdownDescription: "The memory size in MB.",
Computed: true,
},
"storage": schema.Int64Attribute{
MarkdownDescription: "The storage size in GB.",
Computed: true,
},
},
}
}
func (d *ServerDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
d.client = c
}
func (d *ServerDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data ServerDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
result, err := d.client.Get(ctx, fmt.Sprintf("/servers/%d", data.ID.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError("Error Reading Server", err.Error())
return
}
var serverResp client.ServerResponse
if err := json.Unmarshal(result, &serverResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Server Response", err.Error())
return
}
s := serverResp.Data
data.ID = types.Int64Value(s.ID)
data.UUID = types.StringValue(s.UUID)
data.Name = types.StringValue(s.Name)
data.Hostname = types.StringValue(s.Hostname)
data.OwnerID = types.Int64Value(s.OwnerID)
data.HypervisorID = types.Int64Value(s.HypervisorID)
data.Suspended = types.BoolValue(s.Suspended)
// Extract nested resource values from the detailed server response.
var cpuCores, memory, storage int64
if s.CPU != nil {
cpuCores = s.CPU.Cores
}
if s.Settings != nil && s.Settings.Resources != nil {
memory = s.Settings.Resources.Memory
storage = s.Settings.Resources.Storage
}
data.CPUCores = types.Int64Value(cpuCores)
data.Memory = types.Int64Value(memory)
data.Storage = types.Int64Value(storage)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,144 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &ServerBackupsDataSource{}
var _ datasource.DataSourceWithConfigure = &ServerBackupsDataSource{}
// NewServerBackupsDataSource returns a new data source for listing server backups.
func NewServerBackupsDataSource() datasource.DataSource {
return &ServerBackupsDataSource{}
}
// ServerBackupsDataSource defines the data source implementation.
type ServerBackupsDataSource struct {
client *client.Client
}
// ServerBackupsDataSourceModel describes the data source data model.
type ServerBackupsDataSourceModel struct {
ServerID types.Int64 `tfsdk:"server_id"`
Results types.Int64 `tfsdk:"results"`
Backups types.List `tfsdk:"backups"`
}
func (d *ServerBackupsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server_backups"
}
func (d *ServerBackupsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Use this data source to list backups for a VirtFusion server.",
Attributes: map[string]schema.Attribute{
"server_id": schema.Int64Attribute{
MarkdownDescription: "The server ID to list backups for.",
Required: true,
},
"results": resultsSchemaAttribute(),
"backups": schema.ListNestedAttribute{
MarkdownDescription: "The list of backups.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The backup ID.",
Computed: true,
},
"type": schema.StringAttribute{
MarkdownDescription: "The backup type.",
Computed: true,
},
"created_at": schema.StringAttribute{
MarkdownDescription: "The backup creation timestamp.",
Computed: true,
},
},
},
},
},
}
}
func (d *ServerBackupsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
d.client = c
}
func (d *ServerBackupsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data ServerBackupsDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
result, err := d.client.GetAllPages(ctx, fmt.Sprintf("/backups/server/%d?%s", data.ServerID.ValueInt64(), resultsQueryParam(data.Results)))
if err != nil {
resp.Diagnostics.AddError("Error Reading Server Backups", err.Error())
return
}
var backupsResp client.BackupResponse
if err := json.Unmarshal(result, &backupsResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Server Backups Response", err.Error())
return
}
backupAttrTypes := map[string]attr.Type{
"id": types.Int64Type,
"type": types.StringType,
"created_at": types.StringType,
}
backupObjects := make([]attr.Value, len(backupsResp.Data))
for i, b := range backupsResp.Data {
obj, diags := types.ObjectValue(
backupAttrTypes,
map[string]attr.Value{
"id": types.Int64Value(b.ID),
"type": types.StringValue(b.Type),
"created_at": types.StringValue(b.CreatedAt),
},
)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
backupObjects[i] = obj
}
backupsList, diags := types.ListValue(types.ObjectType{AttrTypes: backupAttrTypes}, backupObjects)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.Backups = backupsList
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,166 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &ServerFirewallDataSource{}
var _ datasource.DataSourceWithConfigure = &ServerFirewallDataSource{}
// NewServerFirewallDataSource returns a new data source for reading server firewall information.
func NewServerFirewallDataSource() datasource.DataSource {
return &ServerFirewallDataSource{}
}
// ServerFirewallDataSource defines the data source implementation.
type ServerFirewallDataSource struct {
client *client.Client
}
// ServerFirewallDataSourceModel describes the data source data model.
type ServerFirewallDataSourceModel struct {
ServerID types.Int64 `tfsdk:"server_id"`
InterfaceName types.String `tfsdk:"interface_name"`
Enabled types.Bool `tfsdk:"enabled"`
Rules types.List `tfsdk:"rules"`
}
func (d *ServerFirewallDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server_firewall"
}
func (d *ServerFirewallDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Use this data source to read firewall information for a VirtFusion server.",
Attributes: map[string]schema.Attribute{
"server_id": schema.Int64Attribute{
MarkdownDescription: "The server ID to read firewall information for.",
Required: true,
},
"interface_name": schema.StringAttribute{
MarkdownDescription: "The network interface name. Defaults to `eth0`.",
Optional: true,
Computed: true,
},
"enabled": schema.BoolAttribute{
MarkdownDescription: "Whether the firewall is enabled.",
Computed: true,
},
"rules": schema.ListNestedAttribute{
MarkdownDescription: "The firewall rules.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"action": schema.StringAttribute{
MarkdownDescription: "The action for the rule (e.g. `accept`, `drop`).",
Computed: true,
},
"direction": schema.StringAttribute{
MarkdownDescription: "The direction for the rule (e.g. `in`, `out`).",
Computed: true,
},
"protocol": schema.StringAttribute{
MarkdownDescription: "The protocol for the rule (e.g. `tcp`, `udp`).",
Computed: true,
},
"port": schema.StringAttribute{
MarkdownDescription: "The port or port range for the rule.",
Computed: true,
},
"ip": schema.StringAttribute{
MarkdownDescription: "The IP address or CIDR for the rule.",
Computed: true,
},
},
},
},
},
}
}
func (d *ServerFirewallDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
d.client = c
}
func (d *ServerFirewallDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data ServerFirewallDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Default interface_name to "eth0" if not set.
interfaceName := "eth0"
if !data.InterfaceName.IsNull() && !data.InterfaceName.IsUnknown() && data.InterfaceName.ValueString() != "" {
interfaceName = data.InterfaceName.ValueString()
}
data.InterfaceName = types.StringValue(interfaceName)
result, err := d.client.Get(ctx, fmt.Sprintf("/servers/%d/firewall/%s", data.ServerID.ValueInt64(), interfaceName))
if err != nil {
resp.Diagnostics.AddError("Error Reading Server Firewall", err.Error())
return
}
var fwResp client.FirewallResponse
if err := json.Unmarshal(result, &fwResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Server Firewall Response", err.Error())
return
}
data.Enabled = types.BoolValue(fwResp.Data.Enabled)
ruleObjects := make([]attr.Value, len(fwResp.Data.Rules))
for i, rule := range fwResp.Data.Rules {
obj, diags := types.ObjectValue(
firewallRuleAttrTypes(),
map[string]attr.Value{
"action": types.StringValue(rule.Action),
"direction": types.StringValue(rule.Direction),
"protocol": types.StringValue(rule.Protocol),
"port": types.StringValue(rule.Port),
"ip": types.StringValue(rule.IP),
},
)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ruleObjects[i] = obj
}
rulesList, diags := types.ListValue(types.ObjectType{AttrTypes: firewallRuleAttrTypes()}, ruleObjects)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.Rules = rulesList
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,138 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &ServerTemplatesDataSource{}
var _ datasource.DataSourceWithConfigure = &ServerTemplatesDataSource{}
// NewServerTemplatesDataSource returns a new data source for listing server templates.
func NewServerTemplatesDataSource() datasource.DataSource {
return &ServerTemplatesDataSource{}
}
// ServerTemplatesDataSource defines the data source implementation.
type ServerTemplatesDataSource struct {
client *client.Client
}
// ServerTemplatesDataSourceModel describes the data source data model.
type ServerTemplatesDataSourceModel struct {
ServerID types.Int64 `tfsdk:"server_id"`
Results types.Int64 `tfsdk:"results"`
Templates types.List `tfsdk:"templates"`
}
func (d *ServerTemplatesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server_templates"
}
func (d *ServerTemplatesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Use this data source to list available templates for a VirtFusion server.",
Attributes: map[string]schema.Attribute{
"server_id": schema.Int64Attribute{
MarkdownDescription: "The server ID to list templates for.",
Required: true,
},
"results": resultsSchemaAttribute(),
"templates": schema.ListNestedAttribute{
MarkdownDescription: "The list of available templates.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The template ID.",
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The template name.",
Computed: true,
},
},
},
},
},
}
}
func (d *ServerTemplatesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
d.client = c
}
func (d *ServerTemplatesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data ServerTemplatesDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
result, err := d.client.GetAllPages(ctx, fmt.Sprintf("/servers/%d/templates?%s", data.ServerID.ValueInt64(), resultsQueryParam(data.Results)))
if err != nil {
resp.Diagnostics.AddError("Error Reading Server Templates", err.Error())
return
}
var templateResp client.TemplateResponse
if err := json.Unmarshal(result, &templateResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Server Templates Response", err.Error())
return
}
templateAttrTypes := map[string]attr.Type{
"id": types.Int64Type,
"name": types.StringType,
}
templateObjects := make([]attr.Value, len(templateResp.Data))
for i, t := range templateResp.Data {
obj, diags := types.ObjectValue(
templateAttrTypes,
map[string]attr.Value{
"id": types.Int64Value(t.ID),
"name": types.StringValue(t.Name),
},
)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
templateObjects[i] = obj
}
templatesList, diags := types.ListValue(types.ObjectType{AttrTypes: templateAttrTypes}, templateObjects)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.Templates = templatesList
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,102 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &ServerTrafficDataSource{}
var _ datasource.DataSourceWithConfigure = &ServerTrafficDataSource{}
// NewServerTrafficDataSource returns a new data source for reading server traffic.
func NewServerTrafficDataSource() datasource.DataSource {
return &ServerTrafficDataSource{}
}
// ServerTrafficDataSource defines the data source implementation.
type ServerTrafficDataSource struct {
client *client.Client
}
// ServerTrafficDataSourceModel describes the data source data model.
type ServerTrafficDataSourceModel struct {
ServerID types.Int64 `tfsdk:"server_id"`
Used types.Int64 `tfsdk:"used"`
Limit types.Int64 `tfsdk:"limit"`
}
func (d *ServerTrafficDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server_traffic"
}
func (d *ServerTrafficDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Use this data source to read traffic usage for a VirtFusion server.",
Attributes: map[string]schema.Attribute{
"server_id": schema.Int64Attribute{
MarkdownDescription: "The server ID to read traffic for.",
Required: true,
},
"used": schema.Int64Attribute{
MarkdownDescription: "The amount of traffic used.",
Computed: true,
},
"limit": schema.Int64Attribute{
MarkdownDescription: "The traffic limit.",
Computed: true,
},
},
}
}
func (d *ServerTrafficDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
d.client = c
}
func (d *ServerTrafficDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data ServerTrafficDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
result, err := d.client.Get(ctx, fmt.Sprintf("/servers/%d/traffic", data.ServerID.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError("Error Reading Server Traffic", err.Error())
return
}
var trafficResp client.TrafficResponse
if err := json.Unmarshal(result, &trafficResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Server Traffic Response", err.Error())
return
}
data.Used = types.Int64Value(trafficResp.Data.Used)
data.Limit = types.Int64Value(trafficResp.Data.Limit)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,138 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &ServerTrafficBlocksDataSource{}
var _ datasource.DataSourceWithConfigure = &ServerTrafficBlocksDataSource{}
// NewServerTrafficBlocksDataSource returns a new data source for listing server traffic blocks.
func NewServerTrafficBlocksDataSource() datasource.DataSource {
return &ServerTrafficBlocksDataSource{}
}
// ServerTrafficBlocksDataSource defines the data source implementation.
type ServerTrafficBlocksDataSource struct {
client *client.Client
}
// ServerTrafficBlocksDataSourceModel describes the data source data model.
type ServerTrafficBlocksDataSourceModel struct {
ServerID types.Int64 `tfsdk:"server_id"`
Results types.Int64 `tfsdk:"results"`
Blocks types.List `tfsdk:"blocks"`
}
func (d *ServerTrafficBlocksDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server_traffic_blocks"
}
func (d *ServerTrafficBlocksDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Use this data source to list traffic blocks for a VirtFusion server.",
Attributes: map[string]schema.Attribute{
"server_id": schema.Int64Attribute{
MarkdownDescription: "The server ID to list traffic blocks for.",
Required: true,
},
"results": resultsSchemaAttribute(),
"blocks": schema.ListNestedAttribute{
MarkdownDescription: "The list of traffic blocks.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The traffic block ID.",
Computed: true,
},
"type": schema.StringAttribute{
MarkdownDescription: "The traffic block type.",
Computed: true,
},
},
},
},
},
}
}
func (d *ServerTrafficBlocksDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
d.client = c
}
func (d *ServerTrafficBlocksDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data ServerTrafficBlocksDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
result, err := d.client.GetAllPages(ctx, fmt.Sprintf("/servers/%d/traffic/blocks?%s", data.ServerID.ValueInt64(), resultsQueryParam(data.Results)))
if err != nil {
resp.Diagnostics.AddError("Error Reading Server Traffic Blocks", err.Error())
return
}
var blocksResp client.TrafficBlockListResponse
if err := json.Unmarshal(result, &blocksResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Server Traffic Blocks Response", err.Error())
return
}
blockAttrTypes := map[string]attr.Type{
"id": types.Int64Type,
"type": types.StringType,
}
blockObjects := make([]attr.Value, len(blocksResp.Data))
for i, b := range blocksResp.Data {
obj, diags := types.ObjectValue(
blockAttrTypes,
map[string]attr.Value{
"id": types.Int64Value(b.ID),
"type": types.StringValue(b.Type),
},
)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
blockObjects[i] = obj
}
blocksList, diags := types.ListValue(types.ObjectType{AttrTypes: blockAttrTypes}, blockObjects)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.Blocks = blocksList
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,97 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &ServerVNCDataSource{}
var _ datasource.DataSourceWithConfigure = &ServerVNCDataSource{}
// NewServerVNCDataSource returns a new data source for reading server VNC information.
func NewServerVNCDataSource() datasource.DataSource {
return &ServerVNCDataSource{}
}
// ServerVNCDataSource defines the data source implementation.
type ServerVNCDataSource struct {
client *client.Client
}
// ServerVNCDataSourceModel describes the data source data model.
type ServerVNCDataSourceModel struct {
ServerID types.Int64 `tfsdk:"server_id"`
URL types.String `tfsdk:"url"`
}
func (d *ServerVNCDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server_vnc"
}
func (d *ServerVNCDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Use this data source to read VNC connection information for a VirtFusion server.",
Attributes: map[string]schema.Attribute{
"server_id": schema.Int64Attribute{
MarkdownDescription: "The server ID to read VNC information for.",
Required: true,
},
"url": schema.StringAttribute{
MarkdownDescription: "The VNC connection URL.",
Computed: true,
Sensitive: true,
},
},
}
}
func (d *ServerVNCDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
d.client = c
}
func (d *ServerVNCDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data ServerVNCDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
result, err := d.client.Get(ctx, fmt.Sprintf("/servers/%d/vnc", data.ServerID.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError("Error Reading Server VNC", err.Error())
return
}
var vncResp client.VNCResponse
if err := json.Unmarshal(result, &vncResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Server VNC Response", err.Error())
return
}
data.URL = types.StringValue(vncResp.Data.URL)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,177 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &ServersDataSource{}
var _ datasource.DataSourceWithConfigure = &ServersDataSource{}
// NewServersDataSource returns a new data source for listing all servers.
func NewServersDataSource() datasource.DataSource {
return &ServersDataSource{}
}
// ServersDataSource defines the data source implementation.
type ServersDataSource struct {
client *client.Client
}
// ServersDataSourceModel describes the data source data model.
type ServersDataSourceModel struct {
Results types.Int64 `tfsdk:"results"`
Servers types.List `tfsdk:"servers"`
}
func (d *ServersDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_servers"
}
func (d *ServersDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Use this data source to list all VirtFusion servers.",
Attributes: map[string]schema.Attribute{
"results": resultsSchemaAttribute(),
"servers": schema.ListNestedAttribute{
MarkdownDescription: "The list of servers.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: serverDataSourceSchemaAttributes(),
},
},
},
}
}
func (d *ServersDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
d.client = c
}
func (d *ServersDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data ServersDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
result, err := d.client.GetAllPages(ctx, fmt.Sprintf("/servers?%s", resultsQueryParam(data.Results)))
if err != nil {
resp.Diagnostics.AddError("Error Reading Servers", err.Error())
return
}
var listResp client.ServerListResponse
if err := json.Unmarshal(result, &listResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Servers Response", err.Error())
return
}
serverObjects := make([]attr.Value, len(listResp.Data))
for i, s := range listResp.Data {
obj, diags := types.ObjectValue(
serverDataSourceAttrTypes(),
serverDataToAttrValues(s),
)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
serverObjects[i] = obj
}
serversList, diags := types.ListValue(types.ObjectType{AttrTypes: serverDataSourceAttrTypes()}, serverObjects)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.Servers = serversList
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
// serverDataSourceSchemaAttributes returns the schema attributes for a server object
// used in list data sources.
func serverDataSourceSchemaAttributes() map[string]schema.Attribute {
return map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The server ID.",
Computed: true,
},
"uuid": schema.StringAttribute{
MarkdownDescription: "The server UUID.",
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The server display name.",
Computed: true,
},
"hostname": schema.StringAttribute{
MarkdownDescription: "The server hostname.",
Computed: true,
},
"owner_id": schema.Int64Attribute{
MarkdownDescription: "The owner (user) ID who owns the server.",
Computed: true,
},
"hypervisor_id": schema.Int64Attribute{
MarkdownDescription: "The hypervisor ID where the server is hosted.",
Computed: true,
},
"suspended": schema.BoolAttribute{
MarkdownDescription: "Whether the server is suspended.",
Computed: true,
},
}
}
// serverDataSourceAttrTypes returns the attribute types for a server object.
func serverDataSourceAttrTypes() map[string]attr.Type {
return map[string]attr.Type{
"id": types.Int64Type,
"uuid": types.StringType,
"name": types.StringType,
"hostname": types.StringType,
"owner_id": types.Int64Type,
"hypervisor_id": types.Int64Type,
"suspended": types.BoolType,
}
}
// serverDataToAttrValues converts a client.ServerData to a map of attribute values.
func serverDataToAttrValues(s client.ServerData) map[string]attr.Value {
return map[string]attr.Value{
"id": types.Int64Value(s.ID),
"uuid": types.StringValue(s.UUID),
"name": types.StringValue(s.Name),
"hostname": types.StringValue(s.Hostname),
"owner_id": types.Int64Value(s.OwnerID),
"hypervisor_id": types.Int64Value(s.HypervisorID),
"suspended": types.BoolValue(s.Suspended),
}
}

View File

@@ -0,0 +1,121 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &ServersByUserDataSource{}
var _ datasource.DataSourceWithConfigure = &ServersByUserDataSource{}
// NewServersByUserDataSource returns a new data source for listing servers by user.
func NewServersByUserDataSource() datasource.DataSource {
return &ServersByUserDataSource{}
}
// ServersByUserDataSource defines the data source implementation.
type ServersByUserDataSource struct {
client *client.Client
}
// ServersByUserDataSourceModel describes the data source data model.
type ServersByUserDataSourceModel struct {
UserID types.Int64 `tfsdk:"user_id"`
Results types.Int64 `tfsdk:"results"`
Servers types.List `tfsdk:"servers"`
}
func (d *ServersByUserDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_servers_by_user"
}
func (d *ServersByUserDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Use this data source to list all VirtFusion servers owned by a specific user.",
Attributes: map[string]schema.Attribute{
"user_id": schema.Int64Attribute{
MarkdownDescription: "The user ID to filter servers by.",
Required: true,
},
"results": resultsSchemaAttribute(),
"servers": schema.ListNestedAttribute{
MarkdownDescription: "The list of servers owned by the user.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: serverDataSourceSchemaAttributes(),
},
},
},
}
}
func (d *ServersByUserDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
d.client = c
}
func (d *ServersByUserDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data ServersByUserDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
result, err := d.client.GetAllPages(ctx, fmt.Sprintf("/servers/user/%d?%s", data.UserID.ValueInt64(), resultsQueryParam(data.Results)))
if err != nil {
resp.Diagnostics.AddError("Error Reading Servers By User", err.Error())
return
}
var listResp client.ServerListResponse
if err := json.Unmarshal(result, &listResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Servers Response", err.Error())
return
}
serverObjects := make([]attr.Value, len(listResp.Data))
for i, s := range listResp.Data {
obj, diags := types.ObjectValue(
serverDataSourceAttrTypes(),
serverDataToAttrValues(s),
)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
serverObjects[i] = obj
}
serversList, diags := types.ListValue(types.ObjectType{AttrTypes: serverDataSourceAttrTypes()}, serverObjects)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.Servers = serversList
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,135 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &SSHKeyDataSource{}
_ datasource.DataSourceWithConfigure = &SSHKeyDataSource{}
)
// NewSSHKeyDataSource returns a new SSH key data source.
func NewSSHKeyDataSource() datasource.DataSource {
return &SSHKeyDataSource{}
}
// SSHKeyDataSource defines the data source implementation.
type SSHKeyDataSource struct {
client *client.Client
}
// SSHKeyDataSourceModel describes the data source data model.
type SSHKeyDataSourceModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Type types.String `tfsdk:"type"`
PublicKey types.String `tfsdk:"public_key"`
Enabled types.Bool `tfsdk:"enabled"`
UserID types.Int64 `tfsdk:"user_id"`
CreatedAt types.String `tfsdk:"created_at"`
UpdatedAt types.String `tfsdk:"updated_at"`
}
func (d *SSHKeyDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_ssh_key"
}
func (d *SSHKeyDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches a single VirtFusion SSH key by ID.",
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The SSH key ID.",
Required: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The SSH key name.",
Computed: true,
},
"type": schema.StringAttribute{
MarkdownDescription: "The SSH key type.",
Computed: true,
},
"public_key": schema.StringAttribute{
MarkdownDescription: "The public key content.",
Computed: true,
},
"enabled": schema.BoolAttribute{
MarkdownDescription: "Whether the SSH key is enabled.",
Computed: true,
},
"user_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the user who owns this SSH key.",
Computed: true,
},
"created_at": schema.StringAttribute{
MarkdownDescription: "The creation timestamp.",
Computed: true,
},
"updated_at": schema.StringAttribute{
MarkdownDescription: "The last update timestamp.",
Computed: true,
},
},
}
}
func (d *SSHKeyDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *SSHKeyDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data SSHKeyDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/ssh_keys/%d", data.ID.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError("Error Reading SSH Key", err.Error())
return
}
var sshKeyResp client.SSHKeyResponse
if err := json.Unmarshal(rawResp, &sshKeyResp); err != nil {
resp.Diagnostics.AddError("Error Parsing SSH Key Response", err.Error())
return
}
data.ID = types.Int64Value(sshKeyResp.Data.ID)
data.Name = types.StringValue(sshKeyResp.Data.Name)
data.Type = types.StringValue(sshKeyResp.Data.Type)
data.PublicKey = types.StringValue(sshKeyResp.Data.PublicKey)
data.Enabled = types.BoolValue(sshKeyResp.Data.Enabled)
data.UserID = types.Int64Value(sshKeyResp.Data.UserID)
data.CreatedAt = types.StringValue(sshKeyResp.Data.CreatedAt)
data.UpdatedAt = types.StringValue(sshKeyResp.Data.UpdatedAt)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,142 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &SSHKeysByUserDataSource{}
_ datasource.DataSourceWithConfigure = &SSHKeysByUserDataSource{}
)
// NewSSHKeysByUserDataSource returns a new SSH keys by user data source.
func NewSSHKeysByUserDataSource() datasource.DataSource {
return &SSHKeysByUserDataSource{}
}
// SSHKeysByUserDataSource defines the data source implementation.
type SSHKeysByUserDataSource struct {
client *client.Client
}
// SSHKeysByUserDataSourceModel describes the data source data model.
type SSHKeysByUserDataSourceModel struct {
UserID types.Int64 `tfsdk:"user_id"`
Results types.Int64 `tfsdk:"results"`
SSHKeys []SSHKeyByUserItemModel `tfsdk:"ssh_keys"`
}
// SSHKeyByUserItemModel describes a single SSH key in the list.
type SSHKeyByUserItemModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Type types.String `tfsdk:"type"`
PublicKey types.String `tfsdk:"public_key"`
Enabled types.Bool `tfsdk:"enabled"`
}
func (d *SSHKeysByUserDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_ssh_keys_by_user"
}
func (d *SSHKeysByUserDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches all SSH keys for a VirtFusion user.",
Attributes: map[string]schema.Attribute{
"user_id": schema.Int64Attribute{
MarkdownDescription: "The user ID to fetch SSH keys for.",
Required: true,
},
"results": resultsSchemaAttribute(),
"ssh_keys": schema.ListNestedAttribute{
MarkdownDescription: "List of SSH keys belonging to the user.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The SSH key ID.",
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The SSH key name.",
Computed: true,
},
"type": schema.StringAttribute{
MarkdownDescription: "The SSH key type.",
Computed: true,
},
"public_key": schema.StringAttribute{
MarkdownDescription: "The public key content.",
Computed: true,
},
"enabled": schema.BoolAttribute{
MarkdownDescription: "Whether the SSH key is enabled.",
Computed: true,
},
},
},
},
},
}
}
func (d *SSHKeysByUserDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *SSHKeysByUserDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data SSHKeysByUserDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/ssh_keys/user/%d?%s", data.UserID.ValueInt64(), resultsQueryParam(data.Results)))
if err != nil {
resp.Diagnostics.AddError("Error Reading SSH Keys By User", err.Error())
return
}
var listResp client.SSHKeyListResponse
if err := json.Unmarshal(rawResp, &listResp); err != nil {
resp.Diagnostics.AddError("Error Parsing SSH Keys Response", err.Error())
return
}
data.SSHKeys = make([]SSHKeyByUserItemModel, len(listResp.Data))
for i, k := range listResp.Data {
data.SSHKeys[i] = SSHKeyByUserItemModel{
ID: types.Int64Value(k.ID),
Name: types.StringValue(k.Name),
Type: types.StringValue(k.Type),
PublicKey: types.StringValue(k.PublicKey),
Enabled: types.BoolValue(k.Enabled),
}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -0,0 +1,128 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &UserDataSource{}
_ datasource.DataSourceWithConfigure = &UserDataSource{}
)
// NewUserDataSource returns a new user data source.
func NewUserDataSource() datasource.DataSource {
return &UserDataSource{}
}
// UserDataSource defines the data source implementation.
type UserDataSource struct {
client *client.Client
}
// UserDataSourceModel describes the data source data model.
type UserDataSourceModel struct {
ExtRelationID types.String `tfsdk:"ext_relation_id"`
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Email types.String `tfsdk:"email"`
Enabled types.Bool `tfsdk:"enabled"`
CreatedAt types.String `tfsdk:"created_at"`
UpdatedAt types.String `tfsdk:"updated_at"`
}
func (d *UserDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_user"
}
func (d *UserDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches a VirtFusion user by external relation ID.",
Attributes: map[string]schema.Attribute{
"ext_relation_id": schema.StringAttribute{
MarkdownDescription: "The external relation ID of the user.",
Required: true,
},
"id": schema.Int64Attribute{
MarkdownDescription: "The numeric ID of the user.",
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The user name.",
Computed: true,
},
"email": schema.StringAttribute{
MarkdownDescription: "The user email address.",
Computed: true,
},
"enabled": schema.BoolAttribute{
MarkdownDescription: "Whether the user is enabled.",
Computed: true,
},
"created_at": schema.StringAttribute{
MarkdownDescription: "The creation timestamp.",
Computed: true,
},
"updated_at": schema.StringAttribute{
MarkdownDescription: "The last update timestamp.",
Computed: true,
},
},
}
}
func (d *UserDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
d.client = c
}
func (d *UserDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data UserDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/users/%s/byExtRelation", data.ExtRelationID.ValueString()))
if err != nil {
resp.Diagnostics.AddError("Error Reading User", err.Error())
return
}
var userResp client.UserResponse
if err := json.Unmarshal(rawResp, &userResp); err != nil {
resp.Diagnostics.AddError("Error Parsing User Response", err.Error())
return
}
data.ID = types.Int64Value(userResp.Data.ID)
data.Name = types.StringValue(userResp.Data.Name)
data.Email = types.StringValue(userResp.Data.Email)
data.Enabled = types.BoolValue(userResp.Data.Enabled)
data.CreatedAt = types.StringValue(userResp.Data.CreatedAt)
data.UpdatedAt = types.StringValue(userResp.Data.UpdatedAt)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@@ -1,141 +1,181 @@
// Copyright (c) HashiCorp, Inc.
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"fmt"
"os"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
dsschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"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"
"net/http"
"net/url"
"os"
"path"
)
// Ensure VirtfusionProvider satisfies various provider interfaces.
const defaultResultsPerPage int64 = 300
// resultsSchemaAttribute returns the standard "results" schema attribute for list data sources.
func resultsSchemaAttribute() dsschema.Int64Attribute {
return dsschema.Int64Attribute{
MarkdownDescription: "Maximum number of results to return. Defaults to 300.",
Optional: true,
}
}
// resultsQueryParam returns the query parameter string for the results limit.
// If results is null/unknown, defaults to defaultResultsPerPage.
func resultsQueryParam(results types.Int64) string {
n := defaultResultsPerPage
if !results.IsNull() && !results.IsUnknown() {
n = results.ValueInt64()
}
return fmt.Sprintf("results=%d", n)
}
var _ provider.Provider = &VirtfusionProvider{}
// VirtfusionProvider defines the provider implementation.
type VirtfusionProvider 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
}
// ScaffoldingProviderModel describes the provider data model.
type ScaffoldingProviderModel struct {
// VirtfusionProviderModel describes the provider data model.
type VirtfusionProviderModel struct {
Endpoint types.String `tfsdk:"endpoint"`
ApiToken types.String `tfsdk:"api_token"`
}
func (p *VirtfusionProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
func (p *VirtfusionProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "virtfusion"
resp.Version = p.version
}
func (p *VirtfusionProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
func (p *VirtfusionProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "The VirtFusion provider allows managing VirtFusion virtualization platform resources.",
Attributes: map[string]schema.Attribute{
"endpoint": schema.StringAttribute{
MarkdownDescription: "The endpoint to use for API requests.",
Required: true,
MarkdownDescription: "The VirtFusion API endpoint. Can be a hostname (e.g. `cp.example.com`) or full URL (e.g. `https://cp.example.com/api/v1`). Can also be set with the `VIRTFUSION_ENDPOINT` environment variable.",
Optional: true,
},
"api_token": schema.StringAttribute{
MarkdownDescription: "The API token to use for API requests.",
Required: true,
MarkdownDescription: "The API token for authentication. Can also be set with the `VIRTFUSION_API_TOKEN` environment variable.",
Optional: true,
Sensitive: true,
},
},
}
}
func (p *VirtfusionProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
// Check environment variables
apiToken := os.Getenv("VIRTFUSION_API_TOKEN")
endpoint := os.Getenv("VIRTFUSION_ENDPOINT")
var data ScaffoldingProviderModel
var data VirtfusionProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Configuration values are now available.
// if data.Endpoint.IsNull() { /* ... */ }
// Environment variables as fallback
endpoint := os.Getenv("VIRTFUSION_ENDPOINT")
apiToken := os.Getenv("VIRTFUSION_API_TOKEN")
if data.Endpoint.ValueString() != "" {
// Config values override env vars
if !data.Endpoint.IsNull() && data.Endpoint.ValueString() != "" {
endpoint = data.Endpoint.ValueString()
}
if data.ApiToken.ValueString() != "" {
if !data.ApiToken.IsNull() && data.ApiToken.ValueString() != "" {
apiToken = data.ApiToken.ValueString()
}
if apiToken == "" {
resp.Diagnostics.AddError(
"Missing API Token Configuration",
"While configuring the provider, the API token was not found in "+
"the VIRTFUSION_API_TOKEN environment variable or provider "+
"configuration block api_token attribute.",
)
// Not returning early allows the logic to collect all errors.
}
if endpoint == "" {
resp.Diagnostics.AddError(
"Missing Endpoint Configuration",
"While configuring the provider, the endpoint was not found in "+
"the VIRTFUSION_ENDPOINT environment variable or provider "+
"configuration block endpoint attribute.",
"The VirtFusion endpoint was not found in the VIRTFUSION_ENDPOINT environment variable or provider configuration block endpoint attribute.",
)
// Not returning early allows the logic to collect all errors.
}
if apiToken == "" {
resp.Diagnostics.AddError(
"Missing API Token Configuration",
"The API token was not found in the VIRTFUSION_API_TOKEN environment variable or provider configuration block api_token attribute.",
)
}
if resp.Diagnostics.HasError() {
return
}
customTransport := &CustomTransport{
Transport: http.DefaultTransport,
BaseURL: &url.URL{Scheme: "https", Host: endpoint, Path: "/api/v1"},
Token: apiToken,
c, err := client.New(endpoint, apiToken)
if err != nil {
resp.Diagnostics.AddError("Failed to Create Client", err.Error())
return
}
// Example client configuration for data sources and resources
client := &http.Client{
Transport: customTransport,
}
resp.DataSourceData = client
resp.ResourceData = client
resp.DataSourceData = c
resp.ResourceData = c
}
func (p *VirtfusionProvider) Resources(ctx context.Context) []func() resource.Resource {
func (p *VirtfusionProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewVirtfusionServerResource,
NewVirtfusionServerBuildResource,
NewVirtfusionSSHResource,
NewServerResource,
NewServerBuildResource,
NewSSHKeyResource,
NewUserResource,
NewServerFirewallResource,
NewServerNetworkWhitelistResource,
NewServerIPv4Resource,
NewServerTrafficBlockResource,
NewIPBlockRangeResource,
NewSelfServiceCreditResource,
NewSelfServiceResourcePackResource,
NewSelfServiceHourlyGroupProfileResource,
NewSelfServiceResourceGroupProfileResource,
NewServerPowerActionResource,
NewServerPasswordResetResource,
NewUserPasswordResetResource,
NewUserAuthTokenResource,
NewUserServerAuthTokenResource,
NewSelfServicePackServersActionResource,
NewSelfServiceHourlyResourcePackResource,
}
}
func (p *VirtfusionProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{}
}
type CustomTransport struct {
Transport http.RoundTripper
BaseURL *url.URL
Token string
}
func (c *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Add("Authorization", "Bearer "+c.Token)
req.URL.Scheme = c.BaseURL.Scheme
req.URL.Host = c.BaseURL.Host
req.URL.Path = path.Join(c.BaseURL.Path, req.URL.Path)
return c.Transport.RoundTrip(req)
func (p *VirtfusionProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewHypervisorDataSource,
NewHypervisorsDataSource,
NewHypervisorGroupDataSource,
NewHypervisorGroupsDataSource,
NewHypervisorGroupResourcesDataSource,
NewServerDataSource,
NewServersDataSource,
NewServersByUserDataSource,
NewServerTemplatesDataSource,
NewServerTrafficDataSource,
NewServerTrafficBlocksDataSource,
NewServerVNCDataSource,
NewServerBackupsDataSource,
NewServerFirewallDataSource,
NewPackageDataSource,
NewPackagesDataSource,
NewPackageTemplatesDataSource,
NewIPBlockDataSource,
NewIPBlocksDataSource,
NewSSHKeyDataSource,
NewSSHKeysByUserDataSource,
NewUserDataSource,
NewDNSServiceDataSource,
NewISODataSource,
NewQueueItemDataSource,
NewSelfServiceCurrenciesDataSource,
NewSelfServiceResourcePackDataSource,
NewSelfServiceHourlyStatsDataSource,
NewSelfServiceReportDataSource,
NewSelfServiceUsageDataSource,
}
}
func New(version string) func() provider.Provider {

View File

@@ -0,0 +1,162 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"fmt"
"terraform-provider-virtfusion/internal/client"
"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"
)
// Ensure provider-defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &IPBlockRangeResource{}
_ resource.ResourceWithConfigure = &IPBlockRangeResource{}
)
// NewIPBlockRangeResource creates a new IP block range resource.
func NewIPBlockRangeResource() resource.Resource {
return &IPBlockRangeResource{}
}
// IPBlockRangeResource defines the resource implementation.
type IPBlockRangeResource struct {
client *client.Client
}
// IPBlockRangeResourceModel describes the resource data model.
type IPBlockRangeResourceModel struct {
ID types.String `tfsdk:"id"`
IPBlockID types.Int64 `tfsdk:"ip_block_id"`
StartIP types.String `tfsdk:"start_ip"`
EndIP types.String `tfsdk:"end_ip"`
Gateway types.String `tfsdk:"gateway"`
Netmask types.String `tfsdk:"netmask"`
}
func (r *IPBlockRangeResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_ip_block_range"
}
func (r *IPBlockRangeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Adds an IPv4 address range to a VirtFusion IP block. This is a create-only resource — ranges cannot be deleted via the API.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "The identifier of the IP block range.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"ip_block_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the IP block to add the range to.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
"start_ip": schema.StringAttribute{
MarkdownDescription: "The starting IP address of the range.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"end_ip": schema.StringAttribute{
MarkdownDescription: "The ending IP address of the range.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"gateway": schema.StringAttribute{
MarkdownDescription: "The gateway address for the range.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"netmask": schema.StringAttribute{
MarkdownDescription: "The netmask for the range.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
},
}
}
func (r *IPBlockRangeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = c
}
func (r *IPBlockRangeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data IPBlockRangeResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
rangeReq := client.IPBlockRangeRequest{
StartIP: data.StartIP.ValueString(),
EndIP: data.EndIP.ValueString(),
Gateway: data.Gateway.ValueString(),
Netmask: data.Netmask.ValueString(),
}
apiPath := fmt.Sprintf("/connectivity/ipblocks/%d/ipv4", data.IPBlockID.ValueInt64())
_, err := r.client.Post(ctx, apiPath, rangeReq)
if err != nil {
resp.Diagnostics.AddError(
"Error Creating IP Block Range",
fmt.Sprintf("Could not create IP block range on block %d: %s", data.IPBlockID.ValueInt64(), err),
)
return
}
// Generate a composite ID since the API does not return one.
data.ID = types.StringValue(fmt.Sprintf("%d/%s-%s", data.IPBlockID.ValueInt64(), data.StartIP.ValueString(), data.EndIP.ValueString()))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *IPBlockRangeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data IPBlockRangeResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *IPBlockRangeResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) {
// All attributes require replacement — updates are never called.
}
func (r *IPBlockRangeResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
// No delete API — removing from state only.
}

View File

@@ -0,0 +1,173 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"errors"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/float64planmodifier"
"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"
)
// Ensure provider-defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &SelfServiceCreditResource{}
_ resource.ResourceWithConfigure = &SelfServiceCreditResource{}
)
// NewSelfServiceCreditResource creates a new self-service credit resource.
func NewSelfServiceCreditResource() resource.Resource {
return &SelfServiceCreditResource{}
}
// SelfServiceCreditResource defines the resource implementation.
type SelfServiceCreditResource struct {
client *client.Client
}
// SelfServiceCreditResourceModel describes the resource data model.
type SelfServiceCreditResourceModel struct {
ID types.Int64 `tfsdk:"id"`
Amount types.Float64 `tfsdk:"amount"`
CurrencyCode types.String `tfsdk:"currency_code"`
UserID types.Int64 `tfsdk:"user_id"`
}
func (r *SelfServiceCreditResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_self_service_credit"
}
func (r *SelfServiceCreditResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Manages self-service credit in VirtFusion. Deleting this resource cancels the credit.",
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The identifier of the credit entry.",
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
},
"amount": schema.Float64Attribute{
MarkdownDescription: "The credit amount.",
Required: true,
PlanModifiers: []planmodifier.Float64{
float64planmodifier.RequiresReplace(),
},
},
"currency_code": schema.StringAttribute{
MarkdownDescription: "The currency code (e.g. `USD`, `EUR`).",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"user_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the user to add credit to.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
},
}
}
func (r *SelfServiceCreditResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = c
}
func (r *SelfServiceCreditResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data SelfServiceCreditResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
creditReq := client.SelfServiceCreditRequest{
Amount: data.Amount.ValueFloat64(),
CurrencyCode: data.CurrencyCode.ValueString(),
UserID: data.UserID.ValueInt64(),
}
respBody, err := r.client.Post(ctx, "/selfService/credit", creditReq)
if err != nil {
resp.Diagnostics.AddError(
"Error Creating Credit",
fmt.Sprintf("Could not create credit for user %d: %s", data.UserID.ValueInt64(), err),
)
return
}
var creditResp client.SelfServiceCreditResponse
if err := json.Unmarshal(respBody, &creditResp); err != nil {
resp.Diagnostics.AddError(
"Error Parsing Response",
fmt.Sprintf("Could not parse credit response: %s", err),
)
return
}
data.ID = types.Int64Value(creditResp.Data.ID)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SelfServiceCreditResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data SelfServiceCreditResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SelfServiceCreditResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) {
// All attributes require replacement — updates are never called.
}
func (r *SelfServiceCreditResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data SelfServiceCreditResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
apiPath := fmt.Sprintf("/selfService/credit/%d", data.ID.ValueInt64())
_, err := r.client.Delete(ctx, apiPath)
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
return
}
resp.Diagnostics.AddError(
"Error Cancelling Credit",
fmt.Sprintf("Could not cancel credit %d: %s", data.ID.ValueInt64(), err),
)
return
}
}

View File

@@ -0,0 +1,172 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"errors"
"fmt"
"terraform-provider-virtfusion/internal/client"
"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"
)
// Ensure provider-defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &SelfServiceHourlyGroupProfileResource{}
_ resource.ResourceWithConfigure = &SelfServiceHourlyGroupProfileResource{}
)
// NewSelfServiceHourlyGroupProfileResource creates a new self-service hourly group profile resource.
func NewSelfServiceHourlyGroupProfileResource() resource.Resource {
return &SelfServiceHourlyGroupProfileResource{}
}
// SelfServiceHourlyGroupProfileResource defines the resource implementation.
type SelfServiceHourlyGroupProfileResource struct {
client *client.Client
}
// SelfServiceHourlyGroupProfileResourceModel describes the resource data model.
type SelfServiceHourlyGroupProfileResourceModel struct {
ID types.String `tfsdk:"id"`
UserID types.Int64 `tfsdk:"user_id"`
GroupID types.Int64 `tfsdk:"group_id"`
ProfileID types.Int64 `tfsdk:"profile_id"`
}
func (r *SelfServiceHourlyGroupProfileResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_self_service_hourly_group_profile"
}
func (r *SelfServiceHourlyGroupProfileResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Manages a self-service hourly group profile assignment in VirtFusion.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "The composite identifier of the hourly group profile (userId/groupId/profileId).",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"user_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the user.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
"group_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the hypervisor group.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
"profile_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the hourly profile.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
},
}
}
func (r *SelfServiceHourlyGroupProfileResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = c
}
func (r *SelfServiceHourlyGroupProfileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data SelfServiceHourlyGroupProfileResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
profileReq := map[string]int64{
"userId": data.UserID.ValueInt64(),
"groupId": data.GroupID.ValueInt64(),
"profileId": data.ProfileID.ValueInt64(),
}
_, err := r.client.Post(ctx, "/selfService/hourlyGroupProfile", profileReq)
if err != nil {
resp.Diagnostics.AddError(
"Error Creating Hourly Group Profile",
fmt.Sprintf("Could not create hourly group profile for user %d: %s", data.UserID.ValueInt64(), err),
)
return
}
// Generate a composite ID.
data.ID = types.StringValue(fmt.Sprintf("%d/%d/%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ProfileID.ValueInt64()))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SelfServiceHourlyGroupProfileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data SelfServiceHourlyGroupProfileResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SelfServiceHourlyGroupProfileResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) {
// All attributes require replacement — updates are never called.
}
func (r *SelfServiceHourlyGroupProfileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data SelfServiceHourlyGroupProfileResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
apiPath := fmt.Sprintf("/selfService/hourlyGroupProfile/%d/%d/%d",
data.UserID.ValueInt64(),
data.GroupID.ValueInt64(),
data.ProfileID.ValueInt64(),
)
_, err := r.client.Delete(ctx, apiPath)
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
return
}
resp.Diagnostics.AddError(
"Error Deleting Hourly Group Profile",
fmt.Sprintf("Could not delete hourly group profile for user %d, group %d, profile %d: %s",
data.UserID.ValueInt64(),
data.GroupID.ValueInt64(),
data.ProfileID.ValueInt64(),
err,
),
)
return
}
}

View File

@@ -0,0 +1,186 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"fmt"
"terraform-provider-virtfusion/internal/client"
"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/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"
)
// Ensure provider-defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &SelfServiceHourlyResourcePackResource{}
_ resource.ResourceWithConfigure = &SelfServiceHourlyResourcePackResource{}
)
// NewSelfServiceHourlyResourcePackResource creates a new self-service hourly resource pack resource.
func NewSelfServiceHourlyResourcePackResource() resource.Resource {
return &SelfServiceHourlyResourcePackResource{}
}
// SelfServiceHourlyResourcePackResource defines the resource implementation.
type SelfServiceHourlyResourcePackResource struct {
client *client.Client
}
// SelfServiceHourlyResourcePackResourceModel describes the resource data model.
type SelfServiceHourlyResourcePackResourceModel struct {
ID types.String `tfsdk:"id"`
UserID types.Int64 `tfsdk:"user_id"`
GroupID types.Int64 `tfsdk:"group_id"`
ResourcePackID types.Int64 `tfsdk:"resource_pack_id"`
}
func (r *SelfServiceHourlyResourcePackResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_self_service_hourly_resource_pack"
}
func (r *SelfServiceHourlyResourcePackResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Sets the hourly resource pack for a user and group in VirtFusion self-service. Changing any attribute forces recreation.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "The composite identifier for this hourly resource pack assignment.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"user_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the user.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
"group_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the group.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
"resource_pack_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the resource pack.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
},
}
}
func (r *SelfServiceHourlyResourcePackResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = c
}
func (r *SelfServiceHourlyResourcePackResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data SelfServiceHourlyResourcePackResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
apiPath := fmt.Sprintf("/selfService/hourlyResourcePack/byUser/%d/group/%d/resourcePack/%d",
data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ResourcePackID.ValueInt64())
_, err := r.client.Put(ctx, apiPath, nil)
if err != nil {
resp.Diagnostics.AddError(
"Error Setting Hourly Resource Pack",
fmt.Sprintf("Could not set hourly resource pack (user=%d, group=%d, resource_pack=%d): %s",
data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ResourcePackID.ValueInt64(), err),
)
return
}
data.ID = types.StringValue(fmt.Sprintf("%d-%d-%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ResourcePackID.ValueInt64()))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SelfServiceHourlyResourcePackResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data SelfServiceHourlyResourcePackResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Return stored state as-is. The API does not provide a direct read endpoint for this assignment.
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SelfServiceHourlyResourcePackResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// All attributes have RequiresReplace, so Update should never be called.
var data SelfServiceHourlyResourcePackResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SelfServiceHourlyResourcePackResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
// No-op: removing the hourly resource pack assignment from state only.
}
// ValidateConfig validates the resource configuration.
func (r *SelfServiceHourlyResourcePackResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var data SelfServiceHourlyResourcePackResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Validate user_id is positive.
if !data.UserID.IsNull() && !data.UserID.IsUnknown() && data.UserID.ValueInt64() <= 0 {
resp.Diagnostics.AddAttributeError(
path.Root("user_id"),
"Invalid User ID",
"user_id must be a positive integer.",
)
}
// Validate group_id is positive.
if !data.GroupID.IsNull() && !data.GroupID.IsUnknown() && data.GroupID.ValueInt64() <= 0 {
resp.Diagnostics.AddAttributeError(
path.Root("group_id"),
"Invalid Group ID",
"group_id must be a positive integer.",
)
}
// Validate resource_pack_id is positive.
if !data.ResourcePackID.IsNull() && !data.ResourcePackID.IsUnknown() && data.ResourcePackID.ValueInt64() <= 0 {
resp.Diagnostics.AddAttributeError(
path.Root("resource_pack_id"),
"Invalid Resource Pack ID",
"resource_pack_id must be a positive integer.",
)
}
}

View File

@@ -0,0 +1,204 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"fmt"
"time"
"terraform-provider-virtfusion/internal/client"
"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/mapplanmodifier"
"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"
)
// Ensure provider-defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &SelfServicePackServersActionResource{}
_ resource.ResourceWithConfigure = &SelfServicePackServersActionResource{}
)
// NewSelfServicePackServersActionResource creates a new self-service pack servers action resource.
func NewSelfServicePackServersActionResource() resource.Resource {
return &SelfServicePackServersActionResource{}
}
// SelfServicePackServersActionResource defines the resource implementation.
type SelfServicePackServersActionResource struct {
client *client.Client
}
// SelfServicePackServersActionResourceModel describes the resource data model.
type SelfServicePackServersActionResourceModel struct {
ID types.String `tfsdk:"id"`
PackID types.Int64 `tfsdk:"pack_id"`
Action types.String `tfsdk:"action"`
Triggers types.Map `tfsdk:"triggers"`
}
func (r *SelfServicePackServersActionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_self_service_pack_servers_action"
}
func (r *SelfServicePackServersActionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Performs an action on all servers in a self-service resource pack. This is a trigger-style resource — the action is executed on create and can be re-triggered by changing the `triggers` attribute.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "The identifier for this pack servers action.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"pack_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the resource pack.",
Required: true,
},
"action": schema.StringAttribute{
MarkdownDescription: "The action to perform on the pack servers. Must be one of: `suspend`, `unsuspend`, `delete`.",
Required: true,
},
"triggers": schema.MapAttribute{
MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the action to be re-executed. Works like `triggers` in `terraform_data`.",
ElementType: types.StringType,
Optional: true,
PlanModifiers: []planmodifier.Map{
mapplanmodifier.RequiresReplace(),
},
},
},
}
}
func (r *SelfServicePackServersActionResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = c
}
func (r *SelfServicePackServersActionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data SelfServicePackServersActionResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
action := data.Action.ValueString()
packID := data.PackID.ValueInt64()
switch action {
case "suspend":
apiPath := fmt.Sprintf("/selfService/resourcePack/%d/servers/suspend", packID)
_, err := r.client.Post(ctx, apiPath, nil)
if err != nil {
resp.Diagnostics.AddError(
"Error Suspending Pack Servers",
fmt.Sprintf("Could not suspend servers for resource pack %d: %s", packID, err),
)
return
}
case "unsuspend":
apiPath := fmt.Sprintf("/selfService/resourcePack/%d/servers/unsuspend", packID)
_, err := r.client.Post(ctx, apiPath, nil)
if err != nil {
resp.Diagnostics.AddError(
"Error Unsuspending Pack Servers",
fmt.Sprintf("Could not unsuspend servers for resource pack %d: %s", packID, err),
)
return
}
case "delete":
apiPath := fmt.Sprintf("/selfService/resourcePack/%d/servers", packID)
_, err := r.client.Delete(ctx, apiPath)
if err != nil {
resp.Diagnostics.AddError(
"Error Deleting Pack Servers",
fmt.Sprintf("Could not delete servers for resource pack %d: %s", packID, err),
)
return
}
}
data.ID = types.StringValue(fmt.Sprintf("%d-%s-%d", packID, action, time.Now().UnixNano()))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SelfServicePackServersActionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data SelfServicePackServersActionResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Return stored state as-is for trigger-style resources.
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SelfServicePackServersActionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data SelfServicePackServersActionResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SelfServicePackServersActionResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
// No-op: pack server actions are not reversible. Removing from state only.
}
// ValidateConfig validates the resource configuration.
func (r *SelfServicePackServersActionResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var data SelfServicePackServersActionResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Validate pack_id is positive.
if !data.PackID.IsNull() && !data.PackID.IsUnknown() && data.PackID.ValueInt64() <= 0 {
resp.Diagnostics.AddAttributeError(
path.Root("pack_id"),
"Invalid Pack ID",
"pack_id must be a positive integer.",
)
}
// Validate action is one of the allowed values.
if !data.Action.IsNull() && !data.Action.IsUnknown() {
action := data.Action.ValueString()
validActions := map[string]bool{
"suspend": true,
"unsuspend": true,
"delete": true,
}
if !validActions[action] {
resp.Diagnostics.AddAttributeError(
path.Root("action"),
"Invalid Pack Servers Action",
fmt.Sprintf("action must be one of: suspend, unsuspend, delete. Got: %q", action),
)
}
}
}

View File

@@ -0,0 +1,206 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"fmt"
"terraform-provider-virtfusion/internal/client"
"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/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"
)
// Ensure provider-defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &SelfServiceResourceGroupProfileResource{}
_ resource.ResourceWithConfigure = &SelfServiceResourceGroupProfileResource{}
)
// NewSelfServiceResourceGroupProfileResource creates a new self-service resource group profile resource.
func NewSelfServiceResourceGroupProfileResource() resource.Resource {
return &SelfServiceResourceGroupProfileResource{}
}
// SelfServiceResourceGroupProfileResource defines the resource implementation.
type SelfServiceResourceGroupProfileResource struct {
client *client.Client
}
// SelfServiceResourceGroupProfileResourceModel describes the resource data model.
type SelfServiceResourceGroupProfileResourceModel struct {
ID types.String `tfsdk:"id"`
UserID types.Int64 `tfsdk:"user_id"`
GroupID types.Int64 `tfsdk:"group_id"`
ProfileID types.Int64 `tfsdk:"profile_id"`
}
func (r *SelfServiceResourceGroupProfileResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_self_service_resource_group_profile"
}
func (r *SelfServiceResourceGroupProfileResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Associates a resource group profile with a user in VirtFusion self-service. Changing any attribute forces recreation of the association.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "The composite identifier for this resource group profile association.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"user_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the user.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
"group_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the resource group.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
"profile_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the profile.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
},
}
}
func (r *SelfServiceResourceGroupProfileResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = c
}
func (r *SelfServiceResourceGroupProfileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data SelfServiceResourceGroupProfileResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
body := map[string]int64{
"userId": data.UserID.ValueInt64(),
"groupId": data.GroupID.ValueInt64(),
"profileId": data.ProfileID.ValueInt64(),
}
_, err := r.client.Post(ctx, "/selfService/resourceGroupProfile", body)
if err != nil {
resp.Diagnostics.AddError(
"Error Creating Resource Group Profile Association",
fmt.Sprintf("Could not create resource group profile association (user=%d, group=%d, profile=%d): %s",
data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ProfileID.ValueInt64(), err),
)
return
}
data.ID = types.StringValue(fmt.Sprintf("%d-%d-%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ProfileID.ValueInt64()))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SelfServiceResourceGroupProfileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data SelfServiceResourceGroupProfileResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Return stored state as-is. The API does not provide a direct read endpoint for this association.
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SelfServiceResourceGroupProfileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// All attributes have RequiresReplace, so Update should never be called.
var data SelfServiceResourceGroupProfileResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SelfServiceResourceGroupProfileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data SelfServiceResourceGroupProfileResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
apiPath := fmt.Sprintf("/selfService/resourceGroupProfile/%d/%d/%d",
data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ProfileID.ValueInt64())
_, err := r.client.Delete(ctx, apiPath)
if err != nil {
resp.Diagnostics.AddError(
"Error Deleting Resource Group Profile Association",
fmt.Sprintf("Could not delete resource group profile association (user=%d, group=%d, profile=%d): %s",
data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ProfileID.ValueInt64(), err),
)
return
}
}
// ValidateConfig validates the resource configuration.
func (r *SelfServiceResourceGroupProfileResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var data SelfServiceResourceGroupProfileResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Validate user_id is positive.
if !data.UserID.IsNull() && !data.UserID.IsUnknown() && data.UserID.ValueInt64() <= 0 {
resp.Diagnostics.AddAttributeError(
path.Root("user_id"),
"Invalid User ID",
"user_id must be a positive integer.",
)
}
// Validate group_id is positive.
if !data.GroupID.IsNull() && !data.GroupID.IsUnknown() && data.GroupID.ValueInt64() <= 0 {
resp.Diagnostics.AddAttributeError(
path.Root("group_id"),
"Invalid Group ID",
"group_id must be a positive integer.",
)
}
// Validate profile_id is positive.
if !data.ProfileID.IsNull() && !data.ProfileID.IsUnknown() && data.ProfileID.ValueInt64() <= 0 {
resp.Diagnostics.AddAttributeError(
path.Root("profile_id"),
"Invalid Profile ID",
"profile_id must be a positive integer.",
)
}
}

View File

@@ -0,0 +1,215 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"errors"
"fmt"
"terraform-provider-virtfusion/internal/client"
"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/types"
)
// Ensure provider-defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &SelfServiceResourcePackResource{}
_ resource.ResourceWithConfigure = &SelfServiceResourcePackResource{}
)
// NewSelfServiceResourcePackResource creates a new self-service resource pack resource.
func NewSelfServiceResourcePackResource() resource.Resource {
return &SelfServiceResourcePackResource{}
}
// SelfServiceResourcePackResource defines the resource implementation.
type SelfServiceResourcePackResource struct {
client *client.Client
}
// SelfServiceResourcePackResourceModel describes the resource data model.
type SelfServiceResourcePackResourceModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
UserID types.Int64 `tfsdk:"user_id"`
PackID types.Int64 `tfsdk:"pack_id"`
}
func (r *SelfServiceResourcePackResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_self_service_resource_pack"
}
func (r *SelfServiceResourcePackResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Manages a self-service resource pack in VirtFusion.",
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The identifier of the resource pack.",
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
},
"name": schema.StringAttribute{
MarkdownDescription: "The name of the resource pack.",
Required: true,
},
"user_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the user who owns the resource pack.",
Required: true,
},
"pack_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the pack.",
Required: true,
},
},
}
}
func (r *SelfServiceResourcePackResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = c
}
func (r *SelfServiceResourcePackResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data SelfServiceResourcePackResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
packReq := client.SelfServiceResourcePackRequest{
Name: data.Name.ValueString(),
UserID: data.UserID.ValueInt64(),
PackID: data.PackID.ValueInt64(),
}
respBody, err := r.client.Post(ctx, "/selfService/resourcePack", packReq)
if err != nil {
resp.Diagnostics.AddError(
"Error Creating Resource Pack",
fmt.Sprintf("Could not create resource pack: %s", err),
)
return
}
var packResp client.SelfServiceResourcePackResponse
if err := json.Unmarshal(respBody, &packResp); err != nil {
resp.Diagnostics.AddError(
"Error Parsing Response",
fmt.Sprintf("Could not parse resource pack response: %s", err),
)
return
}
data.ID = types.Int64Value(packResp.Data.ID)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SelfServiceResourcePackResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data SelfServiceResourcePackResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
apiPath := fmt.Sprintf("/selfService/resourcePack/%d", data.ID.ValueInt64())
respBody, err := r.client.Get(ctx, apiPath)
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
// Resource no longer exists, remove from state.
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError(
"Error Reading Resource Pack",
fmt.Sprintf("Could not read resource pack %d: %s", data.ID.ValueInt64(), err),
)
return
}
var packResp client.SelfServiceResourcePackResponse
if err := json.Unmarshal(respBody, &packResp); err != nil {
resp.Diagnostics.AddError(
"Error Parsing Response",
fmt.Sprintf("Could not parse resource pack response: %s", err),
)
return
}
data.Name = types.StringValue(packResp.Data.Name)
data.UserID = types.Int64Value(packResp.Data.UserID)
data.PackID = types.Int64Value(packResp.Data.PackID)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SelfServiceResourcePackResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data SelfServiceResourcePackResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
packReq := client.SelfServiceResourcePackRequest{
Name: data.Name.ValueString(),
UserID: data.UserID.ValueInt64(),
PackID: data.PackID.ValueInt64(),
}
apiPath := fmt.Sprintf("/selfService/resourcePack/%d", data.ID.ValueInt64())
_, err := r.client.Put(ctx, apiPath, packReq)
if err != nil {
resp.Diagnostics.AddError(
"Error Updating Resource Pack",
fmt.Sprintf("Could not update resource pack %d: %s", data.ID.ValueInt64(), err),
)
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SelfServiceResourcePackResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data SelfServiceResourcePackResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
apiPath := fmt.Sprintf("/selfService/resourcePack/%d", data.ID.ValueInt64())
_, err := r.client.Delete(ctx, apiPath)
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
return
}
resp.Diagnostics.AddError(
"Error Deleting Resource Pack",
fmt.Sprintf("Could not delete resource pack %d: %s", data.ID.ValueInt64(), err),
)
return
}
}

View File

@@ -0,0 +1,637 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"terraform-provider-virtfusion/internal/client"
"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/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"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"
)
// Ensure provider defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &ServerResource{}
_ resource.ResourceWithConfigure = &ServerResource{}
_ resource.ResourceWithImportState = &ServerResource{}
)
// NewServerResource returns a new server resource.
func NewServerResource() resource.Resource {
return &ServerResource{}
}
// ServerResource defines the resource implementation.
type ServerResource struct {
client *client.Client
}
// ServerResourceModel describes the resource data model.
type ServerResourceModel struct {
// Computed
ID types.Int64 `tfsdk:"id"`
UUID types.String `tfsdk:"uuid"`
Hostname types.String `tfsdk:"hostname"`
// Required (create)
PackageID types.Int64 `tfsdk:"package_id"`
UserID types.Int64 `tfsdk:"user_id"`
HypervisorID types.Int64 `tfsdk:"hypervisor_id"`
// Optional (create)
Ipv4 types.Int64 `tfsdk:"ipv4"`
Storage types.Int64 `tfsdk:"storage"`
Memory types.Int64 `tfsdk:"memory"`
Cores types.Int64 `tfsdk:"cores"`
Traffic types.Int64 `tfsdk:"traffic"`
InboundNetworkSpeed types.Int64 `tfsdk:"inbound_network_speed"`
OutboundNetworkSpeed types.Int64 `tfsdk:"outbound_network_speed"`
StorageProfile types.Int64 `tfsdk:"storage_profile"`
NetworkProfile types.Int64 `tfsdk:"network_profile"`
DryRun types.Bool `tfsdk:"dry_run"`
AdditionalStorage1 types.Int64 `tfsdk:"additional_storage_1"`
AdditionalStorage1Profile types.Int64 `tfsdk:"additional_storage_1_profile"`
AdditionalStorage2 types.Int64 `tfsdk:"additional_storage_2"`
AdditionalStorage2Profile types.Int64 `tfsdk:"additional_storage_2_profile"`
// Optional (update-only)
Name types.String `tfsdk:"name"`
CPUThrottle types.Int64 `tfsdk:"cpu_throttle"`
VNCEnabled types.Bool `tfsdk:"vnc_enabled"`
Suspended types.Bool `tfsdk:"suspended"`
BackupPlanID types.Int64 `tfsdk:"backup_plan_id"`
CustomXML types.String `tfsdk:"custom_xml"`
OwnerUserID types.Int64 `tfsdk:"owner_user_id"`
}
func (r *ServerResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server"
}
func (r *ServerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Manages a VirtFusion server.",
Attributes: map[string]schema.Attribute{
// Computed
"id": schema.Int64Attribute{
MarkdownDescription: "The server ID.",
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
},
"uuid": schema.StringAttribute{
MarkdownDescription: "The server UUID.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"hostname": schema.StringAttribute{
MarkdownDescription: "The server hostname.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
// Required
"package_id": schema.Int64Attribute{
MarkdownDescription: "The package ID for the server.",
Required: true,
},
"user_id": schema.Int64Attribute{
MarkdownDescription: "The user ID who owns the server.",
Required: true,
},
"hypervisor_id": schema.Int64Attribute{
MarkdownDescription: "The hypervisor ID where the server will be created.",
Required: true,
},
// Optional (create)
"ipv4": schema.Int64Attribute{
MarkdownDescription: "Number of IPv4 addresses to assign. Defaults to 1.",
Optional: true,
Computed: true,
Default: int64default.StaticInt64(1),
},
"storage": schema.Int64Attribute{
MarkdownDescription: "Storage size override in GB.",
Optional: true,
},
"memory": schema.Int64Attribute{
MarkdownDescription: "Memory size override in MB.",
Optional: true,
},
"cores": schema.Int64Attribute{
MarkdownDescription: "Number of CPU cores override.",
Optional: true,
},
"traffic": schema.Int64Attribute{
MarkdownDescription: "Traffic limit override in GB.",
Optional: true,
},
"inbound_network_speed": schema.Int64Attribute{
MarkdownDescription: "Inbound network speed override in Mbps.",
Optional: true,
},
"outbound_network_speed": schema.Int64Attribute{
MarkdownDescription: "Outbound network speed override in Mbps.",
Optional: true,
},
"storage_profile": schema.Int64Attribute{
MarkdownDescription: "Storage profile ID.",
Optional: true,
},
"network_profile": schema.Int64Attribute{
MarkdownDescription: "Network profile ID.",
Optional: true,
},
"dry_run": schema.BoolAttribute{
MarkdownDescription: "If true, validates the request without creating the server.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
"additional_storage_1": schema.Int64Attribute{
MarkdownDescription: "Additional storage 1 size in GB.",
Optional: true,
},
"additional_storage_1_profile": schema.Int64Attribute{
MarkdownDescription: "Additional storage 1 profile ID.",
Optional: true,
},
"additional_storage_2": schema.Int64Attribute{
MarkdownDescription: "Additional storage 2 size in GB.",
Optional: true,
},
"additional_storage_2_profile": schema.Int64Attribute{
MarkdownDescription: "Additional storage 2 profile ID.",
Optional: true,
},
// Optional (update-only)
"name": schema.StringAttribute{
MarkdownDescription: "The server display name.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"cpu_throttle": schema.Int64Attribute{
MarkdownDescription: "CPU throttle percentage (0-100).",
Optional: true,
},
"vnc_enabled": schema.BoolAttribute{
MarkdownDescription: "Whether VNC is enabled on the server.",
Optional: true,
},
"suspended": schema.BoolAttribute{
MarkdownDescription: "Whether the server is suspended.",
Optional: true,
},
"backup_plan_id": schema.Int64Attribute{
MarkdownDescription: "Backup plan ID. Set to 0 to remove the backup plan.",
Optional: true,
},
"custom_xml": schema.StringAttribute{
MarkdownDescription: "Custom XML configuration for the server.",
Optional: true,
},
"owner_user_id": schema.Int64Attribute{
MarkdownDescription: "The user ID to transfer ownership to.",
Optional: true,
},
},
}
}
func (r *ServerResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = c
}
func (r *ServerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan ServerResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
// Build the create request from plan values.
createReq := client.ServerCreateRequest{
PackageID: plan.PackageID.ValueInt64(),
UserID: plan.UserID.ValueInt64(),
HypervisorID: plan.HypervisorID.ValueInt64(),
}
if !plan.Ipv4.IsNull() && !plan.Ipv4.IsUnknown() {
v := plan.Ipv4.ValueInt64()
createReq.Ipv4 = &v
}
if !plan.Storage.IsNull() && !plan.Storage.IsUnknown() {
v := plan.Storage.ValueInt64()
createReq.Storage = &v
}
if !plan.Memory.IsNull() && !plan.Memory.IsUnknown() {
v := plan.Memory.ValueInt64()
createReq.Memory = &v
}
if !plan.Cores.IsNull() && !plan.Cores.IsUnknown() {
v := plan.Cores.ValueInt64()
createReq.CPUCores = &v
}
if !plan.Traffic.IsNull() && !plan.Traffic.IsUnknown() {
v := plan.Traffic.ValueInt64()
createReq.Traffic = &v
}
if !plan.InboundNetworkSpeed.IsNull() && !plan.InboundNetworkSpeed.IsUnknown() {
v := plan.InboundNetworkSpeed.ValueInt64()
createReq.NetworkSpeedInbound = &v
}
if !plan.OutboundNetworkSpeed.IsNull() && !plan.OutboundNetworkSpeed.IsUnknown() {
v := plan.OutboundNetworkSpeed.ValueInt64()
createReq.NetworkSpeedOutbound = &v
}
if !plan.StorageProfile.IsNull() && !plan.StorageProfile.IsUnknown() {
v := plan.StorageProfile.ValueInt64()
createReq.StorageProfile = &v
}
if !plan.NetworkProfile.IsNull() && !plan.NetworkProfile.IsUnknown() {
v := plan.NetworkProfile.ValueInt64()
createReq.NetworkProfile = &v
}
if !plan.DryRun.IsNull() && !plan.DryRun.IsUnknown() {
v := plan.DryRun.ValueBool()
createReq.DryRun = &v
}
if !plan.AdditionalStorage1.IsNull() && !plan.AdditionalStorage1.IsUnknown() {
v := plan.AdditionalStorage1.ValueInt64()
createReq.AdditionalStorage1 = &v
}
if !plan.AdditionalStorage1Profile.IsNull() && !plan.AdditionalStorage1Profile.IsUnknown() {
v := plan.AdditionalStorage1Profile.ValueInt64()
createReq.AdditionalStorage1Profile = &v
}
if !plan.AdditionalStorage2.IsNull() && !plan.AdditionalStorage2.IsUnknown() {
v := plan.AdditionalStorage2.ValueInt64()
createReq.AdditionalStorage2 = &v
}
if !plan.AdditionalStorage2Profile.IsNull() && !plan.AdditionalStorage2Profile.IsUnknown() {
v := plan.AdditionalStorage2Profile.ValueInt64()
createReq.AdditionalStorage2Profile = &v
}
// Create the server.
rawResp, err := r.client.Post(ctx, "/servers", createReq)
if err != nil {
resp.Diagnostics.AddError("Error Creating Server", err.Error())
return
}
// Parse the create response.
var serverResp client.ServerResponse
if err := json.Unmarshal(rawResp, &serverResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Server Response", err.Error())
return
}
// Set computed values from the response.
plan.ID = types.Int64Value(serverResp.Data.ID)
plan.UUID = types.StringValue(serverResp.Data.UUID)
plan.Hostname = types.StringValue(serverResp.Data.Hostname)
// If name was not set in the plan, use the name from the API response.
if plan.Name.IsNull() || plan.Name.IsUnknown() {
plan.Name = types.StringValue(serverResp.Data.Name)
}
// After creation, apply update-only attributes if they are set.
serverID := serverResp.Data.ID
// Apply name if explicitly set in the plan.
if !plan.Name.IsNull() && !plan.Name.IsUnknown() && plan.Name.ValueString() != serverResp.Data.Name {
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/name", serverID), client.ServerModifyNameRequest{
Name: plan.Name.ValueString(),
})
if err != nil {
resp.Diagnostics.AddError("Error Setting Server Name", err.Error())
return
}
}
// Apply CPU throttle if set.
if !plan.CPUThrottle.IsNull() && !plan.CPUThrottle.IsUnknown() {
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/cpuThrottle", serverID), client.ServerModifyCPUThrottleRequest{
Percentage: plan.CPUThrottle.ValueInt64(),
})
if err != nil {
resp.Diagnostics.AddError("Error Setting CPU Throttle", err.Error())
return
}
}
// Apply VNC if set.
if !plan.VNCEnabled.IsNull() && !plan.VNCEnabled.IsUnknown() && plan.VNCEnabled.ValueBool() {
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/vnc", serverID), nil)
if err != nil {
resp.Diagnostics.AddError("Error Enabling VNC", err.Error())
return
}
}
// Apply suspended if set to true.
if !plan.Suspended.IsNull() && !plan.Suspended.IsUnknown() && plan.Suspended.ValueBool() {
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/suspend", serverID), nil)
if err != nil {
resp.Diagnostics.AddError("Error Suspending Server", err.Error())
return
}
}
// Apply backup plan if set.
if !plan.BackupPlanID.IsNull() && !plan.BackupPlanID.IsUnknown() {
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/backups/plan/%d", serverID, plan.BackupPlanID.ValueInt64()), nil)
if err != nil {
resp.Diagnostics.AddError("Error Setting Backup Plan", err.Error())
return
}
}
// Apply custom XML if set.
if !plan.CustomXML.IsNull() && !plan.CustomXML.IsUnknown() {
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/customXML", serverID), client.ServerCustomXMLRequest{
XML: plan.CustomXML.ValueString(),
})
if err != nil {
resp.Diagnostics.AddError("Error Setting Custom XML", err.Error())
return
}
}
// Apply owner change if set and different from the creating user.
if !plan.OwnerUserID.IsNull() && !plan.OwnerUserID.IsUnknown() && plan.OwnerUserID.ValueInt64() != plan.UserID.ValueInt64() {
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/owner/%d", serverID, plan.OwnerUserID.ValueInt64()), nil)
if err != nil {
resp.Diagnostics.AddError("Error Changing Server Owner", err.Error())
return
}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *ServerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state ServerResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
rawResp, err := r.client.Get(ctx, fmt.Sprintf("/servers/%d", state.ID.ValueInt64()))
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Error Reading Server", err.Error())
return
}
var serverResp client.ServerResponse
if err := json.Unmarshal(rawResp, &serverResp); err != nil {
resp.Diagnostics.AddError("Error Parsing Server Response", err.Error())
return
}
// Map API response to state model.
s := serverResp.Data
state.ID = types.Int64Value(s.ID)
state.UUID = types.StringValue(s.UUID)
state.Hostname = types.StringValue(s.Hostname)
state.Name = types.StringValue(s.Name)
state.HypervisorID = types.Int64Value(s.HypervisorID)
// Map optional create attributes from the nested API response if they were set in state.
if !state.Storage.IsNull() && s.Settings != nil && s.Settings.Resources != nil {
state.Storage = types.Int64Value(s.Settings.Resources.Storage)
}
if !state.Memory.IsNull() && s.Settings != nil && s.Settings.Resources != nil {
state.Memory = types.Int64Value(s.Settings.Resources.Memory)
}
if !state.Cores.IsNull() && s.CPU != nil {
state.Cores = types.Int64Value(s.CPU.Cores)
}
if !state.Traffic.IsNull() && s.Settings != nil && s.Settings.Resources != nil {
state.Traffic = types.Int64Value(s.Settings.Resources.Traffic)
}
// NetworkSpeed and profiles are not returned by the detail API; preserve from state.
if !state.Suspended.IsNull() {
state.Suspended = types.BoolValue(s.Suspended)
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *ServerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state ServerResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
serverID := state.ID.ValueInt64()
// Preserve computed values from state.
plan.ID = state.ID
plan.UUID = state.UUID
plan.Hostname = state.Hostname
// Name change.
if !plan.Name.Equal(state.Name) {
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/name", serverID), client.ServerModifyNameRequest{
Name: plan.Name.ValueString(),
})
if err != nil {
resp.Diagnostics.AddError("Error Modifying Server Name", err.Error())
return
}
}
// CPU cores change.
if !plan.Cores.Equal(state.Cores) && !plan.Cores.IsNull() {
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/cpuCores", serverID), client.ServerModifyCPURequest{
CPUCores: plan.Cores.ValueInt64(),
})
if err != nil {
resp.Diagnostics.AddError("Error Modifying Server CPU Cores", err.Error())
return
}
}
// Memory change.
if !plan.Memory.Equal(state.Memory) && !plan.Memory.IsNull() {
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/memory", serverID), client.ServerModifyMemoryRequest{
Memory: plan.Memory.ValueInt64(),
})
if err != nil {
resp.Diagnostics.AddError("Error Modifying Server Memory", err.Error())
return
}
}
// Traffic change.
if !plan.Traffic.Equal(state.Traffic) && !plan.Traffic.IsNull() {
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/traffic", serverID), client.ServerModifyTrafficRequest{
Traffic: plan.Traffic.ValueInt64(),
})
if err != nil {
resp.Diagnostics.AddError("Error Modifying Server Traffic", err.Error())
return
}
}
// CPU throttle change.
if !plan.CPUThrottle.Equal(state.CPUThrottle) && !plan.CPUThrottle.IsNull() {
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/cpuThrottle", serverID), client.ServerModifyCPUThrottleRequest{
Percentage: plan.CPUThrottle.ValueInt64(),
})
if err != nil {
resp.Diagnostics.AddError("Error Modifying CPU Throttle", err.Error())
return
}
}
// VNC toggle.
if !plan.VNCEnabled.Equal(state.VNCEnabled) && !plan.VNCEnabled.IsNull() {
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/vnc", serverID), nil)
if err != nil {
resp.Diagnostics.AddError("Error Toggling VNC", err.Error())
return
}
}
// Suspended change.
if !plan.Suspended.Equal(state.Suspended) && !plan.Suspended.IsNull() {
if plan.Suspended.ValueBool() {
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/suspend", serverID), nil)
if err != nil {
resp.Diagnostics.AddError("Error Suspending Server", err.Error())
return
}
} else {
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/unsuspend", serverID), nil)
if err != nil {
resp.Diagnostics.AddError("Error Unsuspending Server", err.Error())
return
}
}
}
// Backup plan change.
if !plan.BackupPlanID.Equal(state.BackupPlanID) {
planID := int64(0)
if !plan.BackupPlanID.IsNull() {
planID = plan.BackupPlanID.ValueInt64()
}
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/backups/plan/%d", serverID, planID), nil)
if err != nil {
resp.Diagnostics.AddError("Error Modifying Backup Plan", err.Error())
return
}
}
// Custom XML change.
if !plan.CustomXML.Equal(state.CustomXML) && !plan.CustomXML.IsNull() {
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/customXML", serverID), client.ServerCustomXMLRequest{
XML: plan.CustomXML.ValueString(),
})
if err != nil {
resp.Diagnostics.AddError("Error Setting Custom XML", err.Error())
return
}
}
// Owner change.
if !plan.OwnerUserID.Equal(state.OwnerUserID) && !plan.OwnerUserID.IsNull() {
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/owner/%d", serverID, plan.OwnerUserID.ValueInt64()), nil)
if err != nil {
resp.Diagnostics.AddError("Error Changing Server Owner", err.Error())
return
}
}
// Package change.
if !plan.PackageID.Equal(state.PackageID) {
_, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/package/%d", serverID, plan.PackageID.ValueInt64()), nil)
if err != nil {
resp.Diagnostics.AddError("Error Changing Server Package", err.Error())
return
}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *ServerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state ServerResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
_, err := r.client.Delete(ctx, fmt.Sprintf("/servers/%d?delay=0", state.ID.ValueInt64()))
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
// Already deleted, nothing to do.
return
}
resp.Diagnostics.AddError("Error Deleting Server", err.Error())
return
}
}
func (r *ServerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
id, err := strconv.ParseInt(req.ID, 10, 64)
if err != nil {
resp.Diagnostics.AddError(
"Invalid Import ID",
fmt.Sprintf("Could not parse server ID %q as an integer: %s", req.ID, err),
)
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), types.Int64Value(id))...)
}

View File

@@ -0,0 +1,266 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"errors"
"fmt"
"terraform-provider-virtfusion/internal/client"
"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/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
"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"
)
// Ensure provider-defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &ServerBuildResource{}
_ resource.ResourceWithConfigure = &ServerBuildResource{}
)
// NewServerBuildResource creates a new server build resource.
func NewServerBuildResource() resource.Resource {
return &ServerBuildResource{}
}
// ServerBuildResource defines the resource implementation.
type ServerBuildResource struct {
client *client.Client
}
// ServerBuildResourceModel describes the resource data model.
type ServerBuildResourceModel struct {
ID types.String `tfsdk:"id"`
ServerID types.Int64 `tfsdk:"server_id"`
Name types.String `tfsdk:"name"`
Hostname types.String `tfsdk:"hostname"`
OSID types.Int64 `tfsdk:"osid"`
VNC types.Bool `tfsdk:"vnc"`
Ipv6 types.Bool `tfsdk:"ipv6"`
SSHKeys types.List `tfsdk:"ssh_keys"`
Email types.Bool `tfsdk:"email"`
}
func (r *ServerBuildResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server_build"
}
func (r *ServerBuildResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Builds a VirtFusion server with an operating system. This is a one-time operation — once a server is built, it stays built.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "The identifier of the server build (same as server_id).",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"server_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the server to build.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
"name": schema.StringAttribute{
MarkdownDescription: "The name for the server build.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"osid": schema.Int64Attribute{
MarkdownDescription: "The operating system ID to install.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
"hostname": schema.StringAttribute{
MarkdownDescription: "The hostname for the server.",
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"vnc": schema.BoolAttribute{
MarkdownDescription: "Whether to enable VNC access.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.RequiresReplace(),
},
},
"ipv6": schema.BoolAttribute{
MarkdownDescription: "Whether to enable IPv6.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.RequiresReplace(),
},
},
"ssh_keys": schema.ListAttribute{
MarkdownDescription: "List of SSH key IDs to add to the server.",
Optional: true,
ElementType: types.Int64Type,
PlanModifiers: []planmodifier.List{
listplanmodifier.RequiresReplace(),
},
},
"email": schema.BoolAttribute{
MarkdownDescription: "Whether to send a notification email after build.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.RequiresReplace(),
},
},
},
}
}
func (r *ServerBuildResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = c
}
func (r *ServerBuildResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data ServerBuildResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Build the API request body.
buildReq := client.ServerBuildRequest{
Name: data.Name.ValueString(),
OperatingSystemID: data.OSID.ValueInt64(),
VNC: data.VNC.ValueBool(),
Ipv6: data.Ipv6.ValueBool(),
Email: data.Email.ValueBool(),
}
if !data.Hostname.IsNull() && !data.Hostname.IsUnknown() {
buildReq.Hostname = data.Hostname.ValueString()
}
// Convert ssh_keys from types.List to []int64.
if !data.SSHKeys.IsNull() && !data.SSHKeys.IsUnknown() {
var sshKeys []int64
resp.Diagnostics.Append(data.SSHKeys.ElementsAs(ctx, &sshKeys, false)...)
if resp.Diagnostics.HasError() {
return
}
buildReq.SSHKeys = sshKeys
}
apiPath := fmt.Sprintf("/servers/%d/build", data.ServerID.ValueInt64())
_, err := r.client.Post(ctx, apiPath, buildReq)
if err != nil {
resp.Diagnostics.AddError(
"Error Building Server",
fmt.Sprintf("Could not build server %d: %s", data.ServerID.ValueInt64(), err),
)
return
}
// Set the ID to the server_id.
data.ID = types.StringValue(fmt.Sprintf("%d", data.ServerID.ValueInt64()))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerBuildResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data ServerBuildResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Verify the server still exists.
apiPath := fmt.Sprintf("/servers/%d", data.ServerID.ValueInt64())
_, err := r.client.Get(ctx, apiPath)
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
// Server no longer exists, remove from state.
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError(
"Error Reading Server",
fmt.Sprintf("Could not read server %d: %s", data.ServerID.ValueInt64(), err),
)
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerBuildResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data ServerBuildResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerBuildResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
// No-op: building a server is not reversible. Removing from state only.
}
// ValidateConfig validates the resource configuration.
func (r *ServerBuildResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var data ServerBuildResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Validate server_id is positive.
if !data.ServerID.IsNull() && !data.ServerID.IsUnknown() && data.ServerID.ValueInt64() <= 0 {
resp.Diagnostics.AddAttributeError(
path.Root("server_id"),
"Invalid Server ID",
"server_id must be a positive integer.",
)
}
// Validate osid is positive.
if !data.OSID.IsNull() && !data.OSID.IsUnknown() && data.OSID.ValueInt64() <= 0 {
resp.Diagnostics.AddAttributeError(
path.Root("osid"),
"Invalid OS ID",
"osid must be a positive integer.",
)
}
}

View File

@@ -0,0 +1,317 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"errors"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/attr"
"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/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ resource.Resource = &ServerFirewallResource{}
_ resource.ResourceWithConfigure = &ServerFirewallResource{}
)
// NewServerFirewallResource returns a new resource for managing server firewalls.
func NewServerFirewallResource() resource.Resource {
return &ServerFirewallResource{}
}
// ServerFirewallResource defines the resource implementation.
type ServerFirewallResource struct {
client *client.Client
}
// ServerFirewallResourceModel describes the resource data model.
type ServerFirewallResourceModel struct {
ID types.String `tfsdk:"id"`
ServerID types.Int64 `tfsdk:"server_id"`
InterfaceName types.String `tfsdk:"interface_name"`
Rules types.List `tfsdk:"rules"`
}
// FirewallRuleModel describes a single firewall rule.
type FirewallRuleModel struct {
Action types.String `tfsdk:"action"`
Direction types.String `tfsdk:"direction"`
Protocol types.String `tfsdk:"protocol"`
Port types.String `tfsdk:"port"`
IP types.String `tfsdk:"ip"`
}
func (r *ServerFirewallResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server_firewall"
}
func (r *ServerFirewallResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Manages a VirtFusion server firewall.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "Composite identifier in the format `server_id/interface_name`.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"server_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the server.",
Required: true,
},
"interface_name": schema.StringAttribute{
MarkdownDescription: "The network interface name. Defaults to `eth0`.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString("eth0"),
},
"rules": schema.ListNestedAttribute{
MarkdownDescription: "The firewall rules.",
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"action": schema.StringAttribute{
MarkdownDescription: "The action for the rule (e.g. `accept`, `drop`).",
Required: true,
},
"direction": schema.StringAttribute{
MarkdownDescription: "The direction for the rule (e.g. `in`, `out`).",
Required: true,
},
"protocol": schema.StringAttribute{
MarkdownDescription: "The protocol for the rule (e.g. `tcp`, `udp`).",
Required: true,
},
"port": schema.StringAttribute{
MarkdownDescription: "The port or port range for the rule.",
Required: true,
},
"ip": schema.StringAttribute{
MarkdownDescription: "The IP address or CIDR for the rule.",
Required: true,
},
},
},
},
},
}
}
func (r *ServerFirewallResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
r.client = c
}
func (r *ServerFirewallResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data ServerFirewallResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
serverID := data.ServerID.ValueInt64()
iface := data.InterfaceName.ValueString()
// Enable the firewall
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/enable", serverID, iface), nil)
if err != nil {
resp.Diagnostics.AddError("Error enabling server firewall", err.Error())
return
}
// Set rules if provided
rules, diags := r.extractRules(ctx, data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
if len(rules) > 0 {
rulesReq := client.FirewallSetRulesRequest{Rules: rules}
_, err = r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/rules", serverID, iface), rulesReq)
if err != nil {
resp.Diagnostics.AddError("Error setting firewall rules", err.Error())
return
}
}
data.ID = types.StringValue(fmt.Sprintf("%d/%s", serverID, iface))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerFirewallResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data ServerFirewallResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
serverID := data.ServerID.ValueInt64()
iface := data.InterfaceName.ValueString()
result, err := r.client.Get(ctx, fmt.Sprintf("/servers/%d/firewall/%s", serverID, iface))
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Error reading server firewall", err.Error())
return
}
var fwResp client.FirewallResponse
if err := json.Unmarshal(result, &fwResp); err != nil {
resp.Diagnostics.AddError("Error parsing firewall response", err.Error())
return
}
// If the firewall is not enabled, remove from state
if !fwResp.Data.Enabled {
resp.State.RemoveResource(ctx)
return
}
data.ID = types.StringValue(fmt.Sprintf("%d/%s", serverID, iface))
// Map API rules to the model
ruleObjects := make([]attr.Value, len(fwResp.Data.Rules))
for i, rule := range fwResp.Data.Rules {
ruleObj, diags := types.ObjectValue(
firewallRuleAttrTypes(),
map[string]attr.Value{
"action": types.StringValue(rule.Action),
"direction": types.StringValue(rule.Direction),
"protocol": types.StringValue(rule.Protocol),
"port": types.StringValue(rule.Port),
"ip": types.StringValue(rule.IP),
},
)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ruleObjects[i] = ruleObj
}
rulesList, diags := types.ListValue(types.ObjectType{AttrTypes: firewallRuleAttrTypes()}, ruleObjects)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.Rules = rulesList
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerFirewallResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data ServerFirewallResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
serverID := data.ServerID.ValueInt64()
iface := data.InterfaceName.ValueString()
rules, diags := r.extractRules(ctx, data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
rulesReq := client.FirewallSetRulesRequest{Rules: rules}
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/rules", serverID, iface), rulesReq)
if err != nil {
resp.Diagnostics.AddError("Error updating firewall rules", err.Error())
return
}
data.ID = types.StringValue(fmt.Sprintf("%d/%s", serverID, iface))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerFirewallResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data ServerFirewallResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
serverID := data.ServerID.ValueInt64()
iface := data.InterfaceName.ValueString()
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/disable", serverID, iface), nil)
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
return
}
resp.Diagnostics.AddError("Error disabling server firewall", err.Error())
}
}
// extractRules converts the rules list from the model into client.FirewallRule slice.
func (r *ServerFirewallResource) extractRules(ctx context.Context, data ServerFirewallResourceModel) ([]client.FirewallRule, diag.Diagnostics) {
var diags diag.Diagnostics
if data.Rules.IsNull() || data.Rules.IsUnknown() {
return nil, diags
}
var ruleModels []FirewallRuleModel
diags.Append(data.Rules.ElementsAs(ctx, &ruleModels, false)...)
if diags.HasError() {
return nil, diags
}
rules := make([]client.FirewallRule, len(ruleModels))
for i, rm := range ruleModels {
rules[i] = client.FirewallRule{
Action: rm.Action.ValueString(),
Direction: rm.Direction.ValueString(),
Protocol: rm.Protocol.ValueString(),
Port: rm.Port.ValueString(),
IP: rm.IP.ValueString(),
}
}
return rules, diags
}
// firewallRuleAttrTypes returns the attribute types for a firewall rule object.
func firewallRuleAttrTypes() map[string]attr.Type {
return map[string]attr.Type{
"action": types.StringType,
"direction": types.StringType,
"protocol": types.StringType,
"port": types.StringType,
"ip": types.StringType,
}
}

View File

@@ -0,0 +1,160 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"errors"
"fmt"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"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"
)
var (
_ resource.Resource = &ServerIPv4Resource{}
_ resource.ResourceWithConfigure = &ServerIPv4Resource{}
)
// NewServerIPv4Resource returns a new resource for managing server IPv4 addresses.
func NewServerIPv4Resource() resource.Resource {
return &ServerIPv4Resource{}
}
// ServerIPv4Resource defines the resource implementation.
type ServerIPv4Resource struct {
client *client.Client
}
// ServerIPv4ResourceModel describes the resource data model.
type ServerIPv4ResourceModel struct {
ID types.String `tfsdk:"id"`
ServerID types.Int64 `tfsdk:"server_id"`
Quantity types.Int64 `tfsdk:"quantity"`
}
func (r *ServerIPv4Resource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server_ipv4"
}
func (r *ServerIPv4Resource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Adds IPv4 addresses to a VirtFusion server.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "Resource identifier.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"server_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the server. Changing this forces a new resource to be created.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
"quantity": schema.Int64Attribute{
MarkdownDescription: "The number of IPv4 addresses to add. Defaults to `1`. Changing this forces a new resource to be created.",
Optional: true,
Computed: true,
Default: int64default.StaticInt64(1),
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
},
}
}
func (r *ServerIPv4Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
r.client = c
}
func (r *ServerIPv4Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data ServerIPv4ResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
serverID := data.ServerID.ValueInt64()
quantity := data.Quantity.ValueInt64()
var body interface{}
if quantity > 1 {
body = client.ServerIPv4AddRequest{
Quantity: quantity,
}
}
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/ipv4", serverID), body)
if err != nil {
resp.Diagnostics.AddError("Error adding IPv4 to server", err.Error())
return
}
data.ID = types.StringValue(fmt.Sprintf("%d/ipv4/%d", serverID, quantity))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerIPv4Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data ServerIPv4ResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// No dedicated read endpoint; return stored state as-is.
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerIPv4Resource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) {
// All attributes have RequiresReplace, so Update should never be called.
resp.Diagnostics.AddError(
"Update Not Supported",
"All attributes of virtfusion_server_ipv4 require replacement. This function should not be called.",
)
}
func (r *ServerIPv4Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data ServerIPv4ResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
serverID := data.ServerID.ValueInt64()
_, err := r.client.Delete(ctx, fmt.Sprintf("/servers/%d/ipv4", serverID))
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
return
}
resp.Diagnostics.AddError("Error removing IPv4 from server", err.Error())
}
}

View File

@@ -0,0 +1,158 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"errors"
"fmt"
"terraform-provider-virtfusion/internal/client"
"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"
)
var (
_ resource.Resource = &ServerNetworkWhitelistResource{}
_ resource.ResourceWithConfigure = &ServerNetworkWhitelistResource{}
)
// NewServerNetworkWhitelistResource returns a new resource for managing server network whitelist entries.
func NewServerNetworkWhitelistResource() resource.Resource {
return &ServerNetworkWhitelistResource{}
}
// ServerNetworkWhitelistResource defines the resource implementation.
type ServerNetworkWhitelistResource struct {
client *client.Client
}
// ServerNetworkWhitelistResourceModel describes the resource data model.
type ServerNetworkWhitelistResourceModel struct {
ID types.String `tfsdk:"id"`
ServerID types.Int64 `tfsdk:"server_id"`
IP types.String `tfsdk:"ip"`
}
func (r *ServerNetworkWhitelistResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server_network_whitelist"
}
func (r *ServerNetworkWhitelistResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Manages a VirtFusion server network whitelist entry.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "Composite identifier in the format `server_id/ip`.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"server_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the server. Changing this forces a new resource to be created.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
"ip": schema.StringAttribute{
MarkdownDescription: "The IP address to whitelist. Changing this forces a new resource to be created.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
},
}
}
func (r *ServerNetworkWhitelistResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
r.client = c
}
func (r *ServerNetworkWhitelistResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data ServerNetworkWhitelistResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
serverID := data.ServerID.ValueInt64()
ip := data.IP.ValueString()
body := client.NetworkWhitelistRequest{
IP: ip,
}
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/networkWhitelist", serverID), body)
if err != nil {
resp.Diagnostics.AddError("Error adding network whitelist entry", err.Error())
return
}
data.ID = types.StringValue(fmt.Sprintf("%d/%s", serverID, ip))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerNetworkWhitelistResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data ServerNetworkWhitelistResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// No dedicated read endpoint; return stored state as-is.
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerNetworkWhitelistResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) {
// All attributes have RequiresReplace, so Update should never be called.
resp.Diagnostics.AddError(
"Update Not Supported",
"All attributes of virtfusion_server_network_whitelist require replacement. This function should not be called.",
)
}
func (r *ServerNetworkWhitelistResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data ServerNetworkWhitelistResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
serverID := data.ServerID.ValueInt64()
body := client.NetworkWhitelistRequest{
IP: data.IP.ValueString(),
}
_, err := r.client.DeleteWithBody(ctx, fmt.Sprintf("/servers/%d/networkWhitelist", serverID), body)
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
return
}
resp.Diagnostics.AddError("Error removing network whitelist entry", err.Error())
}
}

View File

@@ -0,0 +1,199 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"time"
"terraform-provider-virtfusion/internal/client"
"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/mapplanmodifier"
"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"
)
// Ensure provider-defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &ServerPasswordResetResource{}
_ resource.ResourceWithConfigure = &ServerPasswordResetResource{}
)
// NewServerPasswordResetResource creates a new server password reset resource.
func NewServerPasswordResetResource() resource.Resource {
return &ServerPasswordResetResource{}
}
// ServerPasswordResetResource defines the resource implementation.
type ServerPasswordResetResource struct {
client *client.Client
}
// ServerPasswordResetResourceModel describes the resource data model.
type ServerPasswordResetResourceModel struct {
ID types.String `tfsdk:"id"`
ServerID types.Int64 `tfsdk:"server_id"`
User types.String `tfsdk:"user"`
Password types.String `tfsdk:"password"`
Triggers types.Map `tfsdk:"triggers"`
}
func (r *ServerPasswordResetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server_password_reset"
}
func (r *ServerPasswordResetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Resets the password for a VirtFusion server. This is a trigger-style resource — the reset is executed on create and can be re-triggered by changing the `triggers` attribute.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "The identifier for this password reset.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"server_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the server to reset the password for.",
Required: true,
},
"user": schema.StringAttribute{
MarkdownDescription: "The user to reset the password for. Must be `root` (Linux) or `Administrator` (Windows).",
Required: true,
},
"password": schema.StringAttribute{
MarkdownDescription: "The new password generated by the reset operation.",
Computed: true,
Sensitive: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"triggers": schema.MapAttribute{
MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the password reset to be re-executed. Works like `triggers` in `terraform_data`.",
ElementType: types.StringType,
Optional: true,
PlanModifiers: []planmodifier.Map{
mapplanmodifier.RequiresReplace(),
},
},
},
}
}
func (r *ServerPasswordResetResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = c
}
func (r *ServerPasswordResetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data ServerPasswordResetResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
body := map[string]string{
"user": data.User.ValueString(),
}
apiPath := fmt.Sprintf("/servers/%d/resetPassword", data.ServerID.ValueInt64())
rawResp, err := r.client.Post(ctx, apiPath, body)
if err != nil {
resp.Diagnostics.AddError(
"Error Resetting Server Password",
fmt.Sprintf("Could not reset password for server %d: %s", data.ServerID.ValueInt64(), err),
)
return
}
// Parse the response for the new password.
if rawResp != nil {
var passResp client.PasswordResetResponse
if jsonErr := json.Unmarshal(rawResp, &passResp); jsonErr == nil && passResp.Data.Password != "" {
data.Password = types.StringValue(passResp.Data.Password)
} else {
data.Password = types.StringValue("")
}
} else {
data.Password = types.StringValue("")
}
data.ID = types.StringValue(fmt.Sprintf("%d-%d", data.ServerID.ValueInt64(), time.Now().UnixNano()))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerPasswordResetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data ServerPasswordResetResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Return stored state as-is for trigger-style resources.
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerPasswordResetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data ServerPasswordResetResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerPasswordResetResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
// No-op: password resets are not reversible. Removing from state only.
}
// ValidateConfig validates the resource configuration.
func (r *ServerPasswordResetResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var data ServerPasswordResetResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Validate server_id is positive.
if !data.ServerID.IsNull() && !data.ServerID.IsUnknown() && data.ServerID.ValueInt64() <= 0 {
resp.Diagnostics.AddAttributeError(
path.Root("server_id"),
"Invalid Server ID",
"server_id must be a positive integer.",
)
}
// Validate user is one of the allowed values.
if !data.User.IsNull() && !data.User.IsUnknown() {
user := data.User.ValueString()
if user != "root" && user != "Administrator" {
resp.Diagnostics.AddAttributeError(
path.Root("user"),
"Invalid User",
fmt.Sprintf("user must be either \"root\" or \"Administrator\". Got: %q", user),
)
}
}
}

View File

@@ -0,0 +1,179 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"fmt"
"time"
"terraform-provider-virtfusion/internal/client"
"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/mapplanmodifier"
"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"
)
// Ensure provider-defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &ServerPowerActionResource{}
_ resource.ResourceWithConfigure = &ServerPowerActionResource{}
)
// NewServerPowerActionResource creates a new server power action resource.
func NewServerPowerActionResource() resource.Resource {
return &ServerPowerActionResource{}
}
// ServerPowerActionResource defines the resource implementation.
type ServerPowerActionResource struct {
client *client.Client
}
// ServerPowerActionResourceModel describes the resource data model.
type ServerPowerActionResourceModel struct {
ID types.String `tfsdk:"id"`
ServerID types.Int64 `tfsdk:"server_id"`
Action types.String `tfsdk:"action"`
Triggers types.Map `tfsdk:"triggers"`
}
func (r *ServerPowerActionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server_power_action"
}
func (r *ServerPowerActionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Performs a power action on a VirtFusion server. This is a trigger-style resource — the action is executed on create and can be re-triggered by changing the `triggers` attribute.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "The identifier for this power action.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"server_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the server to perform the power action on.",
Required: true,
},
"action": schema.StringAttribute{
MarkdownDescription: "The power action to perform. Must be one of: `boot`, `shutdown`, `restart`, `poweroff`.",
Required: true,
},
"triggers": schema.MapAttribute{
MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the power action to be re-executed. Works like `triggers` in `terraform_data`.",
ElementType: types.StringType,
Optional: true,
PlanModifiers: []planmodifier.Map{
mapplanmodifier.RequiresReplace(),
},
},
},
}
}
func (r *ServerPowerActionResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = c
}
func (r *ServerPowerActionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data ServerPowerActionResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
apiPath := fmt.Sprintf("/servers/%d/power/%s", data.ServerID.ValueInt64(), data.Action.ValueString())
_, err := r.client.Post(ctx, apiPath, nil)
if err != nil {
resp.Diagnostics.AddError(
"Error Performing Server Power Action",
fmt.Sprintf("Could not perform power action %q on server %d: %s", data.Action.ValueString(), data.ServerID.ValueInt64(), err),
)
return
}
data.ID = types.StringValue(fmt.Sprintf("%d-%s-%d", data.ServerID.ValueInt64(), data.Action.ValueString(), time.Now().UnixNano()))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerPowerActionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data ServerPowerActionResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Return stored state as-is for trigger-style resources.
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerPowerActionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data ServerPowerActionResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerPowerActionResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
// No-op: power actions are not reversible. Removing from state only.
}
// ValidateConfig validates the resource configuration.
func (r *ServerPowerActionResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var data ServerPowerActionResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Validate server_id is positive.
if !data.ServerID.IsNull() && !data.ServerID.IsUnknown() && data.ServerID.ValueInt64() <= 0 {
resp.Diagnostics.AddAttributeError(
path.Root("server_id"),
"Invalid Server ID",
"server_id must be a positive integer.",
)
}
// Validate action is one of the allowed values.
if !data.Action.IsNull() && !data.Action.IsUnknown() {
action := data.Action.ValueString()
validActions := map[string]bool{
"boot": true,
"shutdown": true,
"restart": true,
"poweroff": true,
}
if !validActions[action] {
resp.Diagnostics.AddAttributeError(
path.Root("action"),
"Invalid Power Action",
fmt.Sprintf("action must be one of: boot, shutdown, restart, poweroff. Got: %q", action),
)
}
}
}

View File

@@ -0,0 +1,163 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"errors"
"fmt"
"terraform-provider-virtfusion/internal/client"
"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"
)
// Ensure provider-defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &ServerTrafficBlockResource{}
_ resource.ResourceWithConfigure = &ServerTrafficBlockResource{}
)
// NewServerTrafficBlockResource creates a new server traffic block resource.
func NewServerTrafficBlockResource() resource.Resource {
return &ServerTrafficBlockResource{}
}
// ServerTrafficBlockResource defines the resource implementation.
type ServerTrafficBlockResource struct {
client *client.Client
}
// ServerTrafficBlockResourceModel describes the resource data model.
type ServerTrafficBlockResourceModel struct {
ID types.Int64 `tfsdk:"id"`
ServerID types.Int64 `tfsdk:"server_id"`
Type types.String `tfsdk:"type"`
}
func (r *ServerTrafficBlockResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server_traffic_block"
}
func (r *ServerTrafficBlockResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Manages a traffic block on a VirtFusion server.",
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The identifier of the traffic block.",
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
},
"server_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the server to add the traffic block to.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
"type": schema.StringAttribute{
MarkdownDescription: "The type of traffic block (e.g. `inbound` or `outbound`).",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
},
}
}
func (r *ServerTrafficBlockResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = c
}
func (r *ServerTrafficBlockResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data ServerTrafficBlockResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
blockReq := client.TrafficBlockRequest{
Type: data.Type.ValueString(),
}
apiPath := fmt.Sprintf("/servers/%d/traffic/blocks", data.ServerID.ValueInt64())
respBody, err := r.client.Post(ctx, apiPath, blockReq)
if err != nil {
resp.Diagnostics.AddError(
"Error Creating Traffic Block",
fmt.Sprintf("Could not create traffic block on server %d: %s", data.ServerID.ValueInt64(), err),
)
return
}
var blockResp client.TrafficBlockResponse
if err := json.Unmarshal(respBody, &blockResp); err != nil {
resp.Diagnostics.AddError(
"Error Parsing Response",
fmt.Sprintf("Could not parse traffic block response: %s", err),
)
return
}
data.ID = types.Int64Value(blockResp.Data.ID)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerTrafficBlockResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data ServerTrafficBlockResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *ServerTrafficBlockResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) {
// All attributes require replacement — updates are never called.
}
func (r *ServerTrafficBlockResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data ServerTrafficBlockResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
apiPath := fmt.Sprintf("/servers/%d/traffic/blocks/%d", data.ServerID.ValueInt64(), data.ID.ValueInt64())
_, err := r.client.Delete(ctx, apiPath)
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
return
}
resp.Diagnostics.AddError(
"Error Deleting Traffic Block",
fmt.Sprintf("Could not delete traffic block %d on server %d: %s", data.ID.ValueInt64(), data.ServerID.ValueInt64(), err),
)
return
}
}

View File

@@ -0,0 +1,226 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"terraform-provider-virtfusion/internal/client"
"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/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"
)
// Ensure provider-defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &SSHKeyResource{}
_ resource.ResourceWithConfigure = &SSHKeyResource{}
_ resource.ResourceWithImportState = &SSHKeyResource{}
)
// NewSSHKeyResource creates a new SSH key resource.
func NewSSHKeyResource() resource.Resource {
return &SSHKeyResource{}
}
// SSHKeyResource defines the resource implementation.
type SSHKeyResource struct {
client *client.Client
}
// SSHKeyResourceModel describes the resource data model.
type SSHKeyResourceModel struct {
ID types.Int64 `tfsdk:"id"`
UserID types.Int64 `tfsdk:"user_id"`
Name types.String `tfsdk:"name"`
PublicKey types.String `tfsdk:"public_key"`
}
func (r *SSHKeyResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_ssh_key"
}
func (r *SSHKeyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Manages a VirtFusion SSH key.",
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The ID of the SSH key.",
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
},
"user_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the user who owns this SSH key.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
"name": schema.StringAttribute{
MarkdownDescription: "The name of the SSH key.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"public_key": schema.StringAttribute{
MarkdownDescription: "The public key content.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
},
}
}
func (r *SSHKeyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = c
}
func (r *SSHKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data SSHKeyResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
createReq := client.SSHKeyCreateRequest{
UserID: data.UserID.ValueInt64(),
Name: data.Name.ValueString(),
PublicKey: data.PublicKey.ValueString(),
}
respBody, err := r.client.Post(ctx, "/ssh_keys", createReq)
if err != nil {
resp.Diagnostics.AddError(
"Error Creating SSH Key",
fmt.Sprintf("Could not create SSH key: %s", err),
)
return
}
var sshKeyResp client.SSHKeyResponse
if err := json.Unmarshal(respBody, &sshKeyResp); err != nil {
resp.Diagnostics.AddError(
"Error Parsing Response",
fmt.Sprintf("Could not parse SSH key response: %s", err),
)
return
}
data.ID = types.Int64Value(sshKeyResp.Data.ID)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SSHKeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data SSHKeyResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
apiPath := fmt.Sprintf("/ssh_keys/%d", data.ID.ValueInt64())
respBody, err := r.client.Get(ctx, apiPath)
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
// SSH key no longer exists, remove from state.
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError(
"Error Reading SSH Key",
fmt.Sprintf("Could not read SSH key %d: %s", data.ID.ValueInt64(), err),
)
return
}
var sshKeyResp client.SSHKeyResponse
if err := json.Unmarshal(respBody, &sshKeyResp); err != nil {
resp.Diagnostics.AddError(
"Error Parsing Response",
fmt.Sprintf("Could not parse SSH key response: %s", err),
)
return
}
// Update state from API response.
data.Name = types.StringValue(sshKeyResp.Data.Name)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SSHKeyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data SSHKeyResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *SSHKeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data SSHKeyResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
apiPath := fmt.Sprintf("/ssh_keys/%d", data.ID.ValueInt64())
_, err := r.client.Delete(ctx, apiPath)
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
// Already deleted, nothing to do.
return
}
resp.Diagnostics.AddError(
"Error Deleting SSH Key",
fmt.Sprintf("Could not delete SSH key %d: %s", data.ID.ValueInt64(), err),
)
return
}
}
func (r *SSHKeyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
id, err := strconv.ParseInt(req.ID, 10, 64)
if err != nil {
resp.Diagnostics.AddError(
"Invalid Import ID",
fmt.Sprintf("Could not parse SSH key ID %q as integer: %s", req.ID, err),
)
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), types.Int64Value(id))...)
}

View File

@@ -0,0 +1,218 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"errors"
"fmt"
"terraform-provider-virtfusion/internal/client"
"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 = &UserResource{}
_ resource.ResourceWithConfigure = &UserResource{}
_ resource.ResourceWithImportState = &UserResource{}
)
// NewUserResource returns a new resource for managing VirtFusion users.
func NewUserResource() resource.Resource {
return &UserResource{}
}
// UserResource defines the resource implementation.
type UserResource struct {
client *client.Client
}
// UserResourceModel describes the resource data model.
type UserResourceModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Email types.String `tfsdk:"email"`
ExtRelationID types.String `tfsdk:"ext_relation_id"`
Enabled types.Bool `tfsdk:"enabled"`
}
func (r *UserResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_user"
}
func (r *UserResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Manages a VirtFusion user.",
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
MarkdownDescription: "The numeric ID of the user.",
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The name of the user.",
Required: true,
},
"email": schema.StringAttribute{
MarkdownDescription: "The email address of the user.",
Required: true,
},
"ext_relation_id": schema.StringAttribute{
MarkdownDescription: "The external relation ID used to look up the user. Changing this forces a new resource to be created.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"enabled": schema.BoolAttribute{
MarkdownDescription: "Whether the user is enabled.",
Computed: true,
},
},
}
}
func (r *UserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
)
return
}
r.client = c
}
func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data UserResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
body := client.UserCreateRequest{
Name: data.Name.ValueString(),
Email: data.Email.ValueString(),
ExtRelationID: data.ExtRelationID.ValueString(),
}
result, err := r.client.Post(ctx, "/users", body)
if err != nil {
resp.Diagnostics.AddError("Error creating user", err.Error())
return
}
var userResp client.UserResponse
if err := json.Unmarshal(result, &userResp); err != nil {
resp.Diagnostics.AddError("Error parsing user response", err.Error())
return
}
data.ID = types.Int64Value(userResp.Data.ID)
data.Name = types.StringValue(userResp.Data.Name)
data.Email = types.StringValue(userResp.Data.Email)
data.ExtRelationID = types.StringValue(userResp.Data.ExtRelationID)
data.Enabled = types.BoolValue(userResp.Data.Enabled)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *UserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data UserResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
result, err := r.client.Get(ctx, fmt.Sprintf("/users/%s/byExtRelation", data.ExtRelationID.ValueString()))
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Error reading user", err.Error())
return
}
var userResp client.UserResponse
if err := json.Unmarshal(result, &userResp); err != nil {
resp.Diagnostics.AddError("Error parsing user response", err.Error())
return
}
data.ID = types.Int64Value(userResp.Data.ID)
data.Name = types.StringValue(userResp.Data.Name)
data.Email = types.StringValue(userResp.Data.Email)
data.ExtRelationID = types.StringValue(userResp.Data.ExtRelationID)
data.Enabled = types.BoolValue(userResp.Data.Enabled)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *UserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data UserResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
body := client.UserModifyRequest{
Name: data.Name.ValueString(),
Email: data.Email.ValueString(),
}
result, err := r.client.Put(ctx, fmt.Sprintf("/users/%s/byExtRelation", data.ExtRelationID.ValueString()), body)
if err != nil {
resp.Diagnostics.AddError("Error updating user", err.Error())
return
}
var userResp client.UserResponse
if err := json.Unmarshal(result, &userResp); err != nil {
resp.Diagnostics.AddError("Error parsing user response", err.Error())
return
}
data.ID = types.Int64Value(userResp.Data.ID)
data.Name = types.StringValue(userResp.Data.Name)
data.Email = types.StringValue(userResp.Data.Email)
data.ExtRelationID = types.StringValue(userResp.Data.ExtRelationID)
data.Enabled = types.BoolValue(userResp.Data.Enabled)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *UserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data UserResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
_, err := r.client.Delete(ctx, fmt.Sprintf("/users/%s/byExtRelation", data.ExtRelationID.ValueString()))
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
return
}
resp.Diagnostics.AddError("Error deleting user", err.Error())
}
}
func (r *UserResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("ext_relation_id"), req, resp)
}

View File

@@ -0,0 +1,170 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"time"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier"
"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"
)
// Ensure provider-defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &UserAuthTokenResource{}
_ resource.ResourceWithConfigure = &UserAuthTokenResource{}
)
// NewUserAuthTokenResource creates a new user auth token resource.
func NewUserAuthTokenResource() resource.Resource {
return &UserAuthTokenResource{}
}
// UserAuthTokenResource defines the resource implementation.
type UserAuthTokenResource struct {
client *client.Client
}
// UserAuthTokenResourceModel describes the resource data model.
type UserAuthTokenResourceModel struct {
ID types.String `tfsdk:"id"`
ExtRelationID types.String `tfsdk:"ext_relation_id"`
Token types.String `tfsdk:"token"`
URL types.String `tfsdk:"url"`
Triggers types.Map `tfsdk:"triggers"`
}
func (r *UserAuthTokenResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_user_auth_token"
}
func (r *UserAuthTokenResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Generates an authentication token for a VirtFusion user. This is a trigger-style resource — the token is generated on create and can be re-generated by changing the `triggers` attribute.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "The identifier for this auth token generation.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"ext_relation_id": schema.StringAttribute{
MarkdownDescription: "The external relation ID of the user to generate the auth token for.",
Required: true,
},
"token": schema.StringAttribute{
MarkdownDescription: "The generated authentication token.",
Computed: true,
Sensitive: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"url": schema.StringAttribute{
MarkdownDescription: "The authentication URL for the generated token.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"triggers": schema.MapAttribute{
MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the auth token to be re-generated. Works like `triggers` in `terraform_data`.",
ElementType: types.StringType,
Optional: true,
PlanModifiers: []planmodifier.Map{
mapplanmodifier.RequiresReplace(),
},
},
},
}
}
func (r *UserAuthTokenResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = c
}
func (r *UserAuthTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data UserAuthTokenResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
apiPath := fmt.Sprintf("/users/%s/authenticationTokens", data.ExtRelationID.ValueString())
rawResp, err := r.client.Post(ctx, apiPath, nil)
if err != nil {
resp.Diagnostics.AddError(
"Error Generating User Auth Token",
fmt.Sprintf("Could not generate auth token for user with ext_relation_id %q: %s", data.ExtRelationID.ValueString(), err),
)
return
}
// Parse the response for the token and URL.
if rawResp != nil {
var tokenResp client.AuthTokenResponse
if jsonErr := json.Unmarshal(rawResp, &tokenResp); jsonErr == nil {
data.Token = types.StringValue(tokenResp.Data.Token)
data.URL = types.StringValue(tokenResp.Data.URL)
} else {
data.Token = types.StringValue("")
data.URL = types.StringValue("")
}
} else {
data.Token = types.StringValue("")
data.URL = types.StringValue("")
}
data.ID = types.StringValue(fmt.Sprintf("%s-%d", data.ExtRelationID.ValueString(), time.Now().UnixNano()))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *UserAuthTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data UserAuthTokenResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Return stored state as-is for trigger-style resources.
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *UserAuthTokenResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data UserAuthTokenResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *UserAuthTokenResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
// No-op: auth tokens cannot be revoked via this resource. Removing from state only.
}

View File

@@ -0,0 +1,137 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"fmt"
"time"
"terraform-provider-virtfusion/internal/client"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier"
"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"
)
// Ensure provider-defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &UserPasswordResetResource{}
_ resource.ResourceWithConfigure = &UserPasswordResetResource{}
)
// NewUserPasswordResetResource creates a new user password reset resource.
func NewUserPasswordResetResource() resource.Resource {
return &UserPasswordResetResource{}
}
// UserPasswordResetResource defines the resource implementation.
type UserPasswordResetResource struct {
client *client.Client
}
// UserPasswordResetResourceModel describes the resource data model.
type UserPasswordResetResourceModel struct {
ID types.String `tfsdk:"id"`
ExtRelationID types.String `tfsdk:"ext_relation_id"`
Triggers types.Map `tfsdk:"triggers"`
}
func (r *UserPasswordResetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_user_password_reset"
}
func (r *UserPasswordResetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Resets the password for a VirtFusion user by external relation ID. This is a trigger-style resource — the reset is executed on create and can be re-triggered by changing the `triggers` attribute.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "The identifier for this password reset.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"ext_relation_id": schema.StringAttribute{
MarkdownDescription: "The external relation ID of the user to reset the password for.",
Required: true,
},
"triggers": schema.MapAttribute{
MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the password reset to be re-executed. Works like `triggers` in `terraform_data`.",
ElementType: types.StringType,
Optional: true,
PlanModifiers: []planmodifier.Map{
mapplanmodifier.RequiresReplace(),
},
},
},
}
}
func (r *UserPasswordResetResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = c
}
func (r *UserPasswordResetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data UserPasswordResetResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
apiPath := fmt.Sprintf("/users/%s/byExtRelation/resetPassword", data.ExtRelationID.ValueString())
_, err := r.client.Post(ctx, apiPath, nil)
if err != nil {
resp.Diagnostics.AddError(
"Error Resetting User Password",
fmt.Sprintf("Could not reset password for user with ext_relation_id %q: %s", data.ExtRelationID.ValueString(), err),
)
return
}
data.ID = types.StringValue(fmt.Sprintf("%s-%d", data.ExtRelationID.ValueString(), time.Now().UnixNano()))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *UserPasswordResetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data UserPasswordResetResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Return stored state as-is for trigger-style resources.
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *UserPasswordResetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data UserPasswordResetResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *UserPasswordResetResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
// No-op: password resets are not reversible. Removing from state only.
}

View File

@@ -0,0 +1,194 @@
// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"encoding/json"
"fmt"
"time"
"terraform-provider-virtfusion/internal/client"
"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/mapplanmodifier"
"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"
)
// Ensure provider-defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &UserServerAuthTokenResource{}
_ resource.ResourceWithConfigure = &UserServerAuthTokenResource{}
)
// NewUserServerAuthTokenResource creates a new user server auth token resource.
func NewUserServerAuthTokenResource() resource.Resource {
return &UserServerAuthTokenResource{}
}
// UserServerAuthTokenResource defines the resource implementation.
type UserServerAuthTokenResource struct {
client *client.Client
}
// UserServerAuthTokenResourceModel describes the resource data model.
type UserServerAuthTokenResourceModel struct {
ID types.String `tfsdk:"id"`
ExtRelationID types.String `tfsdk:"ext_relation_id"`
ServerID types.Int64 `tfsdk:"server_id"`
Token types.String `tfsdk:"token"`
URL types.String `tfsdk:"url"`
Triggers types.Map `tfsdk:"triggers"`
}
func (r *UserServerAuthTokenResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_user_server_auth_token"
}
func (r *UserServerAuthTokenResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Generates a server-scoped authentication token for a VirtFusion user. This is a trigger-style resource — the token is generated on create and can be re-generated by changing the `triggers` attribute.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "The identifier for this server auth token generation.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"ext_relation_id": schema.StringAttribute{
MarkdownDescription: "The external relation ID of the user to generate the server auth token for.",
Required: true,
},
"server_id": schema.Int64Attribute{
MarkdownDescription: "The ID of the server to scope the auth token to.",
Required: true,
},
"token": schema.StringAttribute{
MarkdownDescription: "The generated server authentication token.",
Computed: true,
Sensitive: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"url": schema.StringAttribute{
MarkdownDescription: "The authentication URL for the generated server token.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"triggers": schema.MapAttribute{
MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the server auth token to be re-generated. Works like `triggers` in `terraform_data`.",
ElementType: types.StringType,
Optional: true,
PlanModifiers: []planmodifier.Map{
mapplanmodifier.RequiresReplace(),
},
},
},
}
}
func (r *UserServerAuthTokenResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = c
}
func (r *UserServerAuthTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data UserServerAuthTokenResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
apiPath := fmt.Sprintf("/users/%s/serverAuthenticationTokens/%d", data.ExtRelationID.ValueString(), data.ServerID.ValueInt64())
rawResp, err := r.client.Post(ctx, apiPath, nil)
if err != nil {
resp.Diagnostics.AddError(
"Error Generating User Server Auth Token",
fmt.Sprintf("Could not generate server auth token for user %q on server %d: %s", data.ExtRelationID.ValueString(), data.ServerID.ValueInt64(), err),
)
return
}
// Parse the response for the token and URL.
if rawResp != nil {
var tokenResp client.AuthTokenResponse
if jsonErr := json.Unmarshal(rawResp, &tokenResp); jsonErr == nil {
data.Token = types.StringValue(tokenResp.Data.Token)
data.URL = types.StringValue(tokenResp.Data.URL)
} else {
data.Token = types.StringValue("")
data.URL = types.StringValue("")
}
} else {
data.Token = types.StringValue("")
data.URL = types.StringValue("")
}
data.ID = types.StringValue(fmt.Sprintf("%s-%d-%d", data.ExtRelationID.ValueString(), data.ServerID.ValueInt64(), time.Now().UnixNano()))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *UserServerAuthTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data UserServerAuthTokenResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Return stored state as-is for trigger-style resources.
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *UserServerAuthTokenResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data UserServerAuthTokenResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *UserServerAuthTokenResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
// No-op: server auth tokens cannot be revoked via this resource. Removing from state only.
}
// ValidateConfig validates the resource configuration.
func (r *UserServerAuthTokenResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var data UserServerAuthTokenResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Validate server_id is positive.
if !data.ServerID.IsNull() && !data.ServerID.IsUnknown() && data.ServerID.ValueInt64() <= 0 {
resp.Diagnostics.AddAttributeError(
path.Root("server_id"),
"Invalid Server ID",
"server_id must be a positive integer.",
)
}
}

View File

@@ -1,285 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/types"
"io"
"io/ioutil"
"net/http"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
)
// Ensure provider defined types fully satisfy framework interfaces.
var _ resource.Resource = &VirtfusionServerBuildResource{}
var _ resource.ResourceWithImportState = &VirtfusionServerBuildResource{}
func NewVirtfusionServerBuildResource() resource.Resource {
return &VirtfusionServerBuildResource{}
}
// VirtfusionServerBuildResource defines the resource implementation.
type VirtfusionServerBuildResource struct {
client *http.Client
}
type VirtfusionServerBuildResourceModel struct {
ServerId int64 `tfsdk:"server_id"`
Name string `tfsdk:"name" json:"name"`
Hostname string `tfsdk:"hostname" json:"hostname"`
Osid int64 `tfsdk:"osid" json:"operatingSystemId"`
Vnc bool `tfsdk:"vnc" json:"vnc"`
Ipv6 bool `tfsdk:"ipv6" json:"ipv6"`
SshKeys []int64 `tfsdk:"ssh_keys" json:"sshKeys"`
Email bool `tfsdk:"email" json:"email"`
}
func (r *VirtfusionServerBuildResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_build"
}
func (r *VirtfusionServerBuildResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
// This description is used by the documentation generator and the language server.
MarkdownDescription: "Virtfusion Server Build Resource",
Attributes: map[string]schema.Attribute{
"server_id": schema.Int64Attribute{
MarkdownDescription: "Server ID",
Required: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "Server Name",
Required: true,
},
"hostname": schema.StringAttribute{
MarkdownDescription: "Server Hostname",
Optional: true,
},
"osid": schema.Int64Attribute{
MarkdownDescription: "Server Operating System ID",
Required: true,
},
"vnc": schema.BoolAttribute{
MarkdownDescription: "Server VNC",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
"ipv6": schema.BoolAttribute{
MarkdownDescription: "Server IPv6",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
"ssh_keys": schema.ListAttribute{
MarkdownDescription: "Server SSH Keys IDs",
ElementType: types.Int64Type,
Optional: true,
},
"email": schema.BoolAttribute{
MarkdownDescription: "Server Email",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
},
}
}
func (r *VirtfusionServerBuildResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*http.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = client
}
func (r *VirtfusionServerBuildResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data VirtfusionServerBuildResourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
createReq := VirtfusionServerBuildResourceModel{
Name: data.Name,
Hostname: data.Hostname,
Osid: data.Osid,
Vnc: data.Vnc,
Ipv6: data.Ipv6,
SshKeys: data.SshKeys,
Email: data.Email,
}
httpReqBody, err := json.Marshal(createReq)
if err != nil {
resp.Diagnostics.AddError(
"Unable to Create Resource",
"An unexpected error occurred while creating the resource create request. "+
"Please report this issue to the provider developers.\n\n"+
"JSON Error: "+err.Error(),
)
return
}
httpReq, err := http.NewRequest("POST", fmt.Sprintf("/servers/%d/build", data.ServerId), bytes.NewBuffer(httpReqBody))
if err != nil {
resp.Diagnostics.AddError(
"Failed to Create Request",
fmt.Sprintf("Failed to create a new HTTP request: %s", err.Error()),
)
return
}
// Add any additional headers (Content-Type, etc.)
httpReq.Header.Set("Content-Type", "application/json")
httpResponse, err := r.client.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError(
"Failed to Execute Request",
fmt.Sprintf("Failed to execute HTTP request: %s", err.Error()),
)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
resp.Diagnostics.AddError(
"Failed to Close Request",
fmt.Sprintf("Failed to close HTTP request: %s", err.Error()),
)
return
}
}(httpResponse.Body)
if httpResponse.StatusCode == 422 {
responseBody, err := ioutil.ReadAll(httpResponse.Body)
if err != nil {
resp.Diagnostics.AddError(
"Failed to Read Response",
fmt.Sprintf("Failed to read HTTP response body: %s", err.Error()),
)
return
}
var errorResponse map[string]interface{}
err = json.Unmarshal(responseBody, &errorResponse)
if err != nil {
resp.Diagnostics.AddError(
"Failed to Parse Error Response",
fmt.Sprintf("Failed to parse HTTP response body: %s", err.Error()),
)
return
}
if errors, exists := errorResponse["errors"]; exists {
resp.Diagnostics.AddError(
"Server Returned Errors",
fmt.Sprintf("Errors from server: %v", errors),
)
}
return
}
if httpResponse.StatusCode != 200 {
resp.Diagnostics.AddError(
"Failed to Create Resource",
fmt.Sprintf("Failed to create resource: %s", httpResponse.Status),
)
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VirtfusionServerBuildResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data VirtfusionServerBuildResourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// If applicable, this is a great opportunity to initialize any necessary
// provider client data and make a call using it.
// httpResp, err := r.client.Do(httpReq)
// if err != nil {
// resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read example, got error: %s", err))
// return
// }
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VirtfusionServerBuildResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data VirtfusionServerBuildResourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// If applicable, this is a great opportunity to initialize any necessary
// provider client data and make a call using it.
// httpResp, err := r.client.Do(httpReq)
// if err != nil {
// resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update example, got error: %s", err))
// return
// }
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VirtfusionServerBuildResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data VirtfusionServerBuildResourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VirtfusionServerBuildResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

View File

@@ -1,374 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"github.com/hashicorp/terraform-plugin-framework/types"
"io"
"io/ioutil"
"net/http"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
)
// Ensure provider defined types fully satisfy framework interfaces.
var _ resource.Resource = &VirtfusionServerResource{}
var _ resource.ResourceWithImportState = &VirtfusionServerResource{}
func NewVirtfusionServerResource() resource.Resource {
return &VirtfusionServerResource{}
}
// VirtfusionServerResource defines the resource implementation.
type VirtfusionServerResource struct {
client *http.Client
}
// VirtfusionServerResourceModel describes the resource data model.
type VirtfusionServerResourceModel struct {
PackageId *int64 `tfsdk:"package_id" json:"packageId,omitempty"`
UserId *int64 `tfsdk:"user_id" json:"userId,omitempty"`
HypervisorId *int64 `tfsdk:"hypervisor_id" json:"hypervisorId,omitempty"`
Ipv4 *int64 `tfsdk:"ipv4" json:"ipv4,omitempty"`
Storage *int64 `tfsdk:"storage" json:"storage,omitempty"`
Memory *int64 `tfsdk:"memory" json:"memory,omitempty"`
Cores *int64 `tfsdk:"cores" json:"cpuCores,omitempty"`
Traffic *int64 `tfsdk:"traffic" json:"traffic,omitempty"`
InboundNetworkSpeed *int64 `tfsdk:"inbound_network_speed" json:"networkSpeedInbound,omitempty"`
OutboundNetworkSpeed *int64 `tfsdk:"outbound_network_speed" json:"networkSpeedOutbound,omitempty"`
StorageProfile *int64 `tfsdk:"storage_profile" json:"storageProfile,omitempty"`
NetworkProfile *int64 `tfsdk:"network_profile" json:"networkProfile,omitempty"`
Id types.Int64 `tfsdk:"id" json:"id"`
}
func (r *VirtfusionServerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server"
}
func (r *VirtfusionServerResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
// This description is used by the documentation generator and the language server.
MarkdownDescription: "Virtfusion Server Resource",
Attributes: map[string]schema.Attribute{
"package_id": schema.Int64Attribute{
MarkdownDescription: "Package ID",
Required: true,
},
"user_id": schema.Int64Attribute{
MarkdownDescription: "User ID",
Required: true,
},
"hypervisor_id": schema.Int64Attribute{
MarkdownDescription: "Hypervisor Group ID",
Required: true,
},
"ipv4": schema.Int64Attribute{
MarkdownDescription: "IPv4 Addresses to assign. Omit to use the default of 1 IPv4.",
Optional: true,
Computed: true,
Default: int64default.StaticInt64(1),
},
"storage": schema.Int64Attribute{
MarkdownDescription: "Primary storage size in GB. Omit to use the default storage size from the package.",
Optional: true,
},
"memory": schema.Int64Attribute{
MarkdownDescription: "How much memory to allocate in MB. Omit to use the default memory size from the package.",
Optional: true,
},
"cores": schema.Int64Attribute{
MarkdownDescription: "How many cores to allocate. Omit to use the default core count from the package.",
Optional: true,
},
"traffic": schema.Int64Attribute{
MarkdownDescription: "How much traffic to allocate in GB. Omit to use the default traffic size from the package. 0=Unlimited",
Optional: true,
},
"inbound_network_speed": schema.Int64Attribute{
MarkdownDescription: "Inbound network speed in kB/s. Omit to use the default inbound network speed from the package.",
Optional: true,
},
"outbound_network_speed": schema.Int64Attribute{
MarkdownDescription: "Outbound network speed in kB/s. Omit to use the default outbound network speed from the package.",
Optional: true,
},
"storage_profile": schema.Int64Attribute{
MarkdownDescription: "Storage profile ID. Omit to use the default storage profile from the package.",
Optional: true,
},
"network_profile": schema.Int64Attribute{
MarkdownDescription: "Network profile ID. Omit to use the default network profile from the package.",
Optional: true,
},
"id": schema.Int64Attribute{
MarkdownDescription: "Server ID",
Computed: true,
},
},
}
}
func (r *VirtfusionServerResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*http.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = client
}
func (r *VirtfusionServerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data VirtfusionServerResourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
createReq := VirtfusionServerResourceModel{
PackageId: data.PackageId,
UserId: data.UserId,
HypervisorId: data.HypervisorId,
Ipv4: data.Ipv4,
Storage: data.Storage,
Traffic: data.Traffic,
Memory: data.Memory,
Cores: data.Cores,
InboundNetworkSpeed: data.InboundNetworkSpeed,
OutboundNetworkSpeed: data.OutboundNetworkSpeed,
StorageProfile: data.StorageProfile,
NetworkProfile: data.NetworkProfile,
}
httpReqBody, err := json.Marshal(createReq)
if err != nil {
resp.Diagnostics.AddError(
"Unable to Create Resource",
"An unexpected error occurred while creating the resource create request. "+
"Please report this issue to the provider developers.\n\n"+
"JSON Error: "+err.Error(),
)
return
}
httpReq, err := http.NewRequest("POST", "/servers", bytes.NewBuffer(httpReqBody))
if err != nil {
resp.Diagnostics.AddError(
"Failed to Create Request",
fmt.Sprintf("Failed to create a new HTTP request: %s", err.Error()),
)
return
}
// Add any additional headers (Content-Type, etc.)
httpReq.Header.Set("Content-Type", "application/json")
httpResponse, err := r.client.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError(
"Failed to Execute Request",
fmt.Sprintf("Failed to execute HTTP request: %s", err.Error()),
)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
resp.Diagnostics.AddError(
"Failed to Close Request",
fmt.Sprintf("Failed to close HTTP request: %s", err.Error()),
)
return
}
}(httpResponse.Body)
if httpResponse.StatusCode == 422 {
responseBody, err := ioutil.ReadAll(httpResponse.Body)
if err != nil {
resp.Diagnostics.AddError(
"Failed to Read Response",
fmt.Sprintf("Failed to read HTTP response body: %s", err.Error()),
)
return
}
var errorResponse map[string]interface{}
err = json.Unmarshal(responseBody, &errorResponse)
if err != nil {
resp.Diagnostics.AddError(
"Failed to Parse Error Response",
fmt.Sprintf("Failed to parse HTTP response body: %s", err.Error()),
)
return
}
if errors, exists := errorResponse["errors"]; exists {
resp.Diagnostics.AddError(
"Server Returned Errors",
fmt.Sprintf("Errors from server: %v", errors),
)
}
return
}
if httpResponse.StatusCode != 201 {
resp.Diagnostics.AddError(
"Failed to Create Resource",
fmt.Sprintf("Failed to create resource: %s", httpResponse.Status),
)
return
}
responseBody, err := ioutil.ReadAll(httpResponse.Body)
if err != nil {
resp.Diagnostics.AddError(
"Failed to Read Response",
fmt.Sprintf("Failed to read HTTP response body: %s", err.Error()),
)
return
}
type ResponseData struct {
Data struct {
Id int64 `json:"id"`
Uuid string `json:"uuid"`
Name string `json:"name"`
} `json:"data"`
}
var responseData ResponseData
// Unmarshal the JSON response
err = json.Unmarshal(responseBody, &responseData)
if err != nil {
resp.Diagnostics.AddError(
"Failed to Parse Response",
fmt.Sprintf("Failed to parse HTTP response body: %s", err.Error()),
)
return
}
// Update the Terraform state with the server ID
data.Id = types.Int64Value(responseData.Data.Id)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VirtfusionServerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data VirtfusionServerResourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// If applicable, this is a great opportunity to initialize any necessary
// provider client data and make a call using it.
// httpResp, err := r.client.Do(httpReq)
// if err != nil {
// resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read example, got error: %s", err))
// return
// }
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VirtfusionServerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data VirtfusionServerResourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// If applicable, this is a great opportunity to initialize any necessary
// provider client data and make a call using it.
// httpResp, err := r.client.Do(httpReq)
// if err != nil {
// resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update example, got error: %s", err))
// return
// }
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VirtfusionServerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data VirtfusionServerResourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
httpReq, err := http.NewRequest("DELETE", fmt.Sprintf("/servers/%d?delay=0", data.Id.ValueInt64()), bytes.NewBuffer([]byte{}))
if err != nil {
resp.Diagnostics.AddError(
"Failed to Create Request",
fmt.Sprintf("Failed to create a new HTTP request: %s", err.Error()),
)
return
}
// Add any additional headers (Content-Type, etc.)
httpReq.Header.Set("Content-Type", "application/json")
httpResponse, err := r.client.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError(
"Failed to Execute Request",
fmt.Sprintf("Failed to execute HTTP request: %s", err.Error()),
)
return
}
if httpResponse.StatusCode != 204 {
resp.Diagnostics.AddError(
"Failed to Delete Resource",
fmt.Sprintf("Failed to delete resource: %s", httpResponse.Status),
)
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VirtfusionServerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

View File

@@ -1,332 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"bytes"
"context"
"encoding/json"
"fmt"
"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"
"io"
"net/http"
)
// Ensure provider defined types fully satisfy framework interfaces.
var _ resource.Resource = &VirtfusionSSHResource{}
var _ resource.ResourceWithImportState = &VirtfusionSSHResource{}
func NewVirtfusionSSHResource() resource.Resource {
return &VirtfusionSSHResource{}
}
// VirtfusionSSHResource defines the resource implementation.
type VirtfusionSSHResource struct {
client *http.Client
}
// VirtfusionSSHResourceModel describes the resource data model.
type VirtfusionSSHResourceModel struct {
UserId *int64 `tfsdk:"user_id" json:"userId"`
Name *string `tfsdk:"name" json:"name"`
PublicKey *string `tfsdk:"public_key" json:"publicKey"`
Id types.Int64 `tfsdk:"id" json:"id,omitempty"`
}
func (r *VirtfusionSSHResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_ssh"
}
func (r *VirtfusionSSHResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
// This description is used by the documentation generator and the language server.
MarkdownDescription: "Virtfusion SSH Resource",
Attributes: map[string]schema.Attribute{
"user_id": schema.Int64Attribute{
Description: "User ID",
Required: true,
},
"name": schema.StringAttribute{
Description: "Key Name",
Required: true,
},
"public_key": schema.StringAttribute{
Description: "Public Key",
Required: true,
},
"id": schema.Int64Attribute{
Description: "SSH Key ID",
Computed: true,
},
},
}
}
func (r *VirtfusionSSHResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*http.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = client
}
func (r *VirtfusionSSHResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data VirtfusionSSHResourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
createReq := VirtfusionSSHResourceModel{
UserId: data.UserId,
Name: data.Name,
PublicKey: data.PublicKey,
}
// Convert the model to JSON
jsonReq, err := json.Marshal(createReq)
if err != nil {
resp.Diagnostics.AddError(
"Failed to marshal request body",
fmt.Sprintf("Failed to marshal request body: %s", err.Error()),
)
return
}
httpReq, err := r.client.Post("/ssh_keys", "application/json", bytes.NewBuffer(jsonReq))
if err != nil {
resp.Diagnostics.AddError(
"Request failed",
fmt.Sprintf("Request failed: %s", err.Error()),
)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
resp.Diagnostics.AddError(
"Failed to close response body",
fmt.Sprintf("Failed to close response body: %s", err.Error()),
)
}
}(httpReq.Body)
if httpReq.StatusCode != 201 {
if httpReq.StatusCode == 422 {
responseBody, _ := io.ReadAll(httpReq.Body)
var errorResponse map[string]interface{}
err = json.Unmarshal(responseBody, &errorResponse)
if errors, exists := errorResponse["errors"]; exists {
resp.Diagnostics.AddError(
"Failed to create SSH key",
fmt.Sprintf("Errors from server: %v", errors),
)
return
}
}
resp.Diagnostics.AddError(
"Invalid Request",
fmt.Sprintf("Failed to create SSH key: %s", httpReq.Status),
)
return
}
// Read the response body into the model. The response is expected to be a JSON object with the body of the created
// ssh key within the `data` field. The `data` field is a JSON object with the ssh key data.
responseBody, err := io.ReadAll(httpReq.Body)
type ResponseData struct {
Data struct {
Id int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
CreatedAt string `json:"createdAt"`
} `json:"data"`
}
var responseData ResponseData
// Unmarshal the response body into the model
err = json.Unmarshal(responseBody, &responseData)
if err != nil {
resp.Diagnostics.AddError(
"Failed to unmarshal response body",
fmt.Sprintf("Failed to unmarshal response body: %s", err.Error()),
)
return
}
data.Id = types.Int64Value(responseData.Data.Id)
data.Name = &responseData.Data.Name
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VirtfusionSSHResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data VirtfusionSSHResourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
httpReq, err := http.NewRequest("GET", fmt.Sprintf("/ssh_keys/%d", data.Id.ValueInt64()), nil)
if err != nil {
resp.Diagnostics.AddError(
"Failed to Create Request",
fmt.Sprintf("Failed to create a new HTTP request: %s", err.Error()),
)
return
}
// If the resource returns a 404, then the resource has been deleted. Return an empty state.
httpResponse, err := r.client.Do(httpReq)
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
resp.Diagnostics.AddError(
"Failed to close response body",
fmt.Sprintf("Failed to close response body: %s", err.Error()),
)
}
}(httpResponse.Body)
if err != nil {
resp.Diagnostics.AddError(
"Failed to Execute Request",
fmt.Sprintf("Failed to execute HTTP request: %s", err.Error()),
)
return
}
if httpResponse.StatusCode == 404 {
resp.State.RemoveResource(ctx)
return
}
var responseData struct {
Data struct {
Id int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Enabled bool `json:"enabled"`
CreatedAt string `json:"created"`
UpdatedAt string `json:"updated"`
PublicKeyHash string `json:"publicKey"`
} `json:"data"`
}
err = json.NewDecoder(httpResponse.Body).Decode(&responseData)
if err != nil {
resp.Diagnostics.AddError(
"Failed to decode response body",
fmt.Sprintf("Failed to decode response body: %s", err.Error()),
)
return
}
data.Name = &responseData.Data.Name
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VirtfusionSSHResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data VirtfusionSSHResourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VirtfusionSSHResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data VirtfusionSSHResourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
httpReq, err := http.NewRequest("DELETE", fmt.Sprintf("/ssh_keys/%d", data.Id.ValueInt64()), nil)
if err != nil {
resp.Diagnostics.AddError(
"Failed to Create Request",
fmt.Sprintf("Failed to create a new HTTP request: %s", err.Error()),
)
return
}
// Add any additional headers (Content-Type, etc.)
httpReq.Header.Set("Content-Type", "application/json")
httpResponse, err := r.client.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError(
"Failed to Execute Request",
fmt.Sprintf("Failed to execute HTTP request: %s", err.Error()),
)
return
}
if httpResponse.StatusCode != 204 {
resp.Diagnostics.AddError(
"Failed to Delete Resource",
fmt.Sprintf("Failed to delete resource: %s", httpResponse.Status),
)
return
}
if err != nil {
resp.Diagnostics.AddError(
"Request failed",
fmt.Sprintf("Request failed: %s", err.Error()),
)
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VirtfusionSSHResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}