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:
154
scripts/check-endpoint-drift.go
Normal file
154
scripts/check-endpoint-drift.go
Normal file
@@ -0,0 +1,154 @@
|
||||
//go:build ignore
|
||||
|
||||
// check-endpoint-drift compares the current OpenAPI spec against the endpoint manifest
|
||||
// and reports any added or removed endpoints. Exit 0 if no drift, exit 1 if drift detected.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Endpoint struct {
|
||||
Method string `json:"method" yaml:"-"`
|
||||
Path string `json:"path" yaml:"-"`
|
||||
Summary string `json:"summary" yaml:"-"`
|
||||
Tag string `json:"tag" yaml:"-"`
|
||||
}
|
||||
|
||||
func endpointKey(e Endpoint) string {
|
||||
return e.Method + " " + e.Path
|
||||
}
|
||||
|
||||
type OpenAPISpec struct {
|
||||
Paths map[string]map[string]struct {
|
||||
Summary string `yaml:"summary"`
|
||||
Tags []string `yaml:"tags"`
|
||||
} `yaml:"paths"`
|
||||
}
|
||||
|
||||
func extractEndpoints(specPath string) ([]Endpoint, error) {
|
||||
data, err := os.ReadFile(specPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading spec: %w", err)
|
||||
}
|
||||
|
||||
var spec OpenAPISpec
|
||||
if err := yaml.Unmarshal(data, &spec); err != nil {
|
||||
return nil, fmt.Errorf("parsing spec: %w", err)
|
||||
}
|
||||
|
||||
validMethods := map[string]bool{
|
||||
"get": true, "post": true, "put": true, "delete": true, "patch": true,
|
||||
}
|
||||
|
||||
var endpoints []Endpoint
|
||||
for path, methods := range spec.Paths {
|
||||
for method, details := range methods {
|
||||
if !validMethods[strings.ToLower(method)] {
|
||||
continue
|
||||
}
|
||||
tag := "Untagged"
|
||||
if len(details.Tags) > 0 {
|
||||
tag = details.Tags[0]
|
||||
}
|
||||
endpoints = append(endpoints, Endpoint{
|
||||
Method: strings.ToUpper(method),
|
||||
Path: path,
|
||||
Summary: details.Summary,
|
||||
Tag: tag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(endpoints, func(i, j int) bool {
|
||||
if endpoints[i].Path != endpoints[j].Path {
|
||||
return endpoints[i].Path < endpoints[j].Path
|
||||
}
|
||||
return endpoints[i].Method < endpoints[j].Method
|
||||
})
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Resolve paths relative to this script's location
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
rootDir := filepath.Dir(filepath.Dir(filename))
|
||||
|
||||
specPath := filepath.Join(rootDir, "openapi.yaml")
|
||||
manifestPath := filepath.Join(rootDir, "endpoint-manifest.json")
|
||||
|
||||
current, err := extractEndpoints(specPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error extracting endpoints: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
manifestData, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: endpoint-manifest.json not found. Copy it from the MCP repo first.\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var manifest []Endpoint
|
||||
if err := json.Unmarshal(manifestData, &manifest); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing manifest: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
manifestKeys := make(map[string]bool)
|
||||
for _, e := range manifest {
|
||||
manifestKeys[endpointKey(e)] = true
|
||||
}
|
||||
currentKeys := make(map[string]bool)
|
||||
for _, e := range current {
|
||||
currentKeys[endpointKey(e)] = true
|
||||
}
|
||||
|
||||
var added, removed []Endpoint
|
||||
for _, e := range current {
|
||||
if !manifestKeys[endpointKey(e)] {
|
||||
added = append(added, e)
|
||||
}
|
||||
}
|
||||
for _, e := range manifest {
|
||||
if !currentKeys[endpointKey(e)] {
|
||||
removed = append(removed, e)
|
||||
}
|
||||
}
|
||||
|
||||
if len(added) == 0 && len(removed) == 0 {
|
||||
fmt.Printf("No endpoint drift detected. %d endpoints match the manifest.\n", len(current))
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
fmt.Println("Endpoint drift detected!")
|
||||
fmt.Println()
|
||||
|
||||
if len(added) > 0 {
|
||||
fmt.Printf("New endpoints (%d):\n", len(added))
|
||||
for _, e := range added {
|
||||
fmt.Printf(" + %s %s — %s [%s]\n", e.Method, e.Path, e.Summary, e.Tag)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
if len(removed) > 0 {
|
||||
fmt.Printf("Removed endpoints (%d):\n", len(removed))
|
||||
for _, e := range removed {
|
||||
fmt.Printf(" - %s %s — %s [%s]\n", e.Method, e.Path, e.Summary, e.Tag)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
fmt.Println("Update endpoint-manifest.json from the MCP repo to resolve drift.")
|
||||
os.Exit(1)
|
||||
}
|
||||
Reference in New Issue
Block a user