diff --git a/docs/data-sources/git.md b/docs/data-sources/git.md index 8cbd29ef..a2be6b18 100644 --- a/docs/data-sources/git.md +++ b/docs/data-sources/git.md @@ -32,6 +32,11 @@ data "stackit_git" "git" { ### Read-Only +- `acl` (List of String) Restricted ACL for instance access. +- `consumed_disk` (String) How many bytes of disk space is consumed. +- `consumed_object_storage` (String) How many bytes of Object Storage is consumed. +- `created` (String) Instance creation timestamp in RFC3339 format. +- `flavor` (String) Instance flavor. If not provided, defaults to git-100. For a list of available flavors, refer to our API documentation: `https://docs.api.stackit.cloud/documentation/git/version/v1beta` - `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. diff --git a/docs/resources/git.md b/docs/resources/git.md index 26af2027..8ed2cb03 100644 --- a/docs/resources/git.md +++ b/docs/resources/git.md @@ -4,14 +4,14 @@ 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. + ~> 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. This resource currently does not support updates. Changing the ACLs, flavor, or name will trigger resource recreation. Update functionality will be added soon. In the meantime, please proceed with caution. To update these attributes, please open a support ticket. --- # 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. +~> 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. This resource currently does not support updates. Changing the ACLs, flavor, or name will trigger resource recreation. Update functionality will be added soon. In the meantime, please proceed with caution. To update these attributes, please open a support ticket. ## Example Usage @@ -20,6 +20,15 @@ resource "stackit_git" "git" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "git-example-instance" } + +resource "stackit_git" "git" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "git-example-instance" + acl = [ + "0.0.0.0/0" + ] + flavor = "git-100" +} ``` @@ -30,8 +39,16 @@ resource "stackit_git" "git" { - `name` (String) Unique name linked to the git instance. - `project_id` (String) STACKIT project ID to which the git instance is associated. +### Optional + +- `acl` (List of String) Restricted ACL for instance access. +- `flavor` (String) Instance flavor. If not provided, defaults to git-100. For a list of available flavors, refer to our API documentation: `https://docs.api.stackit.cloud/documentation/git/version/v1beta` + ### Read-Only +- `consumed_disk` (String) How many bytes of disk space is consumed. +- `consumed_object_storage` (String) How many bytes of Object Storage is consumed. +- `created` (String) Instance creation timestamp in RFC3339 format. - `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. diff --git a/examples/resources/stackit_git/resource.tf b/examples/resources/stackit_git/resource.tf index b19c4f83..fc5de072 100644 --- a/examples/resources/stackit_git/resource.tf +++ b/examples/resources/stackit_git/resource.tf @@ -1,4 +1,13 @@ resource "stackit_git" "git" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "git-example-instance" +} + +resource "stackit_git" "git" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "git-example-instance" + acl = [ + "0.0.0.0/0" + ] + flavor = "git-100" } \ No newline at end of file diff --git a/stackit/internal/services/git/git_acc_test.go b/stackit/internal/services/git/git_acc_test.go index 687f4e7b..a4a87d00 100644 --- a/stackit/internal/services/git/git_acc_test.go +++ b/stackit/internal/services/git/git_acc_test.go @@ -19,46 +19,74 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) -//go:embed testdata/resource.tf -var resourceConfig string +//go:embed testdata/resource-min.tf +var resourceMin 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)) +//go:embed testdata/resource-max.tf +var resourceMax string -var testConfigVars = config.Variables{ +var nameMin = fmt.Sprintf("git-min-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) +var nameMinUpdated = fmt.Sprintf("git-min-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) +var nameMax = fmt.Sprintf("git-max-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) +var nameMaxUpdated = fmt.Sprintf("git-max-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) +var aclUpdated = "192.168.1.0/32" + +var testConfigVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(name), + "name": config.StringVariable(nameMin), } -func testConfigVarsUpdated() config.Variables { - tempConfig := make(config.Variables, len(testConfigVars)) - maps.Copy(tempConfig, testConfigVars) +var testConfigVarsMax = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "name": config.StringVariable(nameMax), + "acl": config.StringVariable("192.168.0.0/16"), + "flavor": config.StringVariable("git-100"), +} + +func testConfigVarsMinUpdated() config.Variables { + tempConfig := make(config.Variables, len(testConfigVarsMin)) + maps.Copy(tempConfig, testConfigVarsMin) // update git instance to a new name // should trigger creating a new instance - tempConfig["name"] = config.StringVariable(nameUpdated) + tempConfig["name"] = config.StringVariable(nameMinUpdated) return tempConfig } -func TestGitInstance(t *testing.T) { +func testConfigVarsMaxUpdated() config.Variables { + tempConfig := make(config.Variables, len(testConfigVarsMax)) + maps.Copy(tempConfig, testConfigVarsMax) + // update git instance to a new name + // should trigger creating a new instance + tempConfig["name"] = config.StringVariable(nameMaxUpdated) + tempConfig["acl"] = config.StringVariable(aclUpdated) + + return tempConfig +} + +func TestAccGitMin(t *testing.T) { resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckGitInstanceDestroy, Steps: []resource.TestStep{ // Creation { - ConfigVariables: testConfigVars, - Config: testutil.GitProviderConfig() + resourceConfig, + ConfigVariables: testConfigVarsMin, + Config: testutil.GitProviderConfig() + resourceMin, 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.TestCheckResourceAttr("stackit_git.git", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), + resource.TestCheckResourceAttr("stackit_git.git", "name", testutil.ConvertConfigVariable(testConfigVarsMin["name"])), resource.TestCheckResourceAttrSet("stackit_git.git", "url"), resource.TestCheckResourceAttrSet("stackit_git.git", "version"), resource.TestCheckResourceAttrSet("stackit_git.git", "instance_id"), + resource.TestCheckResourceAttrSet("stackit_git.git", "created"), + resource.TestCheckResourceAttrSet("stackit_git.git", "consumed_object_storage"), + resource.TestCheckResourceAttrSet("stackit_git.git", "consumed_disk"), + resource.TestCheckResourceAttrSet("stackit_git.git", "flavor"), ), }, // Data source { - ConfigVariables: testConfigVars, + ConfigVariables: testConfigVarsMin, Config: fmt.Sprintf(` %s @@ -66,11 +94,11 @@ func TestGitInstance(t *testing.T) { project_id = stackit_git.git.project_id instance_id = stackit_git.git.instance_id } - `, testutil.GitProviderConfig()+resourceConfig, + `, testutil.GitProviderConfig()+resourceMin, ), Check: resource.ComposeAggregateTestCheckFunc( // Instance - resource.TestCheckResourceAttr("data.stackit_git.git", "project_id", testutil.ConvertConfigVariable(testConfigVars["project_id"])), + resource.TestCheckResourceAttr("data.stackit_git.git", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), resource.TestCheckResourceAttrPair( "stackit_git.git", "project_id", "data.stackit_git.git", "project_id", @@ -91,11 +119,27 @@ func TestGitInstance(t *testing.T) { "stackit_git.git", "version", "data.stackit_git.git", "version", ), + resource.TestCheckResourceAttrPair( + "stackit_git.git", "created", + "data.stackit_git.git", "created", + ), + resource.TestCheckResourceAttrPair( + "stackit_git.git", "consumed_object_storage", + "data.stackit_git.git", "consumed_object_storage", + ), + resource.TestCheckResourceAttrPair( + "stackit_git.git", "consumed_disk", + "data.stackit_git.git", "consumed_disk", + ), + resource.TestCheckResourceAttrPair( + "stackit_git.git", "flavor", + "data.stackit_git.git", "flavor", + ), ), }, // Import { - ConfigVariables: testConfigVars, + ConfigVariables: testConfigVarsMin, ResourceName: "stackit_git.git", ImportStateIdFunc: func(s *terraform.State) (string, error) { r, ok := s.RootModule().Resources["stackit_git.git"] @@ -113,14 +157,137 @@ func TestGitInstance(t *testing.T) { }, // Update { - ConfigVariables: testConfigVarsUpdated(), - Config: testutil.GitProviderConfig() + resourceConfig, + ConfigVariables: testConfigVarsMinUpdated(), + Config: testutil.GitProviderConfig() + resourceMin, 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.TestCheckResourceAttr("stackit_git.git", "project_id", testutil.ConvertConfigVariable(testConfigVarsMinUpdated()["project_id"])), + resource.TestCheckResourceAttr("stackit_git.git", "name", testutil.ConvertConfigVariable(testConfigVarsMinUpdated()["name"])), resource.TestCheckResourceAttrSet("stackit_git.git", "url"), resource.TestCheckResourceAttrSet("stackit_git.git", "version"), resource.TestCheckResourceAttrSet("stackit_git.git", "instance_id"), + resource.TestCheckResourceAttrSet("stackit_git.git", "created"), + resource.TestCheckResourceAttrSet("stackit_git.git", "consumed_object_storage"), + resource.TestCheckResourceAttrSet("stackit_git.git", "consumed_disk"), + resource.TestCheckResourceAttrSet("stackit_git.git", "flavor"), + ), + }, + // Deletion is done by the framework implicitly + }, + }) +} + +func TestAccGitMax(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckGitInstanceDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigVarsMax, + Config: testutil.GitProviderConfig() + resourceMax, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_git.git", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), + resource.TestCheckResourceAttr("stackit_git.git", "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])), + resource.TestCheckResourceAttr("stackit_git.git", "flavor", testutil.ConvertConfigVariable(testConfigVarsMax["flavor"])), + resource.TestCheckResourceAttr("stackit_git.git", "acl.0", testutil.ConvertConfigVariable(testConfigVarsMax["acl"])), + resource.TestCheckResourceAttrSet("stackit_git.git", "url"), + resource.TestCheckResourceAttrSet("stackit_git.git", "version"), + resource.TestCheckResourceAttrSet("stackit_git.git", "instance_id"), + resource.TestCheckResourceAttrSet("stackit_git.git", "created"), + resource.TestCheckResourceAttrSet("stackit_git.git", "consumed_object_storage"), + resource.TestCheckResourceAttrSet("stackit_git.git", "consumed_disk"), + ), + }, + // Data source + { + ConfigVariables: testConfigVarsMax, + Config: fmt.Sprintf(` + %s + + data "stackit_git" "git" { + project_id = stackit_git.git.project_id + instance_id = stackit_git.git.instance_id + } + `, testutil.GitProviderConfig()+resourceMax, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Instance + resource.TestCheckResourceAttr("data.stackit_git.git", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["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", + ), + resource.TestCheckResourceAttrPair( + "stackit_git.git", "created", + "data.stackit_git.git", "created", + ), + resource.TestCheckResourceAttrPair( + "stackit_git.git", "consumed_object_storage", + "data.stackit_git.git", "consumed_object_storage", + ), + resource.TestCheckResourceAttrPair( + "stackit_git.git", "consumed_disk", + "data.stackit_git.git", "consumed_disk", + ), + resource.TestCheckResourceAttrPair( + "stackit_git.git", "flavor", + "data.stackit_git.git", "flavor", + ), + resource.TestCheckResourceAttrPair( + "stackit_git.git", "acl", + "data.stackit_git.git", "acl", + ), + ), + }, + // Import + { + ConfigVariables: testConfigVarsMax, + 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: testConfigVarsMaxUpdated(), + Config: testutil.GitProviderConfig() + resourceMax, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_git.git", "project_id", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["project_id"])), + resource.TestCheckResourceAttr("stackit_git.git", "name", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["name"])), + resource.TestCheckResourceAttr("stackit_git.git", "flavor", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["flavor"])), + resource.TestCheckResourceAttr("stackit_git.git", "acl.0", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["acl"])), + resource.TestCheckResourceAttrSet("stackit_git.git", "url"), + resource.TestCheckResourceAttrSet("stackit_git.git", "version"), + resource.TestCheckResourceAttrSet("stackit_git.git", "instance_id"), + resource.TestCheckResourceAttrSet("stackit_git.git", "created"), + resource.TestCheckResourceAttrSet("stackit_git.git", "consumed_object_storage"), + resource.TestCheckResourceAttrSet("stackit_git.git", "consumed_disk"), ), }, // Deletion is done by the framework implicitly diff --git a/stackit/internal/services/git/instance/datasource.go b/stackit/internal/services/git/instance/datasource.go index b54d189a..d331810c 100644 --- a/stackit/internal/services/git/instance/datasource.go +++ b/stackit/internal/services/git/instance/datasource.go @@ -12,6 +12,7 @@ import ( "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-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/git" @@ -86,6 +87,27 @@ func (g *gitDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, re validate.NoSeparator(), }, }, + "acl": schema.ListAttribute{ + Description: descriptions["acl"], + Computed: true, + ElementType: types.StringType, + }, + "consumed_disk": schema.StringAttribute{ + Description: descriptions["consumed_disk"], + Computed: true, + }, + "consumed_object_storage": schema.StringAttribute{ + Description: descriptions["consumed_object_storage"], + Computed: true, + }, + "created": schema.StringAttribute{ + Description: descriptions["created"], + Computed: true, + }, + "flavor": schema.StringAttribute{ + Description: descriptions["flavor"], + Computed: true, + }, "name": schema.StringAttribute{ Description: descriptions["name"], Computed: true, @@ -127,7 +149,7 @@ func (g *gitDataSource) Read(ctx context.Context, req datasource.ReadRequest, re return } - err = mapFields(gitInstanceResp, &model) + err = mapFields(ctx, gitInstanceResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading git instance", fmt.Sprintf("Processing API response: %v", err)) return diff --git a/stackit/internal/services/git/instance/resource.go b/stackit/internal/services/git/instance/resource.go index ccc963eb..2b508dd6 100644 --- a/stackit/internal/services/git/instance/resource.go +++ b/stackit/internal/services/git/instance/resource.go @@ -7,15 +7,12 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - gitUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/git/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" "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/listplanmodifier" "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" @@ -24,8 +21,11 @@ import ( "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/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + gitUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/git/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -38,12 +38,17 @@ var ( // 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 + Id types.String `tfsdk:"id"` // Required by Terraform + ACL types.List `tfsdk:"acl"` + ConsumedDisk types.String `tfsdk:"consumed_disk"` + ConsumedObjectStorage types.String `tfsdk:"consumed_object_storage"` + Created types.String `tfsdk:"created"` + Flavor types.String `tfsdk:"flavor"` + InstanceId types.String `tfsdk:"instance_id"` + Name types.String `tfsdk:"name"` + ProjectId types.String `tfsdk:"project_id"` + Url types.String `tfsdk:"url"` + Version types.String `tfsdk:"version"` } // NewGitResource is a helper function to create a new git resource instance. @@ -58,12 +63,17 @@ type gitResource struct { // 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.", + "id": "Terraform's internal resource ID, structured as \"`project_id`,`instance_id`\".", + "acl": "Restricted ACL for instance access.", + "consumed_disk": "How many bytes of disk space is consumed.", + "consumed_object_storage": "How many bytes of Object Storage is consumed.", + "created": "Instance creation timestamp in RFC3339 format.", + "flavor": "Instance flavor. If not provided, defaults to git-100. For a list of available flavors, refer to our API documentation: `https://docs.api.stackit.cloud/documentation/git/version/v1beta`", + "instance_id": "ID linked to the git instance.", + "name": "Unique name linked to the git instance.", + "project_id": "STACKIT project ID to which the git instance is associated.", + "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. @@ -94,8 +104,12 @@ func (g *gitResource) Metadata(_ context.Context, req resource.MetadataRequest, // 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.", core.Resource), - Description: "Git Instance resource schema.", + MarkdownDescription: fmt.Sprintf( + "%s %s", + features.AddBetaDescription("Git Instance resource schema.", core.Resource), + "This resource currently does not support updates. Changing the ACLs, flavor, or name will trigger resource recreation. Update functionality will be added soon. In the meantime, please proceed with caution. To update these attributes, please open a support ticket.", + ), + Description: "Git Instance resource schema.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Description: descriptions["id"], @@ -120,6 +134,35 @@ func (g *gitResource) Schema(_ context.Context, _ resource.SchemaRequest, resp * validate.NoSeparator(), }, }, + "acl": schema.ListAttribute{ + Description: descriptions["acl"], + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + ElementType: types.StringType, + Optional: true, + Computed: true, + }, + "consumed_disk": schema.StringAttribute{ + Description: descriptions["consumed_disk"], + Computed: true, + }, + "consumed_object_storage": schema.StringAttribute{ + Description: descriptions["consumed_object_storage"], + Computed: true, + }, + "created": schema.StringAttribute{ + Description: descriptions["created"], + Computed: true, + }, + "flavor": schema.StringAttribute{ + Description: descriptions["flavor"], + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Optional: true, + Computed: true, + }, "name": schema.StringAttribute{ Description: descriptions["name"], Required: true, @@ -158,9 +201,15 @@ func (g *gitResource) Create(ctx context.Context, req resource.CreateRequest, re ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_name", instanceName) + payload, diags := toCreatePayload(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Create the new git instance via the API client. gitInstanceResp, err := g.client.CreateInstance(ctx, projectId). - CreateInstancePayload(git.CreateInstancePayload{Name: &instanceName}). + CreateInstancePayload(payload). Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating git instance", fmt.Sprintf("Calling API: %v", err)) @@ -174,7 +223,7 @@ func (g *gitResource) Create(ctx context.Context, req resource.CreateRequest, re return } - err = mapFields(gitInstanceResp, &model) + err = mapFields(ctx, gitInstanceResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating git instance", fmt.Sprintf("Mapping fields: %v", err)) return @@ -216,7 +265,7 @@ func (g *gitResource) Read(ctx context.Context, req resource.ReadRequest, resp * return } - err = mapFields(gitInstanceResp, &model) + err = mapFields(ctx, gitInstanceResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading git instance", fmt.Sprintf("Processing API response: %v", err)) return @@ -295,7 +344,7 @@ func (g *gitResource) ImportState(ctx context.Context, req resource.ImportStateR } // mapFields maps a Git response to the model. -func mapFields(resp *git.Instance, model *Model) error { +func mapFields(ctx context.Context, resp *git.Instance, model *Model) error { if resp == nil { return fmt.Errorf("response input is nil") } @@ -307,12 +356,58 @@ func mapFields(resp *git.Instance, model *Model) error { return fmt.Errorf("git instance id not present") } + aclList := types.ListNull(types.StringType) + var diags diag.Diagnostics + if resp.Acl != nil && len(*resp.Acl) > 0 { + aclList, diags = types.ListValueFrom(ctx, types.StringType, resp.Acl) + if diags.HasError() { + return fmt.Errorf("mapping ACL: %w", core.DiagsToError(diags)) + } + } + + model.Created = types.StringNull() + if resp.Created != nil && resp.Created.String() != "" { + model.Created = types.StringValue(resp.Created.String()) + } + // Build the ID by combining the project ID and instance id and assign the model's fields. model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), *resp.Id) - model.Url = types.StringPointerValue(resp.Url) - model.Name = types.StringPointerValue(resp.Name) + model.ACL = aclList + model.ConsumedDisk = types.StringPointerValue(resp.ConsumedDisk) + model.ConsumedObjectStorage = types.StringPointerValue(resp.ConsumedObjectStorage) + model.Flavor = types.StringPointerValue(resp.Flavor) model.InstanceId = types.StringPointerValue(resp.Id) + model.Name = types.StringPointerValue(resp.Name) + model.Url = types.StringPointerValue(resp.Url) model.Version = types.StringPointerValue(resp.Version) return nil } + +// toCreatePayload creates the payload to create a git instance +func toCreatePayload(ctx context.Context, model *Model) (git.CreateInstancePayload, diag.Diagnostics) { + diags := diag.Diagnostics{} + + if model == nil { + return git.CreateInstancePayload{}, diags + } + + payload := git.CreateInstancePayload{ + Name: model.Name.ValueStringPointer(), + } + + if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { + var acl []string + aclDiags := model.ACL.ElementsAs(ctx, &acl, false) + diags.Append(aclDiags...) + if !aclDiags.HasError() { + payload.Acl = &acl + } + } + + if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { + payload.Flavor = model.Flavor.ValueStringPointer() + } + + return payload, diags +} diff --git a/stackit/internal/services/git/instance/resource_test.go b/stackit/internal/services/git/instance/resource_test.go index ff89c5b3..492fa6ae 100644 --- a/stackit/internal/services/git/instance/resource_test.go +++ b/stackit/internal/services/git/instance/resource_test.go @@ -1,79 +1,225 @@ package instance import ( + "context" + "fmt" "testing" + "time" "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/git" ) +var ( + testInstanceId = uuid.New().String() + testProjectId = uuid.New().String() +) + func TestMapFields(t *testing.T) { + createdTime, err := time.Parse("2006-01-02 15:04:05 -0700 MST", "2025-01-01 00:00:00 +0000 UTC") + if err != nil { + t.Fatalf("failed to parse test time: %v", err) + } + tests := []struct { description string input *git.Instance - expected Model + 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"), + description: "minimal_input_name_only", + input: &git.Instance{ + Id: utils.Ptr(testInstanceId), + Name: utils.Ptr("git-min-instance"), }, - 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"), + expected: &Model{ + Id: types.StringValue(fmt.Sprintf("%s,%s", testProjectId, testInstanceId)), + ProjectId: types.StringValue(testProjectId), + InstanceId: types.StringValue(testInstanceId), + Name: types.StringValue("git-min-instance"), + ACL: types.ListNull(types.StringType), + Flavor: types.StringNull(), + Url: types.StringNull(), + Version: types.StringNull(), + Created: types.StringNull(), + ConsumedDisk: types.StringNull(), + ConsumedObjectStorage: types.StringNull(), }, - true, + isValid: true, }, { - "nil_response", - nil, - Model{}, - false, - }, - { - "nil_response_2", - &git.Instance{}, - Model{}, - false, - }, - { - "no_id", - &git.Instance{ - Name: utils.Ptr("foo"), + description: "full_input_with_acl_and_flavor", + input: &git.Instance{ + Acl: &[]string{"192.168.0.0/24"}, + ConsumedDisk: utils.Ptr("1.00 GB"), + ConsumedObjectStorage: utils.Ptr("2.00 GB"), + Created: &createdTime, + Flavor: utils.Ptr("git-100"), + Id: utils.Ptr(testInstanceId), + Name: utils.Ptr("git-full-instance"), + Url: utils.Ptr("https://git-full-instance.git.onstackit.cloud"), + Version: utils.Ptr("v1.9.1"), }, - Model{}, - false, + expected: &Model{ + Id: types.StringValue(fmt.Sprintf("%s,%s", testProjectId, testInstanceId)), + ProjectId: types.StringValue(testProjectId), + InstanceId: types.StringValue(testInstanceId), + Name: types.StringValue("git-full-instance"), + ACL: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("192.168.0.0/24")}), + Flavor: types.StringValue("git-100"), + Url: types.StringValue("https://git-full-instance.git.onstackit.cloud"), + Version: types.StringValue("v1.9.1"), + Created: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), + ConsumedDisk: types.StringValue("1.00 GB"), + ConsumedObjectStorage: types.StringValue("2.00 GB"), + }, + isValid: true, + }, + { + description: "empty_acls", + input: &git.Instance{ + Id: utils.Ptr(testInstanceId), + Name: utils.Ptr("git-empty-acl"), + Acl: &[]string{}, + }, + expected: &Model{ + Id: types.StringValue(fmt.Sprintf("%s,%s", testProjectId, testInstanceId)), + ProjectId: types.StringValue(testProjectId), + InstanceId: types.StringValue(testInstanceId), + Name: types.StringValue("git-empty-acl"), + ACL: types.ListNull(types.StringType), + Flavor: types.StringNull(), + Url: types.StringNull(), + Version: types.StringNull(), + Created: types.StringNull(), + ConsumedDisk: types.StringNull(), + ConsumedObjectStorage: types.StringNull(), + }, + isValid: true, + }, + { + description: "nil_instance", + input: nil, + expected: nil, + isValid: false, + }, + { + description: "empty_instance", + input: &git.Instance{}, + expected: nil, + isValid: false, + }, + { + description: "missing_id", + input: &git.Instance{ + Name: utils.Ptr("git-missing-id"), + }, + expected: nil, + isValid: 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") + state := &Model{} + if tt.expected != nil { + state.ProjectId = tt.expected.ProjectId } + err := mapFields(context.Background(), tt.input, state) + if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) + t.Fatalf("expected success, got error: %v", err) + } + if !tt.isValid && err == nil { + t.Fatalf("expected error, got nil") } if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) + if diff := cmp.Diff(tt.expected, state); diff != "" { + t.Errorf("unexpected diff (-want +got):\n%s", diff) } } }) } } + +func TestToCreatePayload(t *testing.T) { + tests := []struct { + description string + input *Model + expected git.CreateInstancePayload + expectError bool + }{ + { + description: "default values", + input: &Model{ + Name: types.StringValue("example-instance"), + Flavor: types.StringNull(), + ACL: types.ListNull(types.StringType), + }, + expected: git.CreateInstancePayload{ + Name: utils.Ptr("example-instance"), + }, + expectError: false, + }, + { + description: "simple values with ACL and Flavor", + input: &Model{ + Name: types.StringValue("my-instance"), + Flavor: types.StringValue("git-100"), + ACL: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("10.0.0.1"), + types.StringValue("10.0.0.2"), + }), + }, + expected: git.CreateInstancePayload{ + Name: utils.Ptr("my-instance"), + Flavor: utils.Ptr("git-100"), + Acl: &[]string{"10.0.0.1", "10.0.0.2"}, + }, + expectError: false, + }, + { + description: "empty ACL still valid", + input: &Model{ + Name: types.StringValue("my-instance"), + Flavor: types.StringValue("git-101"), + ACL: types.ListValueMust(types.StringType, []attr.Value{}), + }, + expected: git.CreateInstancePayload{ + Name: utils.Ptr("my-instance"), + Flavor: utils.Ptr("git-101"), + Acl: &[]string{}, + }, + expectError: false, + }, + { + description: "nil input model", + input: nil, + expected: git.CreateInstancePayload{}, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output, diags := toCreatePayload(context.Background(), tt.input) + + if tt.expectError && !diags.HasError() { + t.Fatalf("expected diagnostics error but got none") + } + + if !tt.expectError && diags.HasError() { + t.Fatalf("unexpected diagnostics error: %v", diags) + } + + if diff := cmp.Diff(tt.expected, output); diff != "" { + t.Fatalf("unexpected payload (-want +got):\n%s", diff) + } + }) + } +} diff --git a/stackit/internal/services/git/testdata/resource-max.tf b/stackit/internal/services/git/testdata/resource-max.tf new file mode 100644 index 00000000..3945d77b --- /dev/null +++ b/stackit/internal/services/git/testdata/resource-max.tf @@ -0,0 +1,14 @@ + +variable "project_id" {} +variable "name" {} +variable "acl" {} +variable "flavor" {} + +resource "stackit_git" "git" { + project_id = var.project_id + name = var.name + acl = [ + var.acl + ] + flavor = var.flavor +} diff --git a/stackit/internal/services/git/testdata/resource.tf b/stackit/internal/services/git/testdata/resource-min.tf similarity index 81% rename from stackit/internal/services/git/testdata/resource.tf rename to stackit/internal/services/git/testdata/resource-min.tf index 29929022..e412a1c9 100644 --- a/stackit/internal/services/git/testdata/resource.tf +++ b/stackit/internal/services/git/testdata/resource-min.tf @@ -4,5 +4,5 @@ variable "name" {} resource "stackit_git" "git" { project_id = var.project_id - name = var.name + name = var.name }