-
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
[Terraform] New Terraform Provider generator #22949
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
ad46e19
50b9cdd
c5a0e81
cfdb868
893e6a9
78f9db4
f570e8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| generatorName: terraform-provider | ||
| outputDir: samples/client/petstore/terraform | ||
| inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml | ||
| templateDir: modules/openapi-generator/src/main/resources/terraform-provider | ||
| gitHost: github.com | ||
| gitUserId: example | ||
| gitRepoId: terraform-provider-petstore | ||
| additionalProperties: | ||
| providerName: "petstore" | ||
| providerAddress: "registry.terraform.io/example/petstore" | ||
| hideGenerationTimestamp: "true" |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| default: testacc | ||
|
|
||
| # Run acceptance tests | ||
| .PHONY: testacc | ||
| testacc: | ||
| TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m | ||
|
|
||
| # Build provider | ||
| .PHONY: build | ||
| build: | ||
| go build -o terraform-provider-{{providerName}} | ||
|
|
||
| # Install provider locally | ||
| .PHONY: install | ||
| install: build | ||
| mkdir -p ~/.terraform.d/plugins/{{providerAddress}}/{{providerVersion}}/$(shell go env GOOS)_$(shell go env GOARCH) | ||
| mv terraform-provider-{{providerName}} ~/.terraform.d/plugins/{{providerAddress}}/{{providerVersion}}/$(shell go env GOOS)_$(shell go env GOARCH)/ | ||
|
|
||
| # Generate documentation | ||
| .PHONY: docs | ||
| docs: | ||
| go generate ./... | ||
|
|
||
| # Run linter | ||
| .PHONY: lint | ||
| lint: | ||
| golangci-lint run ./... |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # Terraform Provider {{providerName}} | ||
|
|
||
| This Terraform provider was generated using [OpenAPI Generator](https://openapi-generator.tech). | ||
|
|
||
| ## Requirements | ||
|
|
||
| - [Terraform](https://www.terraform.io/downloads.html) >= 1.0 | ||
| - [Go](https://golang.org/doc/install) >= 1.21 | ||
|
|
||
| ## Building The Provider | ||
|
|
||
| 1. Clone the repository | ||
| 2. Enter the repository directory | ||
| 3. Build the provider using the Go `install` command: | ||
|
|
||
| ```shell | ||
| go install | ||
| ``` | ||
|
|
||
| ## Using the provider | ||
|
|
||
| ```hcl | ||
| terraform { | ||
| required_providers { | ||
| {{providerName}} = { | ||
| source = "{{providerAddress}}" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| provider "{{providerName}}" { | ||
| endpoint = "{{basePath}}" | ||
| } | ||
| ``` | ||
|
|
||
| ## Developing the Provider | ||
|
|
||
| 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). | ||
|
|
||
| To compile the provider, run `go install`. This will build the provider and put the provider binary in the `$GOPATH/bin` directory. | ||
|
|
||
| To generate or update documentation, run `go generate`. | ||
|
|
||
| In order to run the full suite of Acceptance tests, run `make testacc`. | ||
|
|
||
| ```shell | ||
| make testacc | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| {{>partial_header}} | ||
| package client | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| ) | ||
|
|
||
| // Client is the API client used to interact with the API. | ||
| type Client struct { | ||
| BaseURL string | ||
| HTTPClient *http.Client | ||
| ApiKey string | ||
| Token string | ||
| Username string | ||
| Password string | ||
| } | ||
|
|
||
| // NewClient creates a new API client with the given base URL. | ||
| func NewClient(baseURL string) *Client { | ||
| return &Client{ | ||
| BaseURL: baseURL, | ||
| HTTPClient: &http.Client{}, | ||
| } | ||
| } | ||
|
|
||
| // DoRequest executes an HTTP request and returns the response body. | ||
| func (c *Client) DoRequest(ctx context.Context, method, path string, body interface{}) ([]byte, error) { | ||
| var reqBody io.Reader | ||
| if body != nil { | ||
| jsonBody, err := json.Marshal(body) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("error marshaling request body: %w", err) | ||
| } | ||
| reqBody = bytes.NewBuffer(jsonBody) | ||
| } | ||
|
|
||
| req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, reqBody) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("error creating request: %w", err) | ||
| } | ||
|
|
||
| if body != nil { | ||
| req.Header.Set("Content-Type", "application/json") | ||
| } | ||
| req.Header.Set("Accept", "application/json") | ||
|
|
||
| // Authentication | ||
| if c.Token != "" { | ||
| req.Header.Set("Authorization", "Bearer "+c.Token) | ||
| } else if c.ApiKey != "" { | ||
| req.Header.Set("Authorization", c.ApiKey) | ||
| } else if c.Username != "" { | ||
| req.SetBasicAuth(c.Username, c.Password) | ||
| } | ||
|
|
||
| resp, err := c.HTTPClient.Do(req) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("error making request: %w", err) | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| respBody, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("error reading response body: %w", err) | ||
| } | ||
|
|
||
| if resp.StatusCode < 200 || resp.StatusCode >= 300 { | ||
| return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(respBody)) | ||
| } | ||
|
|
||
| return respBody, nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| {{#operations}} | ||
| {{>partial_header}} | ||
| package provider | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| {{#hasRead}} | ||
| "encoding/json" | ||
| {{/hasRead}} | ||
|
|
||
| "github.com/hashicorp/terraform-plugin-framework/datasource" | ||
| "github.com/hashicorp/terraform-plugin-framework/datasource/schema" | ||
| {{#hasRead}} | ||
| "github.com/hashicorp/terraform-plugin-log/tflog" | ||
| {{/hasRead}} | ||
|
|
||
| "{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/internal/client" | ||
| ) | ||
|
|
||
| var _ datasource.DataSource = &{{resourceClassName}}DataSource{} | ||
|
|
||
| func New{{resourceClassName}}DataSource() datasource.DataSource { | ||
| return &{{resourceClassName}}DataSource{} | ||
| } | ||
|
|
||
| type {{resourceClassName}}DataSource struct { | ||
| client *client.Client | ||
| } | ||
|
|
||
| func (d *{{resourceClassName}}DataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { | ||
| resp.TypeName = req.ProviderTypeName + "_{{resourceName}}" | ||
| } | ||
|
|
||
| func (d *{{resourceClassName}}DataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { | ||
| resp.Schema = schema.Schema{ | ||
| Description: "Fetches a {{resourceName}} data source.", | ||
| Attributes: map[string]schema.Attribute{ | ||
| {{#tfAttributes}} | ||
| "{{terraformName}}": schema.{{#isString}}String{{/isString}}{{#isInt64}}Int64{{/isInt64}}{{#isFloat64}}Float64{{/isFloat64}}{{#isBool}}Bool{{/isBool}}{{^isString}}{{^isInt64}}{{^isFloat64}}{{^isBool}}String{{/isBool}}{{/isFloat64}}{{/isInt64}}{{/isString}}Attribute{ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Data source schema treats every OpenAPI Prompt for AI agents |
||
| {{#isRequired}} | ||
| Required: true, | ||
| {{/isRequired}} | ||
| {{^isRequired}} | ||
| Computed: true, | ||
| {{/isRequired}} | ||
| Description: "{{description}}", | ||
| }, | ||
| {{/tfAttributes}} | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func (d *{{resourceClassName}}DataSource) 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 | ||
| } | ||
|
|
||
| {{#hasRead}} | ||
| func (d *{{resourceClassName}}DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { | ||
| var config {{resourceClassName}}Model | ||
|
|
||
| resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) | ||
| if resp.Diagnostics.HasError() { | ||
| return | ||
| } | ||
|
|
||
| {{#readHasPathParams}} | ||
| respBody, err := d.client.DoRequest(ctx, "{{readMethod}}", fmt.Sprintf("{{readPath}}", config.{{idFieldExported}}{{idFieldValueAccessor}}), nil) | ||
| {{/readHasPathParams}} | ||
| {{^readHasPathParams}} | ||
| respBody, err := d.client.DoRequest(ctx, "{{readMethod}}", "{{readPath}}", nil) | ||
| {{/readHasPathParams}} | ||
| if err != nil { | ||
| resp.Diagnostics.AddError("Error reading {{resourceName}}", err.Error()) | ||
| return | ||
| } | ||
|
|
||
| var result client.{{responseModel}} | ||
| if err := json.Unmarshal(respBody, &result); err != nil { | ||
| resp.Diagnostics.AddError("Error parsing response", err.Error()) | ||
| return | ||
| } | ||
|
|
||
| config.FromClientModel(&result) | ||
|
|
||
| tflog.Trace(ctx, "read {{resourceName}} data source") | ||
| resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) | ||
| } | ||
| {{/hasRead}} | ||
| {{^hasRead}} | ||
| func (d *{{resourceClassName}}DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { | ||
| resp.Diagnostics.AddError("Not Supported", "Read is not supported for {{resourceName}}") | ||
| } | ||
| {{/hasRead}} | ||
| {{/operations}} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| *.dll | ||
| *.exe | ||
| *.exe~ | ||
| *.dylib | ||
| *.so | ||
| *.test | ||
| *.out | ||
| *.tfstate | ||
| *.tfstate.* | ||
| .terraform/ | ||
| terraform.tfvars | ||
| terraform-provider-{{providerName}} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| module {{gitHost}}/{{gitUserId}}/{{gitRepoId}} | ||
|
|
||
| go 1.22 | ||
|
|
||
| toolchain go1.24.4 | ||
|
|
||
| require ( | ||
| github.com/hashicorp/terraform-plugin-framework v1.17.0 | ||
| github.com/hashicorp/terraform-plugin-log v0.10.0 | ||
| github.com/hashicorp/terraform-plugin-testing v1.14.0 | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| {{>partial_header}} | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "flag" | ||
| "log" | ||
|
|
||
| "github.com/hashicorp/terraform-plugin-framework/providerserver" | ||
|
|
||
| "{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/internal/provider" | ||
| ) | ||
|
|
||
| var ( | ||
| version string = "{{providerVersion}}" | ||
| ) | ||
|
|
||
| func main() { | ||
| var debug bool | ||
|
|
||
| flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") | ||
| flag.Parse() | ||
|
|
||
| opts := providerserver.ServeOpts{ | ||
| Address: "{{providerAddress}}", | ||
| Debug: debug, | ||
| } | ||
|
|
||
| err := providerserver.Serve(context.Background(), provider.New(version), opts) | ||
|
|
||
| if err != nil { | ||
| log.Fatal(err.Error()) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| {{>partial_header}} | ||
| package client | ||
| {{#models}} | ||
| {{#model}} | ||
|
|
||
| // {{classname}} - {{{description}}}{{^description}}{{classname}} struct{{/description}} | ||
| type {{classname}} struct { | ||
| {{#vars}} | ||
| {{name}} {{#isArray}}[]{{#items}}{{dataType}}{{/items}}{{/isArray}}{{^isArray}}{{#isMap}}map[string]{{#items}}{{dataType}}{{/items}}{{/isMap}}{{^isMap}}{{dataType}}{{/isMap}}{{/isArray}}{{{vendorExtensions.x-go-datatag}}} | ||
| {{/vars}} | ||
| } | ||
| {{/model}} | ||
| {{/models}} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. |
Uh oh!
There was an error while loading. Please reload this page.