diff --git a/.github/docs/contribution-guide/resource.go b/.github/docs/contribution-guide/resource.go new file mode 100644 index 00000000..eb01689e --- /dev/null +++ b/.github/docs/contribution-guide/resource.go @@ -0,0 +1,295 @@ +package foo + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + fooUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/foo/utils" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + + "github.com/stackitcloud/stackit-sdk-go/services/foo" // Import service "foo" from the STACKIT SDK for Go + // (...) +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &barResource{} + _ resource.ResourceWithConfigure = &barResource{} + _ resource.ResourceWithImportState = &barResource{} +) + +// Provider's internal model +type Model struct { + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + BarId types.String `tfsdk:"bar_id"` + MyRequiredField types.String `tfsdk:"my_required_field"` + MyOptionalField types.String `tfsdk:"my_optional_field"` + MyReadOnlyField types.String `tfsdk:"my_read_only_field"` +} + +// NewBarResource is a helper function to simplify the provider implementation. +func NewBarResource() resource.Resource { + return &barResource{} +} + +// barResource is the resource implementation. +type barResource struct { + client *foo.APIClient +} + +// Metadata returns the resource type name. +func (r *barResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_foo_bar" +} + +// Configure adds the provider configured client to the resource. +func (r *barResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := fooUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "Foo bar client configured") +} + +// Schema defines the schema for the resource. +func (r *barResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + descriptions := map[string]string{ + "main": "Foo bar resource schema.", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`bar_id`\".", + "project_id": "STACKIT Project ID to which the bar is associated.", + "bar_id": "The bar ID.", + "my_required_field": "My required field description.", + "my_optional_field": "My optional field description.", + "my_read_only_field": "My read-only field description.", + } + + resp.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + // "RequiresReplace" makes the provider recreate the resource when the field is changed in the configuration + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + // Validators can be used to validate the values set to a field + validate.UUID(), + validate.NoSeparator(), + }, + }, + "bar_id": schema.StringAttribute{ + Description: descriptions["bar_id"], + Computed: true, + }, + "my_required_field": schema.StringAttribute{ + Description: descriptions["my_required_field"], + Required: true, + PlanModifiers: []planmodifier.String{ + // "RequiresReplace" makes the provider recreate the resource when the field is changed in the configuration + stringplanmodifier.RequiresReplace(), + }, + }, + "my_optional_field": schema.StringAttribute{ + Description: descriptions["my_optional_field"], + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + // "UseStateForUnknown" can be used to copy a prior state value into the planned value. It should be used when it is known that an unconfigured value will remain the same after a resource update + stringplanmodifier.UseStateForUnknown(), + }, + }, + "my_read_only_field": schema.StringAttribute{ + Description: descriptions["my_read_only_field"], + Computed: true, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *barResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + projectId := model.ProjectId.ValueString() + barId := model.BarId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + + // Create new bar + payload, err := toCreatePayload(&model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Creating API payload: %v", err)) + return + } + resp, err := r.client.CreateBar(ctx, projectId, barId).CreateBarPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bar", fmt.Sprintf("Calling API: %v", err)) + return + } + ctx = tflog.SetField(ctx, "bar_id", resp.BarId) + + // Map response body to schema + err = mapFields(resp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bar", fmt.Sprintf("Processing API payload: %v", err)) + return + } + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Foo bar created") +} + +// Read refreshes the Terraform state with the latest data. +func (r *barResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + projectId := model.ProjectId.ValueString() + barId := model.BarId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "bar_id", barId) + + barResp, err := r.client.GetBar(ctx, projectId, barId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading bar", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Map response body to schema + err = mapFields(barResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading bar", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Foo bar read") +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *barResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + // Similar to Create method, calls r.client.UpdateBar instead +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *barResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + projectId := model.ProjectId.ValueString() + barId := model.BarId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "bar_id", barId) + + // Delete existing bar + _, err := r.client.DeleteBar(ctx, projectId, barId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting bar", fmt.Sprintf("Calling API: %v", err)) + } + + tflog.Info(ctx, "Foo bar deleted") +} + +// ImportState imports a resource into the Terraform state on success. +// The expected format of the bar resource import identifier is: project_id,bar_id +func (r *barResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing bar", + fmt.Sprintf("Expected import identifier with format [project_id],[bar_id], 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("bar_id"), idParts[1])...) + tflog.Info(ctx, "Foo bar state imported") +} + +// Maps bar fields to the provider's internal model +func mapFields(barResp *foo.GetBarResponse, model *Model) error { + if barResp == nil { + return fmt.Errorf("response input is nil") + } + if barResp.Bar == nil { + return fmt.Errorf("response bar is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + bar := barResp.Bar + model.BarId = types.StringPointerValue(bar.BarId) + + idParts := []string{ + model.ProjectId.ValueString(), + model.BarId.ValueString(), + } + model.Id = types.StringValue( + strings.Join(idParts, core.Separator), + ) + + model.MyRequiredField = types.StringPointerValue(bar.MyRequiredField) + model.MyOptionalField = types.StringPointerValue(bar.MyOptionalField) + model.MyReadOnlyField = types.StringPointerValue(bar.MyOtherField) + return nil +} + +// Build CreateBarPayload from provider's model +func toCreatePayload(model *Model) (*foo.CreateBarPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + myRequiredFieldValue := conversion.StringValueToPointer(model.MyRequiredField) + myOptionalFieldValue := conversion.StringValueToPointer(model.MyOptionalField) + return &foo.CreateBarPayload{ + MyRequiredField: myRequiredFieldValue, + MyOptionalField: myOptionalFieldValue, + }, nil +} diff --git a/.github/docs/contribution-guide/utils/util.go b/.github/docs/contribution-guide/utils/util.go new file mode 100644 index 00000000..bc58a48d --- /dev/null +++ b/.github/docs/contribution-guide/utils/util.go @@ -0,0 +1,31 @@ +package utils + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/foo" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *foo.APIClient { + apiClientConfigOptions := []config.ConfigurationOption{ + config.WithCustomAuth(providerData.RoundTripper), + utils.UserAgentConfigOption(providerData.Version), + } + if providerData.FooCustomEndpoint != "" { + apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.FooCustomEndpoint)) + } else { + apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) + } + apiClient, err := foo.NewAPIClient(apiClientConfigOptions...) + if err != nil { + core.LogAndAddError(ctx, diags, "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 nil + } + + return apiClient +} diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index 67ccdbe0..daf6a82a 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -45,7 +45,7 @@ Let's suppose you want to want to implement a new resource `bar` of service `foo 2. Please refer to the [Resource file structure](./CONTRIBUTION.md/#resource-file-structure) section for details on the structure of the file itself 3. To register the new resource `bar` in the provider, add it to the `Resources` in `stackit/provider.go`, using the `NewBarResource` method 4. Add an example in `examples/resources/stackit_foo_bar/resource.tf` with an example configuration for the new resource, e.g.: - ```tf + ```hcl resource "stackit_foo_bar" "example" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" my_required_field = "my-required-field-value" @@ -61,307 +61,7 @@ Additionally, remember to run `make generate-docs` after your changes to keep th Below is a typical structure of a STACKIT Terraform provider resource: -```go -package foo - -import ( - (...) - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/foo" // Import service "foo" from the STACKIT SDK for Go -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &barResource{} - _ resource.ResourceWithConfigure = &barResource{} - _ resource.ResourceWithImportState = &barResource{} -) - -// Provider's internal model -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - BarId types.String `tfsdk:"bar_id"` - MyRequiredField types.String `tfsdk:"my_required_field"` - MyOptionalField types.String `tfsdk:"my_optional_field"` - MyReadOnlyField types.String `tfsdk:"my_read_only_field"` -} - -// NewBarResource is a helper function to simplify the provider implementation. -func NewBarResource() resource.Resource { - return &barResource{} -} - -// barResource is the resource implementation. -type barResource struct { - client *foo.APIClient -} - -// Metadata returns the resource type name. -func (r *barResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_foo_bar" -} - -// Configure adds the provider configured client to the resource. -func (r *barResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - // Prevent panic if the provider has not been configured. - if req.ProviderData == nil { - return - } - - providerData, ok := req.ProviderData.(core.ProviderData) - if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) - return - } - - var apiClient *foo.APIClient - var err error - if providerData.FooCustomEndpoint != "" { - apiClient, err = foo.NewAPIClient( - config.WithCustomAuth(providerData.RoundTripper), - config.WithEndpoint(providerData.FooCustomEndpoint), - ) - } else { - apiClient, err = foo.NewAPIClient( - config.WithCustomAuth(providerData.RoundTripper), - config.WithRegion(providerData.DefaultRegion), - ) - } - - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "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, "Foo bar client configured") -} - -// Schema defines the schema for the resource. -func (r *barResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Foo bar resource schema.", - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`bar_id`\".", - "project_id": "STACKIT Project ID to which the bar is associated.", - "bar_id": "The bar ID.", - "my_required_field": "My required field description.", - "my_optional_field": "My optional field description.", - "my_read_only_field": "My read-only field description.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - // "RequiresReplace" makes the provider recreate the resource when the field is changed in the configuration - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - // Validators can be used to validate the values set to a field - validate.UUID(), - validate.NoSeparator(), - }, - }, - "bar_id": schema.StringAttribute{ - Description: descriptions["bar_id"], - Computed: true, - }, - "my_required_field": schema.StringAttribute{ - Description: descriptions["my_required_field"], - Required: true, - PlanModifiers: []planmodifier.String{ - // "RequiresReplace" makes the provider recreate the resource when the field is changed in the configuration - stringplanmodifier.RequiresReplace(), - }, - }, - "my_optional_field": schema.StringAttribute{ - Description: descriptions["my_optional_field"], - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - // "UseStateForUnknown" can be used to copy a prior state value into the planned value. It should be used when it is known that an unconfigured value will remain the same after a resource update - stringplanmodifier.UseStateForUnknown(), - } - }, - "my_read_only_field": schema.StringAttribute{ - Description: descriptions["my_read_only_field"], - Computed: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *barResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - barId := model.BarId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - - // Create new bar - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Creating API payload: %v", err)) - return - } - resp, err := r.client.CreateBar(ctx, projectId, barId).CreateBarPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bar", fmt.Sprintf("Calling API: %v", err)) - return - } - ctx = tflog.SetField(ctx, "bar_id", resp.BarId) - - // Map response body to schema - err = mapFields(resp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bar", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Foo bar created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *barResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - barId := model.BarId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "bar_id", barId) - - barResp, err := r.client.GetBar(ctx, projectId, barId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading bar", fmt.Sprintf("Calling API: %v", err)) - return - } - - // Map response body to schema - err = mapFields(barResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading bar", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Foo bar read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *barResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Similar to Create method, calls r.client.UpdateBar instead -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *barResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - barId := model.BarId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "bar_id", barId) - - // Delete existing bar - _, err := r.client.DeleteBar(ctx, projectId, barId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting bar", fmt.Sprintf("Calling API: %v", err)) - } - - tflog.Info(ctx, "Foo bar deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the bar resource import identifier is: project_id,bar_id -func (r *barResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing bar", - fmt.Sprintf("Expected import identifier with format [project_id],[bar_id], 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("bar_id"), idParts[1])...) - tflog.Info(ctx, "Foo bar state imported") -} - -// Maps bar fields to the provider's internal model -func mapFields(barResp *foo.GetBarResponse, model *Model) error { - if barResp == nil { - return fmt.Errorf("response input is nil") - } - if barResp.Bar == nil { - return fmt.Errorf("response bar is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - bar := barResp.Bar - model.BarId = types.StringPointerValue(bar.BarId) - - idParts := []string{ - model.ProjectId.ValueString(), - model.BarId.ValueString(), - } - model.Id = types.StringValue( - strings.Join(idParts, core.Separator), - ) - - model.MyRequiredField = types.StringPointerValue(bar.MyRequiredField) - model.MyOptionalField = types.StringPointerValue(bar.MyOptionalField) - model.MyReadOnlyField = types.StringPointerValue(bar.MyOtherField) - return nil -} - -// Build CreateBarPayload from provider's model -func toCreatePayload(model *Model) *foo.CreateBarPayload { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - myRequiredFieldValue := conversion.StringValueToPointer(model.MyRequiredField) - myOptionalFieldValue := conversion.StringValueToPointer(model.MyOptionalField) - return &foo.CreateBarPayload{ - MyRequiredField: myRequiredFieldValue, - MyOptionalField: myOptionalFieldValue, - }, nil -} -``` +https://github.com/stackitcloud/terraform-provider-stackit/blob/1b9225598a007cda8d8bcadf0db1836e96451353/.github/docs/contribution-guide/resource.go#L26-L295 If the new resource `bar` is the first resource in the TFP using a STACKIT service `foo`, please refer to [Onboarding a new STACKIT service](./CONTRIBUTION.md/#onboarding-a-new-stackit-service). @@ -384,6 +84,9 @@ If you want to onboard resources of a STACKIT service `foo` that was not yet in providerData.FooCustomEndpoint = providerConfig.FooCustomEndpoint.ValueString() } ``` +4. Create a utils package, for service `foo` it would be `stackit/internal/foo/utils`. Add a `ConfigureClient()` func and use it in your resource and datasource implementations. + +https://github.com/stackitcloud/terraform-provider-stackit/blob/1b9225598a007cda8d8bcadf0db1836e96451353/.github/docs/contribution-guide/utils/util.go#L14-L31 ### Local development @@ -392,7 +95,7 @@ To test your changes locally, you have to compile the provider (requires Go 1.24 1. Clone the repository. 1. Create a `.terraformrc` config file in your home directory (`~`) for the terraform CLI with the following content: - ``` + ```hcl provider_installation { dev_overrides { "registry.terraform.io/stackitcloud/stackit" = "" @@ -406,7 +109,7 @@ To test your changes locally, you have to compile the provider (requires Go 1.24 ``` 1. Copy one of the folders in the [examples](examples/) folder to a location of your choosing, and define the Terraform variables according to its README. The main.tf file needs some additional configuration to use the local provider: - ``` + ```hcl terraform { required_providers { stackit = { @@ -443,7 +146,7 @@ You'll need a storage bucket to store the Terraform state and a pair of access k In the main.tf file location, initialize Terraform by running the following: -``` +```shell terraform init -reconfigure -upgrade -backend-config="access_key=" -backend-config="secret_key=" ``` diff --git a/README.md b/README.md index c0416f96..799f26e4 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ This project is the official [Terraform Provider](https://registry.terraform.io/ To install the [STACKIT Terraform Provider](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs), copy and paste this code into your Terraform configuration. Then, run `terraform init`. -``` +```hcl terraform { required_providers { stackit = { @@ -124,7 +124,7 @@ To do so, you need an Object Storage [S3 bucket](https://docs.stackit.cloud/stac Once you have everything setup, you can configure the backend by adding the following block to your Terraform configuration: -``` +```hcl terraform { backend "s3" { bucket = "BUCKET_NAME"