## Description
<!-- **Please link some issue here describing what you are trying to achieve.**
In case there is no issue present for your PR, please consider creating one.
At least please give us some description what you are trying to achieve and why your change is needed. -->
relates to #1234
## Checklist
- [ ] Issue was linked above
- [ ] Code format was applied: `make fmt`
- [ ] Examples were added / adjusted (see `examples/` directory)
- [x] Docs are up-to-date: `make generate-docs` (will be checked by CI)
- [ ] Unit tests got implemented or updated
- [ ] Acceptance tests got implemented or updated (see e.g. [here](f5f99d1709/stackit/internal/services/dns/dns_acc_test.go))
- [x] Unit tests are passing: `make test` (will be checked by CI)
- [x] No linter issues: `make lint` (will be checked by CI)
Co-authored-by: Marcel S. Henselin <marcel.henselin@stackit.cloud>
Co-authored-by: marcel.henselin <marcel.henselin@stackit.cloud>
Reviewed-on: #81
429 lines
13 KiB
Go Template
429 lines
13 KiB
Go Template
package {{.PackageName}}
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/identityschema"
|
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
|
"github.com/stackitcloud/stackit-sdk-go/core/config"
|
|
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion"
|
|
|
|
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/{{.PackageName}}"
|
|
|
|
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core"
|
|
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils"
|
|
|
|
{{.PackageName}}ResGen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/{{.PackageName}}/{{.NameSnake}}/resources_gen"
|
|
)
|
|
|
|
var (
|
|
_ resource.Resource = &{{.NameCamel}}Resource{}
|
|
_ resource.ResourceWithConfigure = &{{.NameCamel}}Resource{}
|
|
_ resource.ResourceWithImportState = &{{.NameCamel}}Resource{}
|
|
_ resource.ResourceWithModifyPlan = &{{.NameCamel}}Resource{}
|
|
_ resource.ResourceWithIdentity = &{{.NameCamel}}Resource{}
|
|
)
|
|
|
|
func New{{.NamePascal}}Resource() resource.Resource {
|
|
return &{{.NameCamel}}Resource{}
|
|
}
|
|
|
|
type {{.NameCamel}}Resource struct{
|
|
client *{{.PackageName}}.APIClient
|
|
providerData core.ProviderData
|
|
}
|
|
|
|
// resourceModel represents the Terraform resource state
|
|
type resourceModel = {{.PackageName}}.{{.NamePascal}}Model
|
|
|
|
type {{.NamePascal}}ResourceIdentityModel struct {
|
|
ProjectID types.String `tfsdk:"project_id"`
|
|
Region types.String `tfsdk:"region"`
|
|
// TODO: implement further needed parts
|
|
{{.NamePascal}}ID types.String `tfsdk:"{{.NameSnake}}_id"`
|
|
}
|
|
|
|
// Metadata defines terraform resource name
|
|
func (r *{{.NameCamel}}Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
|
resp.TypeName = req.ProviderTypeName + "_{{.PackageName}}_{{.NameSnake}}"
|
|
}
|
|
|
|
//go:embed planModifiers.yaml
|
|
var modifiersFileByte []byte
|
|
|
|
// Schema loads the schema from generated files and adds plan modifiers
|
|
func (r *{{.NameCamel}}Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
|
schema = {{.PackageName}}ResGen.{{.NamePascal}}ResourceSchema(ctx)
|
|
|
|
fields, err := {{.PackageName}}Utils.ReadModifiersConfig(modifiersFileByte)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("error during read modifiers config file", err.Error())
|
|
return
|
|
}
|
|
|
|
err = {{.PackageName}}Utils.AddPlanModifiersToResourceSchema(fields, &schema)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("error adding plan modifiers", err.Error())
|
|
return
|
|
}
|
|
resp.Schema = schema
|
|
}
|
|
|
|
// IdentitySchema defines the identity schema
|
|
func (r *instanceResource) IdentitySchema(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) {
|
|
resp.IdentitySchema = identityschema.Schema{
|
|
Attributes: map[string]identityschema.Attribute{
|
|
"project_id": identityschema.StringAttribute{
|
|
RequiredForImport: true, // must be set during import by the practitioner
|
|
},
|
|
"region": identityschema.StringAttribute{
|
|
RequiredForImport: true, // can be defaulted by the provider configuration
|
|
},
|
|
"instance_id": identityschema.StringAttribute{
|
|
RequiredForImport: true, // can be defaulted by the provider configuration
|
|
},
|
|
// TODO: implement remaining schema parts
|
|
},
|
|
}
|
|
}
|
|
|
|
// Configure adds the provider configured client to the resource.
|
|
func (r *{{.NameCamel}}Resource) Configure(
|
|
ctx context.Context,
|
|
req resource.ConfigureRequest,
|
|
resp *resource.ConfigureResponse,
|
|
) {
|
|
var ok bool
|
|
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
apiClientConfigOptions := []config.ConfigurationOption{
|
|
config.WithCustomAuth(r.providerData.RoundTripper),
|
|
utils.UserAgentConfigOption(r.providerData.Version),
|
|
}
|
|
if r.providerData.{{.PackageNamePascal}}CustomEndpoint != "" {
|
|
apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(r.providerData.{{.PackageName}}CustomEndpoint))
|
|
} else {
|
|
apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(r.providerData.GetRegion()))
|
|
}
|
|
apiClient, err := {{.PackageName}}.NewAPIClient(apiClientConfigOptions...)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError(
|
|
"Error configuring API client",
|
|
fmt.Sprintf(
|
|
"Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration",
|
|
err,
|
|
),
|
|
)
|
|
return
|
|
}
|
|
r.client = apiClient
|
|
tflog.Info(ctx, "{{.PackageName}}.{{.NamePascal}} client configured")
|
|
}
|
|
|
|
// ModifyPlan implements resource.ResourceWithModifyPlan.
|
|
// Use the modifier to set the effective region in the current plan.
|
|
func (r *{{.NameCamel}}Resource) ModifyPlan(
|
|
ctx context.Context,
|
|
req resource.ModifyPlanRequest,
|
|
resp *resource.ModifyPlanResponse,
|
|
) { // nolint:gocritic // function signature required by Terraform
|
|
|
|
// skip initial empty configuration to avoid follow-up errors
|
|
if req.Config.Raw.IsNull() {
|
|
return
|
|
}
|
|
var configModel {{.PackageName}}ResGen.{{.NamePascal}}Model
|
|
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
if req.Plan.Raw.IsNull() {
|
|
return
|
|
}
|
|
var planModel {{.PackageName}}ResGen.{{.NamePascal}}Model
|
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Create creates a new resource
|
|
func (r *{{.NameCamel}}Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
|
var data {{.PackageName}}ResGen.{{.NamePascal}}Model
|
|
|
|
// Read Terraform plan data into the model
|
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
|
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
ctx = core.InitProviderContext(ctx)
|
|
|
|
projectId := data.ProjectId.ValueString()
|
|
region := data.Region.ValueString()
|
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
ctx = tflog.SetField(ctx, "region", region)
|
|
// TODO: add remaining fields
|
|
|
|
// TODO: Create API call logic
|
|
/*
|
|
// Generate API request body from model
|
|
payload, err := toCreatePayload(ctx, &model)
|
|
if err != nil {
|
|
core.LogAndAddError(
|
|
ctx,
|
|
&resp.Diagnostics,
|
|
"Error creating {{.NamePascal}}",
|
|
fmt.Sprintf("Creating API payload: %v", err),
|
|
)
|
|
return
|
|
}
|
|
// Create new {{.NamePascal}}
|
|
createResp, err := r.client.Create{{.NamePascal}}Request(
|
|
ctx,
|
|
projectId,
|
|
region,
|
|
).Create{{.NamePascal}}RequestPayload(*payload).Execute()
|
|
if err != nil {
|
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating {{.NamePascal}}", fmt.Sprintf("Calling API: %v", err))
|
|
return
|
|
}
|
|
|
|
ctx = core.LogResponse(ctx)
|
|
|
|
{{.NamePascal}}Id := *createResp.Id
|
|
*/
|
|
|
|
// Example data value setting
|
|
data.{{.NameCamel | ucfirst}}Id = types.StringValue("id-from-response")
|
|
|
|
// TODO: Set data returned by API in identity
|
|
identity := {{.NamePascal}}ResourceIdentityModel{
|
|
ProjectID: types.StringValue(projectId),
|
|
Region: types.StringValue(region),
|
|
// TODO: add missing values
|
|
{{.NamePascal}}ID: types.StringValue({{.NamePascal}}Id),
|
|
}
|
|
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// TODO: implement wait handler if needed
|
|
/*
|
|
|
|
waitResp, err := wait.Create{{.NamePascal}}WaitHandler(
|
|
ctx,
|
|
r.client,
|
|
projectId,
|
|
{{.NamePascal}}Id,
|
|
region,
|
|
).SetSleepBeforeWait(
|
|
30 * time.Second,
|
|
).SetTimeout(
|
|
90 * time.Minute,
|
|
).WaitWithContext(ctx)
|
|
if err != nil {
|
|
core.LogAndAddError(
|
|
ctx,
|
|
&resp.Diagnostics,
|
|
"Error creating {{.NamePascal}}",
|
|
fmt.Sprintf("{{.NamePascal}} creation waiting: %v", err),
|
|
)
|
|
return
|
|
}
|
|
|
|
if waitResp.Id == nil {
|
|
core.LogAndAddError(
|
|
ctx,
|
|
&resp.Diagnostics,
|
|
"Error creating {{.NamePascal}}",
|
|
"{{.NamePascal}} creation waiting: returned id is nil",
|
|
)
|
|
return
|
|
}
|
|
|
|
// Map response body to schema
|
|
err = mapResponseToModel(ctx, waitResp, &model, resp.Diagnostics)
|
|
if err != nil {
|
|
core.LogAndAddError(
|
|
ctx,
|
|
&resp.Diagnostics,
|
|
"Error creating {{.NamePascal}}",
|
|
fmt.Sprintf("Processing API payload: %v", err),
|
|
)
|
|
return
|
|
}
|
|
|
|
*/
|
|
|
|
// Save data into Terraform state
|
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
|
|
tflog.Info(ctx, "{{.PackageName}}.{{.NamePascal}} created")
|
|
}
|
|
|
|
func (r *{{.NameCamel}}Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
|
var data {{.PackageName}}ResGen.{{.NamePascal}}Model
|
|
|
|
// Read Terraform prior state data into the model
|
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// Read identity data
|
|
var identityData {{.NamePascal}}ResourceIdentityModel
|
|
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
|
|
ctx = core.InitProviderContext(ctx)
|
|
|
|
projectId := identityData.ProjectID.ValueString()
|
|
region := identityData.Region.ValueString()
|
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
ctx = tflog.SetField(ctx, "region", region)
|
|
|
|
// TODO: Read API call logic
|
|
|
|
// Save updated data into Terraform state
|
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
|
|
// TODO: Set data returned by API in identity
|
|
identity := {{.NamePascal}}ResourceIdentityModel{
|
|
ProjectID: types.StringValue(projectId),
|
|
Region: types.StringValue(region),
|
|
// InstanceID: types.StringValue(instanceId),
|
|
}
|
|
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
tflog.Info(ctx, "{{.PackageName}}.{{.NamePascal}} read")
|
|
}
|
|
|
|
func (r *{{.NameCamel}}Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
|
var data {{.PackageName}}ResGen.{{.NamePascal}}Model
|
|
|
|
// Read Terraform prior state data into the model
|
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
ctx = core.InitProviderContext(ctx)
|
|
|
|
projectId := data.ProjectId.ValueString()
|
|
region := data.Region.ValueString()
|
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
ctx = tflog.SetField(ctx, "region", region)
|
|
|
|
// TODO: Update API call logic
|
|
|
|
// TODO: Set data returned by API in identity
|
|
identity := {{.NamePascal}}ResourceIdentityModel{
|
|
ProjectID: types.StringValue(projectId),
|
|
Region: types.StringValue(region),
|
|
// TODO: add missing values
|
|
{{.NamePascal}}ID: types.StringValue({{.NamePascal}}Id),
|
|
}
|
|
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// Save updated data into Terraform state
|
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
|
|
tflog.Info(ctx, "{{.PackageName}}.{{.NamePascal}} updated")
|
|
}
|
|
|
|
func (r *{{.NameCamel}}Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
|
var data {{.PackageName}}ResGen.{{.NamePascal}}Model
|
|
|
|
// Read Terraform prior state data into the model
|
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// Read identity data
|
|
var identityData {{.NamePascal}}ResourceIdentityModel
|
|
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
|
|
ctx = core.InitProviderContext(ctx)
|
|
|
|
projectId := identityData.ProjectID.ValueString()
|
|
region := identityData.Region.ValueString()
|
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
ctx = tflog.SetField(ctx, "region", region)
|
|
|
|
// TODO: Delete API call logic
|
|
|
|
tflog.Info(ctx, "{{.PackageName}}.{{.NamePascal}} deleted")
|
|
}
|
|
|
|
// ImportState imports a resource into the Terraform state on success.
|
|
// The expected format of the resource import identifier is: project_id,zone_id,record_set_id
|
|
func (r *{{.NameCamel}}Resource) ImportState(
|
|
ctx context.Context,
|
|
req resource.ImportStateRequest,
|
|
resp *resource.ImportStateResponse,
|
|
) {
|
|
idParts := strings.Split(req.ID, core.Separator)
|
|
|
|
// TODO: Import logic
|
|
// TODO: fix len and parts itself
|
|
if len(idParts) < 2 || idParts[0] == "" || idParts[1] == "" {
|
|
core.LogAndAddError(
|
|
ctx, &resp.Diagnostics,
|
|
"Error importing database",
|
|
fmt.Sprintf(
|
|
"Expected import identifier with format [project_id],[region],..., got %q",
|
|
req.ID,
|
|
),
|
|
)
|
|
return
|
|
}
|
|
|
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
|
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...)
|
|
// ... more ...
|
|
|
|
core.LogAndAddWarning(
|
|
ctx,
|
|
&resp.Diagnostics,
|
|
"{{.PackageName | ucfirst}} database imported with empty password",
|
|
"The database password is not imported as it is only available upon creation of a new database. The password field will be empty.",
|
|
)
|
|
tflog.Info(ctx, "{{.PackageName | ucfirst}} {{.NameCamel}} state imported")
|
|
}
|