From cf7a7d154e3495618aa2d1ab98507e3274ae89ac Mon Sep 17 00:00:00 2001 From: Mauritz Uphoff <39736813+h3adex@users.noreply.github.com> Date: Fri, 9 May 2025 08:50:23 +0200 Subject: [PATCH] feat: implement stackit git instances (#791) * feat: implement stackit git instances * review changes --- docs/data-sources/git.md | 38 ++ docs/index.md | 1 + docs/resources/git.md | 38 ++ .../data-sources/stackit_git/data-source.tf | 4 + examples/resources/stackit_git/resource.tf | 4 + go.mod | 1 + go.sum | 2 + stackit/internal/core/core.go | 1 + stackit/internal/services/git/git_acc_test.go | 175 +++++++++ .../services/git/instance/datasource.go | 162 +++++++++ .../services/git/instance/resource.go | 339 ++++++++++++++++++ .../services/git/instance/resource_test.go | 79 ++++ .../services/git/testdata/resource.tf | 8 + .../serviceaccount/account/resource.go | 2 +- stackit/internal/testutil/testutil.go | 18 + stackit/provider.go | 161 ++++----- 16 files changed, 937 insertions(+), 96 deletions(-) create mode 100644 docs/data-sources/git.md create mode 100644 docs/resources/git.md create mode 100644 examples/data-sources/stackit_git/data-source.tf create mode 100644 examples/resources/stackit_git/resource.tf create mode 100644 stackit/internal/services/git/git_acc_test.go create mode 100644 stackit/internal/services/git/instance/datasource.go create mode 100644 stackit/internal/services/git/instance/resource.go create mode 100644 stackit/internal/services/git/instance/resource_test.go create mode 100644 stackit/internal/services/git/testdata/resource.tf diff --git a/docs/data-sources/git.md b/docs/data-sources/git.md new file mode 100644 index 00000000..c4865611 --- /dev/null +++ b/docs/data-sources/git.md @@ -0,0 +1,38 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_git Data Source - stackit" +subcategory: "" +description: |- + Git Instance datasource schema. + ~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. +--- + +# stackit_git (Data Source) + +Git Instance datasource schema. + +~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. + +## Example Usage + +```terraform +data "stackit_git" "git" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `instance_id` (String) ID linked to the git instance. +- `project_id` (String) STACKIT project ID to which the git instance is associated. + +### Read-Only + +- `id` (String) Terraform's internal resource ID, structured as "`project_id`,`instance_id`". +- `name` (String) Unique name linked to the git instance. +- `url` (String) Url linked to the git instance. +- `version` (String) Version linked to the git instance. diff --git a/docs/index.md b/docs/index.md index 0c26c39a..276da809 100644 --- a/docs/index.md +++ b/docs/index.md @@ -159,6 +159,7 @@ Note: AWS specific checks must be skipped as they do not work on STACKIT. For de - `dns_custom_endpoint` (String) Custom endpoint for the DNS service - `enable_beta_resources` (Boolean) Enable beta resources. Default is false. - `experiments` (List of String) Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: [iam] +- `git_custom_endpoint` (String) Custom endpoint for the Git service - `iaas_custom_endpoint` (String) Custom endpoint for the IaaS service - `loadbalancer_custom_endpoint` (String) Custom endpoint for the Load Balancer service - `logme_custom_endpoint` (String) Custom endpoint for the LogMe service diff --git a/docs/resources/git.md b/docs/resources/git.md new file mode 100644 index 00000000..26af2027 --- /dev/null +++ b/docs/resources/git.md @@ -0,0 +1,38 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_git Resource - stackit" +subcategory: "" +description: |- + Git Instance resource schema. + ~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. +--- + +# stackit_git (Resource) + +Git Instance resource schema. + +~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. + +## Example Usage + +```terraform +resource "stackit_git" "git" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "git-example-instance" +} +``` + + +## Schema + +### Required + +- `name` (String) Unique name linked to the git instance. +- `project_id` (String) STACKIT project ID to which the git instance is associated. + +### Read-Only + +- `id` (String) Terraform's internal resource ID, structured as "`project_id`,`instance_id`". +- `instance_id` (String) ID linked to the git instance. +- `url` (String) Url linked to the git instance. +- `version` (String) Version linked to the git instance. diff --git a/examples/data-sources/stackit_git/data-source.tf b/examples/data-sources/stackit_git/data-source.tf new file mode 100644 index 00000000..d6e73d27 --- /dev/null +++ b/examples/data-sources/stackit_git/data-source.tf @@ -0,0 +1,4 @@ +data "stackit_git" "git" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} \ No newline at end of file diff --git a/examples/resources/stackit_git/resource.tf b/examples/resources/stackit_git/resource.tf new file mode 100644 index 00000000..b19c4f83 --- /dev/null +++ b/examples/resources/stackit_git/resource.tf @@ -0,0 +1,4 @@ +resource "stackit_git" "git" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "git-example-instance" +} \ No newline at end of file diff --git a/go.mod b/go.mod index cb12d89b..519898d3 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/core v0.17.1 github.com/stackitcloud/stackit-sdk-go/services/cdn v1.0.1 github.com/stackitcloud/stackit-sdk-go/services/dns v0.13.2 + github.com/stackitcloud/stackit-sdk-go/services/git v0.3.2 github.com/stackitcloud/stackit-sdk-go/services/iaas v0.22.1 github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.0.2 github.com/stackitcloud/stackit-sdk-go/services/logme v0.22.1 diff --git a/go.sum b/go.sum index 2d6c99dc..e968f36c 100644 --- a/go.sum +++ b/go.sum @@ -158,6 +158,8 @@ github.com/stackitcloud/stackit-sdk-go/services/cdn v1.0.1 h1:fWCWCefwgPTJ0BIbFQ github.com/stackitcloud/stackit-sdk-go/services/cdn v1.0.1/go.mod h1:Gd+M/UZR0rIaHRXhJzkb0r7kl/nDRmpcmqnOPN5fCSQ= github.com/stackitcloud/stackit-sdk-go/services/dns v0.13.2 h1:6rb3EM0yXuMIBd1U6WsJoMzEiVaHC3WQFWFvT23OE4Y= github.com/stackitcloud/stackit-sdk-go/services/dns v0.13.2/go.mod h1:PMHoavoIaRZpkI9BA0nsnRjGoHASVSBon45XB3QyhMA= +github.com/stackitcloud/stackit-sdk-go/services/git v0.3.2 h1:AguA1s9FuwLmSyvGNv4I/O9u7c/x7cMwyCsrM6Tm2SU= +github.com/stackitcloud/stackit-sdk-go/services/git v0.3.2/go.mod h1:XhXHJpOVC9Rpwyf1G+EpMbprBafH9aZb8vWBdR+z0WM= github.com/stackitcloud/stackit-sdk-go/services/iaas v0.22.1 h1:JXcLcbVesTtwVVb+jJjU3o0FmSpXBRnOw6PVETaeK+E= github.com/stackitcloud/stackit-sdk-go/services/iaas v0.22.1/go.mod h1:QNH50Pq0Hu21lLDOwa02PIjRjTl0LfEdHoz5snGQRn8= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.0.2 h1:5rVt3n7kDJvJQxFCtxfx8uZI9PGkvJY9fVJ4yar10Uc= diff --git a/stackit/internal/core/core.go b/stackit/internal/core/core.go index 8b7d62c1..28b79251 100644 --- a/stackit/internal/core/core.go +++ b/stackit/internal/core/core.go @@ -23,6 +23,7 @@ type ProviderData struct { AuthorizationCustomEndpoint string CdnCustomEndpoint string DnsCustomEndpoint string + GitCustomEndpoint string IaaSCustomEndpoint string LoadBalancerCustomEndpoint string LogMeCustomEndpoint string diff --git a/stackit/internal/services/git/git_acc_test.go b/stackit/internal/services/git/git_acc_test.go new file mode 100644 index 00000000..687f4e7b --- /dev/null +++ b/stackit/internal/services/git/git_acc_test.go @@ -0,0 +1,175 @@ +package git + +import ( + "context" + _ "embed" + "fmt" + "maps" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/git" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" +) + +//go:embed testdata/resource.tf +var resourceConfig string + +var name = fmt.Sprintf("git-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) +var nameUpdated = fmt.Sprintf("git-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) + +var testConfigVars = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "name": config.StringVariable(name), +} + +func testConfigVarsUpdated() config.Variables { + tempConfig := make(config.Variables, len(testConfigVars)) + maps.Copy(tempConfig, testConfigVars) + // update git instance to a new name + // should trigger creating a new instance + tempConfig["name"] = config.StringVariable(nameUpdated) + return tempConfig +} + +func TestGitInstance(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckGitInstanceDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigVars, + Config: testutil.GitProviderConfig() + resourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_git.git", "project_id", testutil.ConvertConfigVariable(testConfigVars["project_id"])), + resource.TestCheckResourceAttr("stackit_git.git", "name", testutil.ConvertConfigVariable(testConfigVars["name"])), + resource.TestCheckResourceAttrSet("stackit_git.git", "url"), + resource.TestCheckResourceAttrSet("stackit_git.git", "version"), + resource.TestCheckResourceAttrSet("stackit_git.git", "instance_id"), + ), + }, + // Data source + { + ConfigVariables: testConfigVars, + Config: fmt.Sprintf(` + %s + + data "stackit_git" "git" { + project_id = stackit_git.git.project_id + instance_id = stackit_git.git.instance_id + } + `, testutil.GitProviderConfig()+resourceConfig, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Instance + resource.TestCheckResourceAttr("data.stackit_git.git", "project_id", testutil.ConvertConfigVariable(testConfigVars["project_id"])), + resource.TestCheckResourceAttrPair( + "stackit_git.git", "project_id", + "data.stackit_git.git", "project_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_git.git", "instance_id", + "data.stackit_git.git", "instance_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_git.git", "name", + "data.stackit_git.git", "name", + ), + resource.TestCheckResourceAttrPair( + "stackit_git.git", "url", + "data.stackit_git.git", "url", + ), + resource.TestCheckResourceAttrPair( + "stackit_git.git", "version", + "data.stackit_git.git", "version", + ), + ), + }, + // Import + { + ConfigVariables: testConfigVars, + ResourceName: "stackit_git.git", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_git.git"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_git.git") + } + instanceId, ok := r.Primary.Attributes["instance_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute instance_id") + } + return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testConfigVarsUpdated(), + Config: testutil.GitProviderConfig() + resourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_git.git", "project_id", testutil.ConvertConfigVariable(testConfigVars["project_id"])), + resource.TestCheckResourceAttr("stackit_git.git", "name", testutil.ConvertConfigVariable(testConfigVarsUpdated()["name"])), + resource.TestCheckResourceAttrSet("stackit_git.git", "url"), + resource.TestCheckResourceAttrSet("stackit_git.git", "version"), + resource.TestCheckResourceAttrSet("stackit_git.git", "instance_id"), + ), + }, + // Deletion is done by the framework implicitly + }, + }) +} + +func testAccCheckGitInstanceDestroy(s *terraform.State) error { + ctx := context.Background() + var client *git.APIClient + var err error + + if testutil.GitCustomEndpoint == "" { + client, err = git.NewAPIClient() + } else { + client, err = git.NewAPIClient( + stackitSdkConfig.WithEndpoint(testutil.GitCustomEndpoint), + ) + } + + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + var instancesToDestroy []string + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_git" { + continue + } + instanceId := strings.Split(rs.Primary.ID, core.Separator)[1] + instancesToDestroy = append(instancesToDestroy, instanceId) + } + + instancesResp, err := client.ListInstances(ctx, testutil.ProjectId).Execute() + if err != nil { + return fmt.Errorf("getting git instances: %w", err) + } + + gitInstances := *instancesResp.Instances + for i := range gitInstances { + if gitInstances[i].Id == nil { + continue + } + if utils.Contains(instancesToDestroy, *gitInstances[i].Id) { + err := client.DeleteInstance(ctx, testutil.ProjectId, *gitInstances[i].Id).Execute() + if err != nil { + return fmt.Errorf("destroying git instance %s during CheckDestroy: %w", *gitInstances[i].Id, err) + } + } + } + return nil +} diff --git a/stackit/internal/services/git/instance/datasource.go b/stackit/internal/services/git/instance/datasource.go new file mode 100644 index 00000000..b9fb5aed --- /dev/null +++ b/stackit/internal/services/git/instance/datasource.go @@ -0,0 +1,162 @@ +package instance + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/git" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &gitDataSource{} +) + +// NewGitDataSource creates a new instance of the gitDataSource. +func NewGitDataSource() datasource.DataSource { + return &gitDataSource{} +} + +// gitDataSource is the datasource implementation. +type gitDataSource struct { + client *git.APIClient +} + +// Configure sets up the API client for the git instance resource. +func (g *gitDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent potential panics if the provider is not properly configured. + if req.ProviderData == nil { + return + } + + // Validate provider data type before proceeding. + 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 + } + + features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_git", "datasource") + if resp.Diagnostics.HasError() { + return + } + + // Initialize the API client with the appropriate authentication and endpoint settings. + var apiClient *git.APIClient + var err error + if providerData.GitCustomEndpoint != "" { + apiClient, err = git.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + config.WithEndpoint(providerData.GitCustomEndpoint), + ) + } else { + apiClient, err = git.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + ) + } + + // Handle API client initialization errors. + 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 + } + + // Store the initialized client. + g.client = apiClient + tflog.Info(ctx, "git client configured") +} + +// Metadata provides metadata for the git datasource. +func (g *gitDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_git" +} + +// Schema defines the schema for the git data source. +func (g *gitDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: features.AddBetaDescription("Git Instance datasource schema."), + Description: "Git Instance datasource schema.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "instance_id": schema.StringAttribute{ + Description: descriptions["instance_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Computed: true, + }, + "url": schema.StringAttribute{ + Description: descriptions["url"], + Computed: true, + }, + "version": schema.StringAttribute{ + Description: descriptions["version"], + Computed: true, + }, + }, + } +} + +func (g *gitDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Extract the project ID and instance id of the model + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + + // Read the current git instance via id + gitInstanceResp, err := g.client.GetInstance(ctx, projectId, instanceId).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + if ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading git instance", fmt.Sprintf("Calling API: %v", err)) + return + } + + err = mapFields(gitInstanceResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading git instance", fmt.Sprintf("Processing API response: %v", err)) + return + } + + // Set the updated state. + diags = resp.State.Set(ctx, &model) + resp.Diagnostics.Append(diags...) + tflog.Info(ctx, fmt.Sprintf("read git instance %s", instanceId)) +} diff --git a/stackit/internal/services/git/instance/resource.go b/stackit/internal/services/git/instance/resource.go new file mode 100644 index 00000000..4a48936e --- /dev/null +++ b/stackit/internal/services/git/instance/resource.go @@ -0,0 +1,339 @@ +package instance + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/git" + "github.com/stackitcloud/stackit-sdk-go/services/git/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &gitResource{} + _ resource.ResourceWithConfigure = &gitResource{} + _ resource.ResourceWithImportState = &gitResource{} +) + +// Model represents the schema for the git resource. +type Model struct { + Id types.String `tfsdk:"id"` // Required by Terraform + ProjectId types.String `tfsdk:"project_id"` // ProjectId associated with the git instance + InstanceId types.String `tfsdk:"instance_id"` // InstanceId associated with the git instance + Name types.String `tfsdk:"name"` // Name linked to the git instance + Url types.String `tfsdk:"url"` // Url linked to the git instance + Version types.String `tfsdk:"version"` // Version linked to the git instance +} + +// NewGitResource is a helper function to create a new git resource instance. +func NewGitResource() resource.Resource { + return &gitResource{} +} + +// gitResource implements the resource interface for git instances. +type gitResource struct { + client *git.APIClient +} + +// descriptions for the attributes in the Schema +var descriptions = map[string]string{ + "id": "Terraform's internal resource ID, structured as \"`project_id`,`instance_id`\".", + "project_id": "STACKIT project ID to which the git instance is associated.", + "instance_id": "ID linked to the git instance.", + "name": "Unique name linked to the git instance.", + "url": "Url linked to the git instance.", + "version": "Version linked to the git instance.", +} + +// Configure sets up the API client for the git instance resource. +func (g *gitResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent potential panics if the provider is not properly configured. + if req.ProviderData == nil { + return + } + + // Validate provider data type before proceeding. + 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 + } + + features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_git", "resource") + if resp.Diagnostics.HasError() { + return + } + + // Initialize the API client with the appropriate authentication and endpoint settings. + var apiClient *git.APIClient + var err error + if providerData.GitCustomEndpoint != "" { + apiClient, err = git.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + config.WithEndpoint(providerData.GitCustomEndpoint), + ) + } else { + apiClient, err = git.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + ) + } + + // Handle API client initialization errors. + 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 + } + + // Store the initialized client. + g.client = apiClient + tflog.Info(ctx, "git client configured") +} + +// Metadata sets the resource type name for the git instance resource. +func (g *gitResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_git" +} + +// Schema defines the schema for the resource. +func (g *gitResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: features.AddBetaDescription("Git Instance resource schema."), + Description: "Git Instance resource schema.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "instance_id": schema.StringAttribute{ + Description: descriptions["instance_id"], + Computed: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthBetween(5, 32), + }, + }, + "url": schema.StringAttribute{ + Description: descriptions["url"], + Computed: true, + }, + "version": schema.StringAttribute{ + Description: descriptions["version"], + Computed: true, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state for the git instance. +func (g *gitResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve the planned values for the resource. + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Set logging context with the project ID and instance ID. + projectId := model.ProjectId.ValueString() + instanceName := model.Name.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_name", instanceName) + + // Create the new git instance via the API client. + gitInstanceResp, err := g.client.CreateInstance(ctx, projectId). + CreateInstancePayload(git.CreateInstancePayload{Name: &instanceName}). + Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating git instance", fmt.Sprintf("Calling API: %v", err)) + return + } + + gitInstanceId := *gitInstanceResp.Id + _, err = wait.CreateGitInstanceWaitHandler(ctx, g.client, projectId, gitInstanceId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating git instance", fmt.Sprintf("Git instance creation waiting: %v", err)) + return + } + + err = mapFields(gitInstanceResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating git instance", fmt.Sprintf("Mapping fields: %v", err)) + return + } + + // Set the state with fully populated data. + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Git Instance created") +} + +// Read refreshes the Terraform state with the latest git instance data. +func (g *gitResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve the current state of the resource. + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Extract the project ID and instance id of the model + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + + // Read the current git instance via id + gitInstanceResp, err := g.client.GetInstance(ctx, projectId, instanceId).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + if ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading git instance", fmt.Sprintf("Calling API: %v", err)) + return + } + + err = mapFields(gitInstanceResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading git instance", fmt.Sprintf("Processing API response: %v", err)) + return + } + + // Set the updated state. + diags = resp.State.Set(ctx, &model) + resp.Diagnostics.Append(diags...) + tflog.Info(ctx, fmt.Sprintf("read git instance %s", instanceId)) +} + +// Update attempts to update the resource. In this case, git instances cannot be updated. +// Note: This method is intentionally left without update logic because changes +// to 'project_id' or 'name' require the resource to be entirely replaced. +// As a result, the Update function is redundant since any modifications will +// automatically trigger a resource recreation through Terraform's built-in +// lifecycle management. +func (g *gitResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + // git instances cannot be updated, so we log an error. + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating git instance", "Git Instance can't be updated") +} + +// Delete deletes the git instance and removes it from the Terraform state on success. +func (g *gitResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve current state of the resource. + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + + // Call API to delete the existing git instance. + err := g.client.DeleteInstance(ctx, projectId, instanceId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting git instance", fmt.Sprintf("Calling API: %v", err)) + return + } + + _, err = wait.DeleteGitInstanceWaitHandler(ctx, g.client, projectId, instanceId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error waiting for instance deletion", fmt.Sprintf("Instance deletion waiting: %v", err)) + return + } + + tflog.Info(ctx, "Git instance deleted") +} + +// ImportState imports a resource into the Terraform state on success. +// The expected format of the resource import identifier is: project_id,instance_id +func (g *gitResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Split the import identifier to extract project ID and email. + idParts := strings.Split(req.ID, core.Separator) + + // Ensure the import identifier format is correct. + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing git instance", + fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), + ) + return + } + + projectId := idParts[0] + instanceId := idParts[1] + + // Set the project ID and instance ID attributes in the state. + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), instanceId)...) + tflog.Info(ctx, "Git instance state imported") +} + +// mapFields maps a Git response to the model. +func mapFields(resp *git.Instance, model *Model) error { + if resp == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + if resp.Id == nil { + return fmt.Errorf("git instance id not present") + } + + // Build the ID by combining the project ID and instance id and assign the model's fields. + idParts := []string{model.ProjectId.ValueString(), *resp.Id} + model.Id = types.StringValue(strings.Join(idParts, core.Separator)) + model.Url = types.StringPointerValue(resp.Url) + model.Name = types.StringPointerValue(resp.Name) + model.InstanceId = types.StringPointerValue(resp.Id) + model.Version = types.StringPointerValue(resp.Version) + + return nil +} diff --git a/stackit/internal/services/git/instance/resource_test.go b/stackit/internal/services/git/instance/resource_test.go new file mode 100644 index 00000000..ff89c5b3 --- /dev/null +++ b/stackit/internal/services/git/instance/resource_test.go @@ -0,0 +1,79 @@ +package instance + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/git" +) + +func TestMapFields(t *testing.T) { + tests := []struct { + description string + input *git.Instance + expected Model + isValid bool + }{ + { + "default_values", + &git.Instance{ + Created: nil, + Id: utils.Ptr("id"), + Name: utils.Ptr("foo"), + Url: utils.Ptr("https://foo.com"), + Version: utils.Ptr("v0.0.1"), + }, + Model{ + Id: types.StringValue("pid,id"), + ProjectId: types.StringValue("pid"), + InstanceId: types.StringValue("id"), + Name: types.StringValue("foo"), + Url: types.StringValue("https://foo.com"), + Version: types.StringValue("v0.0.1"), + }, + true, + }, + { + "nil_response", + nil, + Model{}, + false, + }, + { + "nil_response_2", + &git.Instance{}, + Model{}, + false, + }, + { + "no_id", + &git.Instance{ + Name: utils.Ptr("foo"), + }, + Model{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + state := &Model{ + ProjectId: tt.expected.ProjectId, + } + err := mapFields(tt.input, state) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(state, &tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/git/testdata/resource.tf b/stackit/internal/services/git/testdata/resource.tf new file mode 100644 index 00000000..29929022 --- /dev/null +++ b/stackit/internal/services/git/testdata/resource.tf @@ -0,0 +1,8 @@ + +variable "project_id" {} +variable "name" {} + +resource "stackit_git" "git" { + project_id = var.project_id + name = var.name +} diff --git a/stackit/internal/services/serviceaccount/account/resource.go b/stackit/internal/services/serviceaccount/account/resource.go index 138de1a2..be13bfaf 100644 --- a/stackit/internal/services/serviceaccount/account/resource.go +++ b/stackit/internal/services/serviceaccount/account/resource.go @@ -173,7 +173,7 @@ func (r *serviceAccountResource) Create(ctx context.Context, req resource.Create model.Name = types.StringValue(serviceAccountName) err = mapFields(serviceAccountResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating service account", fmt.Sprintf("Processing API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating service account", fmt.Sprintf("Processing API response: %v", err)) return } diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index 7131c0eb..220a79b8 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -53,6 +53,7 @@ var ( ArgusCustomEndpoint = os.Getenv("TF_ACC_ARGUS_CUSTOM_ENDPOINT") CdnCustomEndpoint = os.Getenv("TF_ACC_CDN_CUSTOM_ENDPOINT") DnsCustomEndpoint = os.Getenv("TF_ACC_DNS_CUSTOM_ENDPOINT") + GitCustomEndpoint = os.Getenv("TF_ACC_GIT_CUSTOM_ENDPOINT") IaaSCustomEndpoint = os.Getenv("TF_ACC_IAAS_CUSTOM_ENDPOINT") LoadBalancerCustomEndpoint = os.Getenv("TF_ACC_LOADBALANCER_CUSTOM_ENDPOINT") LogMeCustomEndpoint = os.Getenv("TF_ACC_LOGME_CUSTOM_ENDPOINT") @@ -433,6 +434,23 @@ func ServiceAccountProviderConfig() string { ) } +func GitProviderConfig() string { + if GitCustomEndpoint == "" { + return ` + provider "stackit" { + default_region = "eu01" + enable_beta_resources = true + }` + } + return fmt.Sprintf(` + provider "stackit" { + git_custom_endpoint = "%s" + enable_beta_resources = true + }`, + GitCustomEndpoint, + ) +} + func ResourceNameWithDateTime(name string) string { dateTime := time.Now().Format(time.RFC3339) // Remove timezone to have a smaller datetime diff --git a/stackit/provider.go b/stackit/provider.go index e39586f3..e1af0693 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -12,12 +12,17 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + sdkauth "github.com/stackitcloud/stackit-sdk-go/core/auth" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" roleAssignements "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/roleassignments" cdnCustomDomain "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/customdomain" cdn "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/distribution" dnsRecordSet "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/recordset" dnsZone "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/zone" + gitInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/git/instance" iaasAffinityGroup "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/affinitygroup" iaasImage "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/image" iaasKeyPair "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/keypair" @@ -73,11 +78,6 @@ import ( skeKubeconfig "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/kubeconfig" sqlServerFlexInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/instance" sqlServerFlexUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/user" - - sdkauth "github.com/stackitcloud/stackit-sdk-go/core/auth" - "github.com/stackitcloud/stackit-sdk-go/core/config" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" ) // Ensure the implementation satisfies the expected interfaces @@ -118,6 +118,7 @@ type providerModel struct { ArgusCustomEndpoint types.String `tfsdk:"argus_custom_endpoint"` CdnCustomEndpoint types.String `tfsdk:"cdn_custom_endpoint"` DNSCustomEndpoint types.String `tfsdk:"dns_custom_endpoint"` + GitCustomEndpoint types.String `tfsdk:"git_custom_endpoint"` IaaSCustomEndpoint types.String `tfsdk:"iaas_custom_endpoint"` PostgresFlexCustomEndpoint types.String `tfsdk:"postgresflex_custom_endpoint"` MongoDBFlexCustomEndpoint types.String `tfsdk:"mongodbflex_custom_endpoint"` @@ -159,6 +160,7 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro "argus_custom_endpoint": "Custom endpoint for the Argus service", "cdn_custom_endpoint": "Custom endpoint for the CDN service", "dns_custom_endpoint": "Custom endpoint for the DNS service", + "git_custom_endpoint": "Custom endpoint for the Git service", "iaas_custom_endpoint": "Custom endpoint for the IaaS service", "mongodbflex_custom_endpoint": "Custom endpoint for the MongoDB Flex service", "modelserving_custom_endpoint": "Custom endpoint for the AI Model Serving service", @@ -244,6 +246,10 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro Optional: true, Description: descriptions["dns_custom_endpoint"], }, + "git_custom_endpoint": schema.StringAttribute{ + Optional: true, + Description: descriptions["git_custom_endpoint"], + }, "iaas_custom_endpoint": schema.StringAttribute{ Optional: true, Description: descriptions["iaas_custom_endpoint"], @@ -358,98 +364,61 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, // Configure SDK client sdkConfig := &config.Configuration{} var providerData core.ProviderData - if !(providerConfig.CredentialsFilePath.IsUnknown() || providerConfig.CredentialsFilePath.IsNull()) { - sdkConfig.CredentialsFilePath = providerConfig.CredentialsFilePath.ValueString() + + // Helper function to set a string field if it's known and not null + setStringField := func(v basetypes.StringValue, setter func(string)) { + if !v.IsUnknown() && !v.IsNull() { + setter(v.ValueString()) + } } - if !(providerConfig.ServiceAccountKey.IsUnknown() || providerConfig.ServiceAccountKey.IsNull()) { - sdkConfig.ServiceAccountKey = providerConfig.ServiceAccountKey.ValueString() - } - if !(providerConfig.ServiceAccountKeyPath.IsUnknown() || providerConfig.ServiceAccountKeyPath.IsNull()) { - sdkConfig.ServiceAccountKeyPath = providerConfig.ServiceAccountKeyPath.ValueString() - } - if !(providerConfig.PrivateKey.IsUnknown() || providerConfig.PrivateKey.IsNull()) { - sdkConfig.PrivateKey = providerConfig.PrivateKey.ValueString() - } - if !(providerConfig.PrivateKeyPath.IsUnknown() || providerConfig.PrivateKeyPath.IsNull()) { - sdkConfig.PrivateKeyPath = providerConfig.PrivateKeyPath.ValueString() - } - if !(providerConfig.Token.IsUnknown() || providerConfig.Token.IsNull()) { - sdkConfig.Token = providerConfig.Token.ValueString() - } - if !(providerConfig.DefaultRegion.IsUnknown() || providerConfig.DefaultRegion.IsNull()) { - providerData.DefaultRegion = providerConfig.DefaultRegion.ValueString() - } else if !(providerConfig.Region.IsUnknown() || providerConfig.Region.IsNull()) { // nolint:staticcheck // preliminary handling of deprecated attribute - providerData.Region = providerConfig.Region.ValueString() // nolint:staticcheck // preliminary handling of deprecated attribute - } - if !(providerConfig.CdnCustomEndpoint.IsUnknown() || providerConfig.CdnCustomEndpoint.IsNull()) { - providerData.CdnCustomEndpoint = providerConfig.CdnCustomEndpoint.ValueString() - } - if !(providerConfig.DNSCustomEndpoint.IsUnknown() || providerConfig.DNSCustomEndpoint.IsNull()) { - providerData.DnsCustomEndpoint = providerConfig.DNSCustomEndpoint.ValueString() - } - if !(providerConfig.IaaSCustomEndpoint.IsUnknown() || providerConfig.IaaSCustomEndpoint.IsNull()) { - providerData.IaaSCustomEndpoint = providerConfig.IaaSCustomEndpoint.ValueString() - } - if !(providerConfig.PostgresFlexCustomEndpoint.IsUnknown() || providerConfig.PostgresFlexCustomEndpoint.IsNull()) { - providerData.PostgresFlexCustomEndpoint = providerConfig.PostgresFlexCustomEndpoint.ValueString() - } - if !(providerConfig.ModelServingCustomEndpoint.IsUnknown() || providerConfig.ModelServingCustomEndpoint.IsNull()) { - providerData.ModelServingCustomEndpoint = providerConfig.ModelServingCustomEndpoint.ValueString() - } - if !(providerConfig.MongoDBFlexCustomEndpoint.IsUnknown() || providerConfig.MongoDBFlexCustomEndpoint.IsNull()) { - providerData.MongoDBFlexCustomEndpoint = providerConfig.MongoDBFlexCustomEndpoint.ValueString() - } - if !(providerConfig.LoadBalancerCustomEndpoint.IsUnknown() || providerConfig.LoadBalancerCustomEndpoint.IsNull()) { - providerData.LoadBalancerCustomEndpoint = providerConfig.LoadBalancerCustomEndpoint.ValueString() - } - if !(providerConfig.LogMeCustomEndpoint.IsUnknown() || providerConfig.LogMeCustomEndpoint.IsNull()) { - providerData.LogMeCustomEndpoint = providerConfig.LogMeCustomEndpoint.ValueString() - } - if !(providerConfig.RabbitMQCustomEndpoint.IsUnknown() || providerConfig.RabbitMQCustomEndpoint.IsNull()) { - providerData.RabbitMQCustomEndpoint = providerConfig.RabbitMQCustomEndpoint.ValueString() - } - if !(providerConfig.MariaDBCustomEndpoint.IsUnknown() || providerConfig.MariaDBCustomEndpoint.IsNull()) { - providerData.MariaDBCustomEndpoint = providerConfig.MariaDBCustomEndpoint.ValueString() - } - if !(providerConfig.AuthorizationCustomEndpoint.IsUnknown() || providerConfig.AuthorizationCustomEndpoint.IsNull()) { - providerData.AuthorizationCustomEndpoint = providerConfig.AuthorizationCustomEndpoint.ValueString() - } - if !(providerConfig.ObjectStorageCustomEndpoint.IsUnknown() || providerConfig.ObjectStorageCustomEndpoint.IsNull()) { - providerData.ObjectStorageCustomEndpoint = providerConfig.ObjectStorageCustomEndpoint.ValueString() - } - if !(providerConfig.ObservabilityCustomEndpoint.IsUnknown() || providerConfig.ObservabilityCustomEndpoint.IsNull()) { - providerData.ObservabilityCustomEndpoint = providerConfig.ObservabilityCustomEndpoint.ValueString() - } - if !(providerConfig.OpenSearchCustomEndpoint.IsUnknown() || providerConfig.OpenSearchCustomEndpoint.IsNull()) { - providerData.OpenSearchCustomEndpoint = providerConfig.OpenSearchCustomEndpoint.ValueString() - } - if !(providerConfig.RedisCustomEndpoint.IsUnknown() || providerConfig.RedisCustomEndpoint.IsNull()) { - providerData.RedisCustomEndpoint = providerConfig.RedisCustomEndpoint.ValueString() - } - if !(providerConfig.ResourceManagerCustomEndpoint.IsUnknown() || providerConfig.ResourceManagerCustomEndpoint.IsNull()) { - providerData.ResourceManagerCustomEndpoint = providerConfig.ResourceManagerCustomEndpoint.ValueString() - } - if !(providerConfig.SecretsManagerCustomEndpoint.IsUnknown() || providerConfig.SecretsManagerCustomEndpoint.IsNull()) { - providerData.SecretsManagerCustomEndpoint = providerConfig.SecretsManagerCustomEndpoint.ValueString() - } - if !(providerConfig.SQLServerFlexCustomEndpoint.IsUnknown() || providerConfig.SQLServerFlexCustomEndpoint.IsNull()) { - providerData.SQLServerFlexCustomEndpoint = providerConfig.SQLServerFlexCustomEndpoint.ValueString() - } - if !(providerConfig.ServiceAccountCustomEndpoint.IsUnknown() || providerConfig.ServiceAccountCustomEndpoint.IsNull()) { - providerData.ServiceAccountCustomEndpoint = providerConfig.ServiceAccountCustomEndpoint.ValueString() - } - if !(providerConfig.SKECustomEndpoint.IsUnknown() || providerConfig.SKECustomEndpoint.IsNull()) { - providerData.SKECustomEndpoint = providerConfig.SKECustomEndpoint.ValueString() - } - if !(providerConfig.ServiceEnablementCustomEndpoint.IsUnknown() || providerConfig.ServiceEnablementCustomEndpoint.IsNull()) { - providerData.ServiceEnablementCustomEndpoint = providerConfig.ServiceEnablementCustomEndpoint.ValueString() - } - if !(providerConfig.TokenCustomEndpoint.IsUnknown() || providerConfig.TokenCustomEndpoint.IsNull()) { - sdkConfig.TokenCustomUrl = providerConfig.TokenCustomEndpoint.ValueString() - } - if !(providerConfig.EnableBetaResources.IsUnknown() || providerConfig.EnableBetaResources.IsNull()) { - providerData.EnableBetaResources = providerConfig.EnableBetaResources.ValueBool() + + // Helper function to set a boolean field if it's known and not null + setBoolField := func(v basetypes.BoolValuable, setter func(bool)) { + if !v.IsUnknown() && !v.IsNull() { + val, err := v.ToBoolValue(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring provider", fmt.Sprintf("Setting up bool value: %v", diags.Errors())) + } + setter(val.ValueBool()) + } } + + // Configure SDK client + setStringField(providerConfig.CredentialsFilePath, func(v string) { sdkConfig.CredentialsFilePath = v }) + setStringField(providerConfig.ServiceAccountKey, func(v string) { sdkConfig.ServiceAccountKey = v }) + setStringField(providerConfig.ServiceAccountKeyPath, func(v string) { sdkConfig.ServiceAccountKeyPath = v }) + setStringField(providerConfig.PrivateKey, func(v string) { sdkConfig.PrivateKey = v }) + setStringField(providerConfig.PrivateKeyPath, func(v string) { sdkConfig.PrivateKeyPath = v }) + setStringField(providerConfig.Token, func(v string) { sdkConfig.Token = v }) + setStringField(providerConfig.TokenCustomEndpoint, func(v string) { sdkConfig.TokenCustomUrl = v }) + + // Provider Data Configuration + setStringField(providerConfig.DefaultRegion, func(v string) { providerData.DefaultRegion = v }) + setStringField(providerConfig.Region, func(v string) { providerData.Region = v }) // nolint:staticcheck // preliminary handling of deprecated attribute + setStringField(providerConfig.CdnCustomEndpoint, func(v string) { providerData.CdnCustomEndpoint = v }) + setStringField(providerConfig.DNSCustomEndpoint, func(v string) { providerData.DnsCustomEndpoint = v }) + setStringField(providerConfig.GitCustomEndpoint, func(v string) { providerData.GitCustomEndpoint = v }) + setStringField(providerConfig.IaaSCustomEndpoint, func(v string) { providerData.IaaSCustomEndpoint = v }) + setStringField(providerConfig.PostgresFlexCustomEndpoint, func(v string) { providerData.PostgresFlexCustomEndpoint = v }) + setStringField(providerConfig.ModelServingCustomEndpoint, func(v string) { providerData.ModelServingCustomEndpoint = v }) + setStringField(providerConfig.MongoDBFlexCustomEndpoint, func(v string) { providerData.MongoDBFlexCustomEndpoint = v }) + setStringField(providerConfig.LoadBalancerCustomEndpoint, func(v string) { providerData.LoadBalancerCustomEndpoint = v }) + setStringField(providerConfig.LogMeCustomEndpoint, func(v string) { providerData.LogMeCustomEndpoint = v }) + setStringField(providerConfig.RabbitMQCustomEndpoint, func(v string) { providerData.RabbitMQCustomEndpoint = v }) + setStringField(providerConfig.MariaDBCustomEndpoint, func(v string) { providerData.MariaDBCustomEndpoint = v }) + setStringField(providerConfig.AuthorizationCustomEndpoint, func(v string) { providerData.AuthorizationCustomEndpoint = v }) + setStringField(providerConfig.ObjectStorageCustomEndpoint, func(v string) { providerData.ObjectStorageCustomEndpoint = v }) + setStringField(providerConfig.ObservabilityCustomEndpoint, func(v string) { providerData.ObservabilityCustomEndpoint = v }) + setStringField(providerConfig.OpenSearchCustomEndpoint, func(v string) { providerData.OpenSearchCustomEndpoint = v }) + setStringField(providerConfig.RedisCustomEndpoint, func(v string) { providerData.RedisCustomEndpoint = v }) + setStringField(providerConfig.ResourceManagerCustomEndpoint, func(v string) { providerData.ResourceManagerCustomEndpoint = v }) + setStringField(providerConfig.SecretsManagerCustomEndpoint, func(v string) { providerData.SecretsManagerCustomEndpoint = v }) + setStringField(providerConfig.SQLServerFlexCustomEndpoint, func(v string) { providerData.SQLServerFlexCustomEndpoint = v }) + setStringField(providerConfig.ServiceAccountCustomEndpoint, func(v string) { providerData.ServiceAccountCustomEndpoint = v }) + setStringField(providerConfig.SKECustomEndpoint, func(v string) { providerData.SKECustomEndpoint = v }) + setStringField(providerConfig.ServiceEnablementCustomEndpoint, func(v string) { providerData.ServiceEnablementCustomEndpoint = v }) + setBoolField(providerConfig.EnableBetaResources, func(v bool) { providerData.EnableBetaResources = v }) + if !(providerConfig.Experiments.IsUnknown() || providerConfig.Experiments.IsNull()) { var experimentValues []string diags := providerConfig.Experiments.ElementsAs(ctx, &experimentValues, false) @@ -480,6 +449,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource cdnCustomDomain.NewCustomDomainDataSource, dnsZone.NewZoneDataSource, dnsRecordSet.NewRecordSetDataSource, + gitInstance.NewGitDataSource, iaasAffinityGroup.NewAffinityGroupDatasource, iaasImage.NewImageDataSource, iaasNetwork.NewNetworkDataSource, @@ -537,6 +507,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { cdnCustomDomain.NewCustomDomainResource, dnsZone.NewZoneResource, dnsRecordSet.NewRecordSetResource, + gitInstance.NewGitResource, iaasAffinityGroup.NewAffinityGroupResource, iaasImage.NewImageResource, iaasNetwork.NewNetworkResource,