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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user