diff --git a/CHANGELOG.md b/CHANGELOG.md index e6a27015c..2ed70f4de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,8 @@ nav_order: 1 - Add `alloydbomni` BETA resource and datasource - Add `aiven_alloydbomni_user` BETA resource and datasource - Add `aiven_alloydbomni_database` BETA resource and datasource -- Fix `terraform plan`: new resources don't display zero values for user configuration options +- Fix `terraform plan` doesn't display automatically assigned `project_vpc_id` +- Fix `terraform plan` doesn't display fields with zero values in service user config - Add `aiven_service_integration` resource and datasource field `destination_service_project`: Destination service project name - Add `aiven_service_integration` resource and datasource field `source_service_project`: Source service project name - Change `aiven_account_team_project` resource and datasource field `team_type` (enum): remove diff --git a/internal/schemautil/custom_diff.go b/internal/schemautil/custom_diff.go index 2cbb796dd..1634d9d7e 100644 --- a/internal/schemautil/custom_diff.go +++ b/internal/schemautil/custom_diff.go @@ -21,6 +21,7 @@ import ( func CustomizeDiffGenericService(serviceType string) schema.CustomizeDiffFunc { return customdiff.Sequence( SetServiceTypeIfEmpty(serviceType), + CustomizeDiffProjectVPCID, CustomizeDiffDisallowMultipleManyToOneKeys, customdiff.IfValueChange("tag", ShouldNotBeEmpty, @@ -320,3 +321,60 @@ func CustomizeDiffAdditionalDiskSpace(ctx context.Context, diff *schema.Resource // which otherwise will be suppressed by TF return diff.SetNew(k, "0B") } + +// CustomizeDiffProjectVPCID +// 1. Validates project_vpc_id when set +// 2. Sets project_vpc_id when there is only one VPC in the cloud (that's how it works on the backend) +func CustomizeDiffProjectVPCID(ctx context.Context, d *schema.ResourceDiff, _ any) error { + client, err := common.GenClient() + if err != nil { + return err + } + + if v, ok := d.GetOk("project_vpc_id"); ok { + return validateProjectVPCID(ctx, client, v.(string)) + } + + project := d.Get("project").(string) + list, err := client.VpcList(ctx, project) + if err != nil { + return err + } + + vpcIDs := make([]string, 0) + cloudName := d.Get("cloud_name").(string) + for _, v := range list { + if v.CloudName == cloudName { + vpcIDs = append(vpcIDs, v.ProjectVpcId) + } + } + + switch len(vpcIDs) { + case 0: + // It would be nice to set the project_vpc_id to an empty string here, + // but it just won't work because TF takes "" and nil as nothing set. + // That would still result in project_vpc_id marked "known after apply" in the plan, + // even though we know it's going to be empty. + return nil + case 1: + return d.SetNew("project_vpc_id", BuildResourceID(project, vpcIDs[0])) + } + + return fmt.Errorf( + "project %q has multiple active VPCs in cloud %q. Please specify the project_vpc_id: %s", + project, cloudName, strings.Join(vpcIDs, ", "), + ) +} + +func validateProjectVPCID(ctx context.Context, client avngen.Client, projectVPCID string) error { + project, vpcID, err := SplitResourceID2(projectVPCID) + if err != nil { + return err + } + + _, err = client.VpcGet(ctx, project, vpcID) + if avngen.IsNotFound(err) { + return fmt.Errorf("VPC ID %q not found in project %q", vpcID, project) + } + return err +} diff --git a/internal/schemautil/schemautil.go b/internal/schemautil/schemautil.go index 5f116c3a6..9e20b0f40 100644 --- a/internal/schemautil/schemautil.go +++ b/internal/schemautil/schemautil.go @@ -12,6 +12,8 @@ import ( "github.com/aiven/aiven-go-client/v2" "github.com/aiven/go-client-codegen/handler/service" "github.com/docker/go-units" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -195,6 +197,29 @@ func ValidateEmailAddress(v any, k string) (ws []string, errors []error) { return } +// ValidateIDN validates that the given string is a valid resource ID with n parts +func ValidateIDN(n int, expected ...string) schema.SchemaValidateDiagFunc { + if n != len(expected) { + panic(fmt.Sprintf("expected %d parts, got %d", n, len(expected))) + } + + return func(i any, _ cty.Path) diag.Diagnostics { + _, err := SplitResourceID(i.(string), n) + if err == nil { + return nil + } + + return diag.Errorf( + "invalid resource id, should have the following format %q", + strings.Join(expected, "/"), + ) + } +} + +func ValidateIDWithProject(expected string) schema.SchemaValidateDiagFunc { + return ValidateIDN(2, "project_name", expected) +} + func BuildResourceID(parts ...string) string { finalParts := make([]string, len(parts)) for idx, part := range parts { diff --git a/internal/schemautil/service.go b/internal/schemautil/service.go index 95dc5de30..7369f150d 100644 --- a/internal/schemautil/service.go +++ b/internal/schemautil/service.go @@ -136,10 +136,11 @@ func ServiceCommonSchema() map[string]*schema.Schema { Description: "Aiven internal service type code", }, "project_vpc_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - Description: "Specifies the VPC the service should run in. If the value is not set the service is not run inside a VPC. When set, the value should be given as a reference to set up dependencies correctly and the VPC must be in the same cloud and region as the service itself. Project can be freely moved to and from VPC after creation but doing so triggers migration to new servers so the operation can take significant amount of time to complete if the service has a lot of data.", + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Specifies the VPC the service should run in. If the value is not set the service is not run inside a VPC. When set, the value should be given as a reference to set up dependencies correctly and the VPC must be in the same cloud and region as the service itself. Project can be freely moved to and from VPC after creation but doing so triggers migration to new servers so the operation can take significant amount of time to complete if the service has a lot of data.", + ValidateDiagFunc: ValidateIDWithProject("project_vpc_id"), }, "maintenance_window_dow": { Type: schema.TypeString,