Skip to content

Commit ad46e19

Browse files
jasondamourclaude
andcommitted
[new] Add Terraform provider generator
Add a new experimental code generator that produces HashiCorp Terraform providers from OpenAPI specifications using the Plugin Framework SDK. The generator (terraform-provider) supports: - CRUD operation detection from REST patterns and vendor extensions - Resource, data source, and model generation per API tag - HTTP client with API key, bearer token, and basic auth - Type mapping from OpenAPI/Go types to Terraform schema types - Import state support - Configurable provider name, registry address, and version Includes mustache templates for: provider, resources, data sources, models, client, go.mod, GNUmakefile, README, and example configs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d47ab0f commit ad46e19

16 files changed

Lines changed: 1489 additions & 0 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TerraformProviderCodegen.java

Lines changed: 541 additions & 0 deletions
Large diffs are not rendered by default.

modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ org.openapitools.codegen.languages.StaticHtml2Generator
148148
org.openapitools.codegen.languages.Swift5ClientCodegen
149149
org.openapitools.codegen.languages.Swift6ClientCodegen
150150
org.openapitools.codegen.languages.SwiftCombineClientCodegen
151+
org.openapitools.codegen.languages.TerraformProviderCodegen
151152
org.openapitools.codegen.languages.TypeScriptClientCodegen
152153
org.openapitools.codegen.languages.TypeScriptAngularClientCodegen
153154
org.openapitools.codegen.languages.TypeScriptAureliaClientCodegen
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
default: testacc
2+
3+
# Run acceptance tests
4+
.PHONY: testacc
5+
testacc:
6+
TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m
7+
8+
# Build provider
9+
.PHONY: build
10+
build:
11+
go build -o terraform-provider-{{providerName}}
12+
13+
# Install provider locally
14+
.PHONY: install
15+
install: build
16+
mkdir -p ~/.terraform.d/plugins/{{providerAddress}}/{{providerVersion}}/$(shell go env GOOS)_$(shell go env GOARCH)
17+
mv terraform-provider-{{providerName}} ~/.terraform.d/plugins/{{providerAddress}}/{{providerVersion}}/$(shell go env GOOS)_$(shell go env GOARCH)/
18+
19+
# Generate documentation
20+
.PHONY: docs
21+
docs:
22+
go generate ./...
23+
24+
# Run linter
25+
.PHONY: lint
26+
lint:
27+
golangci-lint run ./...
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Terraform Provider {{providerName}}
2+
3+
This Terraform provider was generated using [OpenAPI Generator](https://openapi-generator.tech).
4+
5+
## Requirements
6+
7+
- [Terraform](https://www.terraform.io/downloads.html) >= 1.0
8+
- [Go](https://golang.org/doc/install) >= 1.21
9+
10+
## Building The Provider
11+
12+
1. Clone the repository
13+
2. Enter the repository directory
14+
3. Build the provider using the Go `install` command:
15+
16+
```shell
17+
go install
18+
```
19+
20+
## Using the provider
21+
22+
```hcl
23+
terraform {
24+
required_providers {
25+
{{providerName}} = {
26+
source = "{{providerAddress}}"
27+
}
28+
}
29+
}
30+
31+
provider "{{providerName}}" {
32+
endpoint = "{{basePath}}"
33+
}
34+
```
35+
36+
## Developing the Provider
37+
38+
If you wish to work on the provider, you'll first need [Go](http://www.golang.org) installed on your machine (see [Requirements](#requirements) above).
39+
40+
To compile the provider, run `go install`. This will build the provider and put the provider binary in the `$GOPATH/bin` directory.
41+
42+
To generate or update documentation, run `go generate`.
43+
44+
In order to run the full suite of Acceptance tests, run `make testacc`.
45+
46+
```shell
47+
make testacc
48+
```
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
{{>partial_header}}
2+
package client
3+
4+
import (
5+
"bytes"
6+
"context"
7+
"encoding/json"
8+
"fmt"
9+
"io"
10+
"net/http"
11+
)
12+
13+
// Client is the API client used to interact with the API.
14+
type Client struct {
15+
BaseURL string
16+
HTTPClient *http.Client
17+
ApiKey string
18+
Token string
19+
Username string
20+
Password string
21+
}
22+
23+
// NewClient creates a new API client with the given base URL.
24+
func NewClient(baseURL string) *Client {
25+
return &Client{
26+
BaseURL: baseURL,
27+
HTTPClient: &http.Client{},
28+
}
29+
}
30+
31+
// DoRequest executes an HTTP request and returns the response body.
32+
func (c *Client) DoRequest(ctx context.Context, method, path string, body interface{}) ([]byte, error) {
33+
var reqBody io.Reader
34+
if body != nil {
35+
jsonBody, err := json.Marshal(body)
36+
if err != nil {
37+
return nil, fmt.Errorf("error marshaling request body: %w", err)
38+
}
39+
reqBody = bytes.NewBuffer(jsonBody)
40+
}
41+
42+
req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, reqBody)
43+
if err != nil {
44+
return nil, fmt.Errorf("error creating request: %w", err)
45+
}
46+
47+
if body != nil {
48+
req.Header.Set("Content-Type", "application/json")
49+
}
50+
req.Header.Set("Accept", "application/json")
51+
52+
// Authentication
53+
if c.Token != "" {
54+
req.Header.Set("Authorization", "Bearer "+c.Token)
55+
} else if c.ApiKey != "" {
56+
req.Header.Set("Authorization", c.ApiKey)
57+
} else if c.Username != "" {
58+
req.SetBasicAuth(c.Username, c.Password)
59+
}
60+
61+
resp, err := c.HTTPClient.Do(req)
62+
if err != nil {
63+
return nil, fmt.Errorf("error making request: %w", err)
64+
}
65+
defer resp.Body.Close()
66+
67+
respBody, err := io.ReadAll(resp.Body)
68+
if err != nil {
69+
return nil, fmt.Errorf("error reading response body: %w", err)
70+
}
71+
72+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
73+
return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(respBody))
74+
}
75+
76+
return respBody, nil
77+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
{{#operations}}
2+
{{>partial_header}}
3+
package provider
4+
5+
import (
6+
"context"
7+
"fmt"
8+
{{#hasRead}}
9+
"encoding/json"
10+
{{/hasRead}}
11+
12+
"github.com/hashicorp/terraform-plugin-framework/datasource"
13+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
14+
{{#hasRead}}
15+
"github.com/hashicorp/terraform-plugin-log/tflog"
16+
{{/hasRead}}
17+
18+
"{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/internal/client"
19+
)
20+
21+
var _ datasource.DataSource = &{{resourceClassName}}DataSource{}
22+
23+
func New{{resourceClassName}}DataSource() datasource.DataSource {
24+
return &{{resourceClassName}}DataSource{}
25+
}
26+
27+
type {{resourceClassName}}DataSource struct {
28+
client *client.Client
29+
}
30+
31+
func (d *{{resourceClassName}}DataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
32+
resp.TypeName = req.ProviderTypeName + "_{{resourceName}}"
33+
}
34+
35+
func (d *{{resourceClassName}}DataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
36+
resp.Schema = schema.Schema{
37+
Description: "Fetches a {{resourceName}} data source.",
38+
Attributes: map[string]schema.Attribute{
39+
{{#tfAttributes}}
40+
"{{terraformName}}": schema.{{#isString}}String{{/isString}}{{#isInt64}}Int64{{/isInt64}}{{#isFloat64}}Float64{{/isFloat64}}{{#isBool}}Bool{{/isBool}}{{^isString}}{{^isInt64}}{{^isFloat64}}{{^isBool}}String{{/isBool}}{{/isFloat64}}{{/isInt64}}{{/isString}}Attribute{
41+
{{#isRequired}}
42+
Required: true,
43+
{{/isRequired}}
44+
{{^isRequired}}
45+
Computed: true,
46+
{{/isRequired}}
47+
Description: "{{description}}",
48+
},
49+
{{/tfAttributes}}
50+
},
51+
}
52+
}
53+
54+
func (d *{{resourceClassName}}DataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
55+
if req.ProviderData == nil {
56+
return
57+
}
58+
59+
c, ok := req.ProviderData.(*client.Client)
60+
if !ok {
61+
resp.Diagnostics.AddError(
62+
"Unexpected Data Source Configure Type",
63+
fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
64+
)
65+
return
66+
}
67+
68+
d.client = c
69+
}
70+
71+
{{#hasRead}}
72+
func (d *{{resourceClassName}}DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
73+
var config {{resourceClassName}}Model
74+
75+
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
76+
if resp.Diagnostics.HasError() {
77+
return
78+
}
79+
80+
{{#readHasPathParams}}
81+
respBody, err := d.client.DoRequest(ctx, "{{readMethod}}", fmt.Sprintf("{{readPath}}", config.{{idFieldExported}}{{idFieldValueAccessor}}), nil)
82+
{{/readHasPathParams}}
83+
{{^readHasPathParams}}
84+
respBody, err := d.client.DoRequest(ctx, "{{readMethod}}", "{{readPath}}", nil)
85+
{{/readHasPathParams}}
86+
if err != nil {
87+
resp.Diagnostics.AddError("Error reading {{resourceName}}", err.Error())
88+
return
89+
}
90+
91+
var result client.{{responseModel}}
92+
if err := json.Unmarshal(respBody, &result); err != nil {
93+
resp.Diagnostics.AddError("Error parsing response", err.Error())
94+
return
95+
}
96+
97+
config.FromClientModel(&result)
98+
99+
tflog.Trace(ctx, "read {{resourceName}} data source")
100+
resp.Diagnostics.Append(resp.State.Set(ctx, &config)...)
101+
}
102+
{{/hasRead}}
103+
{{^hasRead}}
104+
func (d *{{resourceClassName}}DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
105+
resp.Diagnostics.AddError("Not Supported", "Read is not supported for {{resourceName}}")
106+
}
107+
{{/hasRead}}
108+
{{/operations}}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
*.dll
2+
*.exe
3+
*.exe~
4+
*.dylib
5+
*.so
6+
*.test
7+
*.out
8+
*.tfstate
9+
*.tfstate.*
10+
.terraform/
11+
terraform.tfvars
12+
terraform-provider-{{providerName}}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module {{gitHost}}/{{gitUserId}}/{{gitRepoId}}
2+
3+
go 1.22
4+
5+
toolchain go1.24.4
6+
7+
require (
8+
github.com/hashicorp/terraform-plugin-framework v1.17.0
9+
github.com/hashicorp/terraform-plugin-log v0.10.0
10+
github.com/hashicorp/terraform-plugin-testing v1.14.0
11+
)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{{>partial_header}}
2+
package main
3+
4+
import (
5+
"context"
6+
"flag"
7+
"log"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/providerserver"
10+
11+
"{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/internal/provider"
12+
)
13+
14+
var (
15+
version string = "{{providerVersion}}"
16+
)
17+
18+
func main() {
19+
var debug bool
20+
21+
flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve")
22+
flag.Parse()
23+
24+
opts := providerserver.ServeOpts{
25+
Address: "{{providerAddress}}",
26+
Debug: debug,
27+
}
28+
29+
err := providerserver.Serve(context.Background(), provider.New(version), opts)
30+
31+
if err != nil {
32+
log.Fatal(err.Error())
33+
}
34+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{{>partial_header}}
2+
package client
3+
{{#models}}
4+
{{#model}}
5+
6+
// {{classname}} - {{{description}}}{{^description}}{{classname}} struct{{/description}}
7+
type {{classname}} struct {
8+
{{#vars}}
9+
{{name}} {{#isArray}}[]{{#items}}{{dataType}}{{/items}}{{/isArray}}{{^isArray}}{{#isMap}}map[string]{{#items}}{{dataType}}{{/items}}{{/isMap}}{{^isMap}}{{dataType}}{{/isMap}}{{/isArray}}{{{vendorExtensions.x-go-datatag}}}
10+
{{/vars}}
11+
}
12+
{{/model}}
13+
{{/models}}

0 commit comments

Comments
 (0)