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

@@ -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 {