package postgresflexalpha_test import ( "context" _ "embed" "fmt" "log" "math" "os" "regexp" "strconv" "strings" "testing" "time" "github.com/hashicorp/terraform-plugin-testing/compare" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/knownvalue" "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/statecheck" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v3alpha1api" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core" postgresflexalphaInstance "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/instance" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/wait/postgresflexalpha" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/internal/testutils" // The fwresource import alias is so there is no collision // with the more typical acceptance testing import: // "github.com/hashicorp/terraform-plugin-testing/helper/resource" fwresource "github.com/hashicorp/terraform-plugin-framework/resource" ) const ( pfx = "stackitprivatepreview_postgresflexalpha" singleFlavorID = "2.4" replicasFlavorID = "2.4-replica" ) func TestInstanceResourceSchema(t *testing.T) { // t.Parallel() ctx := context.Background() schemaRequest := fwresource.SchemaRequest{} schemaResponse := &fwresource.SchemaResponse{} // Instantiate the resource.Resource and call its Schema method postgresflexalphaInstance.NewInstanceResource().Schema(ctx, schemaRequest, schemaResponse) if schemaResponse.Diagnostics.HasError() { t.Fatalf("Schema method diagnostics: %+v", schemaResponse.Diagnostics) } // Validate the schema diagnostics := schemaResponse.Schema.ValidateImplementation(ctx) if diagnostics.HasError() { t.Fatalf("Schema validation diagnostics: %+v", diagnostics) } } func TestMain(m *testing.M) { testutils.Setup() code := m.Run() // shutdown() os.Exit(code) } func testAccPreCheck(t *testing.T) { if _, ok := os.LookupEnv("TF_ACC_PROJECT_ID"); !ok { t.Fatalf("could not find env var TF_ACC_PROJECT_ID") } } type resData struct { ServiceAccountFilePath string ProjectID string Region string Name string TfName string FlavorID string BackupSchedule string UseEncryption bool KekKeyID string KekKeyRingID string KekKeyVersion uint8 KekServiceAccount string PerformanceClass string Replicas uint32 Size uint32 ACLStrings []string AccessScope string RetentionDays uint32 Version string Users []User Databases []Database } type User struct { Name string ProjectID string Roles []string } type Database struct { Name string ProjectID string Owner string } func getExample() resData { name := acctest.RandomWithPrefix("tf-acc") return resData{ Region: os.Getenv("TF_ACC_REGION"), ServiceAccountFilePath: os.Getenv("TF_ACC_SERVICE_ACCOUNT_FILE"), ProjectID: os.Getenv("TF_ACC_PROJECT_ID"), Name: name, TfName: name, FlavorID: singleFlavorID, BackupSchedule: "0 0 * * *", UseEncryption: false, RetentionDays: 33, Replicas: 1, PerformanceClass: "premium-perf2-stackit", Size: 10, ACLStrings: []string{"0.0.0.0/0"}, AccessScope: "PUBLIC", Version: "17", } } func TestAccInstance(t *testing.T) { exData := getExample() updNameData := exData updNameData.Name = "name-updated" updSizeData := exData updSizeData.Size = 25 updBackupSched := updSizeData // api should complain about more than one daily backup updBackupSched.BackupSchedule = "30 3 * * *" updNetACL := updBackupSched updNetACL.ACLStrings = append(updNetACL.ACLStrings, "192.168.0.0/24") /* { "flavorId": "1.2", "network": { "acl": [ "198.51.100.0/24" ] }, "replicas": 1, "retentionDays": 35, "storage": { "size": 10 }, "version": "string" } */ testItemID := testutils.ResStr(pfx, "instance", exData.TfName) compareValuesSame := statecheck.CompareValue(compare.ValuesSame()) resource.ParallelTest( t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) t.Logf(" ... working on instance %s", exData.TfName) }, CheckDestroy: testAccCheckPostgresFlexDestroy, ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Create and verify { //PreConfig: func() { // // // }, Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", exData, ), ConfigStateChecks: []statecheck.StateCheck{ compareValuesSame.AddStateValue( testItemID, tfjsonpath.New("id"), ), statecheck.ExpectKnownValue( testItemID, tfjsonpath.New("is_deletable"), knownvalue.Bool(true), ), statecheck.ExpectKnownValue( testItemID, tfjsonpath.New("connection_info"), knownvalue.MapExact(map[string]knownvalue.Check{ "write": knownvalue.MapExact(map[string]knownvalue.Check{ "host": knownvalue.StringRegexp(regexp.MustCompile("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}.postgresql.[a-z0-9]+.onstackit.cloud")), "port": knownvalue.Int32Func(func(v int32) error { if v < 0 { return fmt.Errorf("value is negative") } if v <= 1024 { return fmt.Errorf("value uses protected port range") } return nil }), }), }), ), }, Check: defaultNoEncInstanceTestChecks(testItemID, exData), }, // Second apply should not have changes { //PreConfig: func() { // // // }, Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", exData, ), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectEmptyPlan(), }, }, ConfigStateChecks: []statecheck.StateCheck{ compareValuesSame.AddStateValue( testItemID, tfjsonpath.New("id"), ), statecheck.ExpectKnownValue( testItemID, tfjsonpath.New("is_deletable"), knownvalue.Bool(true), ), }, }, // Refresh state test { RefreshState: true, }, // Update name and verify { Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", updNameData, ), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr( testItemID, "name", updNameData.Name, ), ), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectNonEmptyPlan(), }, }, }, // Update size and verify { Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", updSizeData, ), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr( testItemID, "storage.size", strconv.Itoa(int(updSizeData.Size)), ), // network should contain 4 sub entries resource.TestCheckResourceAttr(testItemID, "network.acl.#", "1"), ), }, // Update backup schedule { Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", updBackupSched, ), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr( testItemID, "backup_schedule", updBackupSched.BackupSchedule, ), // network should contain 4 sub entries resource.TestCheckResourceAttr(testItemID, "network.acl.#", "1"), ), }, // Update network ACL { Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", updNetACL, ), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr( testItemID, "backup_schedule", updBackupSched.BackupSchedule, ), // network should contain 4 sub entries resource.TestCheckResourceAttr(testItemID, "network.acl.#", "2"), ), }, // Import test /* { ImportState: true, ImportStateKind: resource.ImportBlockWithResourceIdentity, ResourceName: testItemID, }, { ImportState: true, ImportStateKind: resource.ImportCommandWithID, ResourceName: testItemID, }, */ }, }, ) } func TestAccInstanceHA(t *testing.T) { data := getExample() data.FlavorID = replicasFlavorID data.Replicas = 3 testItemID := testutils.ResStr(pfx, "instance", data.TfName) resource.ParallelTest( t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) t.Logf(" ... working on instance %s", data.TfName) }, CheckDestroy: testAccCheckPostgresFlexDestroy, ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Create and verify { Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", data, ), Check: defaultNoEncInstanceTestChecks(testItemID, data), }, }, }, ) } func TestAccInstanceWithUsers(t *testing.T) { data := getExample() userName := "testUser" data.Users = []User{ { Name: userName, ProjectID: os.Getenv("TF_ACC_PROJECT_ID"), Roles: []string{"login"}, }, } testItemID := testutils.ResStr(pfx, "instance", data.TfName) // TODO : implement check multiple users testUserItemID := testutils.ResStr(pfx, "user", userName) resource.ParallelTest( t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) t.Logf(" ... working on instance %s", data.TfName) }, CheckDestroy: testAccCheckPostgresFlexDestroy, ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Create and verify { Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", data, ), Check: resource.ComposeAggregateTestCheckFunc( defaultNoEncInstanceTestChecks(testItemID, data), resource.TestCheckResourceAttr(testUserItemID, "name", userName), resource.TestCheckResourceAttrSet(testUserItemID, "id"), resource.TestCheckResourceAttr(testUserItemID, "roles.#", "1"), ), }, }, }, ) } func TestAccInstanceWithDatabases(t *testing.T) { data := getExample() dbName := "testdb" userName := "testUser" data.Users = []User{ { Name: userName, ProjectID: os.Getenv("TF_ACC_PROJECT_ID"), Roles: []string{"login"}, }, } data.Databases = []Database{ { Name: dbName, ProjectID: os.Getenv("TF_ACC_PROJECT_ID"), Owner: userName, }, } testItemID := testutils.ResStr(pfx, "instance", data.TfName) resource.ParallelTest( t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) t.Logf(" ... working on instance %s", data.TfName) }, CheckDestroy: testAccCheckPostgresFlexDestroy, ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Create and verify { Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", data, ), Check: resource.ComposeAggregateTestCheckFunc( defaultNoEncInstanceTestChecks(testItemID, data), resource.TestCheckResourceAttr(testutils.ResStr(pfx, "user", userName), "name", userName), resource.TestCheckResourceAttrSet(testutils.ResStr(pfx, "user", userName), "id"), resource.TestCheckResourceAttr(testutils.ResStr(pfx, "database", dbName), "name", dbName), resource.TestCheckResourceAttr(testutils.ResStr(pfx, "database", dbName), "owner", userName), resource.TestCheckResourceAttrSet(testutils.ResStr(pfx, "database", dbName), "id"), ), }, }, }, ) } func TestAccEncryptedInstanceWithDatabases(t *testing.T) { encKekKeyID, ok := os.LookupEnv("TF_ACC_KEK_KEY_ID") if !ok || encKekKeyID == "" { t.Skip("env var TF_ACC_KEK_KEY_ID needed for encryption test") } encKekKeyRingID, ok := os.LookupEnv("TF_ACC_KEK_KEY_RING_ID") if !ok || encKekKeyRingID == "" { t.Skip("env var TF_ACC_KEK_KEY_RING_ID needed for encryption test") } encKekKeyVersion, ok := os.LookupEnv("TF_ACC_KEK_KEY_VERSION") if !ok || encKekKeyVersion == "" { t.Skip("env var TF_ACC_KEK_KEY_VERSION needed for encryption test") } encSvcAcc, ok := os.LookupEnv("TF_ACC_KEK_SERVICE_ACCOUNT") if !ok || encSvcAcc == "" { t.Skip("env var TF_ACC_KEK_SERVICE_ACCOUNT needed for encryption test") } data := getExample() data.UseEncryption = true data.KekKeyID = encKekKeyID data.KekKeyRingID = encKekKeyRingID data.KekServiceAccount = encSvcAcc encKekKeyVersionInt, err := strconv.Atoi(encKekKeyVersion) if err != nil { t.Errorf("error converting string to int") } if encKekKeyVersionInt > math.MaxUint8 { t.Errorf("value too large to convert to uint8") } data.KekKeyVersion = uint8(encKekKeyVersionInt) //nolint:gosec // handled above dbName := "testdb" userName := "testUser" data.Users = []User{ { Name: userName, ProjectID: os.Getenv("TF_ACC_PROJECT_ID"), Roles: []string{"login"}, }, } data.Databases = []Database{ { Name: dbName, ProjectID: os.Getenv("TF_ACC_PROJECT_ID"), Owner: userName, }, } testItemID := testutils.ResStr(pfx, "instance", data.TfName) resource.ParallelTest( t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) t.Logf(" ... working on instance %s", data.TfName) }, CheckDestroy: testAccCheckPostgresFlexDestroy, ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Create and verify { Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", data, ), Check: resource.ComposeAggregateTestCheckFunc( defaultEncInstanceTestChecks(testItemID, data), resource.TestCheckResourceAttr(testutils.ResStr(pfx, "user", userName), "name", userName), resource.TestCheckResourceAttrSet(testutils.ResStr(pfx, "user", userName), "id"), resource.TestCheckResourceAttr(testutils.ResStr(pfx, "database", dbName), "name", dbName), resource.TestCheckResourceAttr(testutils.ResStr(pfx, "database", dbName), "owner", userName), resource.TestCheckResourceAttrSet(testutils.ResStr(pfx, "database", dbName), "id"), ), }, }, }, ) } func testAccCheckPostgresFlexDestroy(s *terraform.State) error { testutils.Setup() pID, ok := os.LookupEnv("TF_ACC_PROJECT_ID") if !ok { log.Fatalln("unable to read TF_ACC_PROJECT_ID") } ctx := context.Background() var client *v3alpha1api.APIClient var err error var region, projectID string region = testutils.Region if region == "" { region = "eu01" } projectID = pID if projectID == "" { return fmt.Errorf("projectID could not be determined in destroy function") } apiClientConfigOptions := []config.ConfigurationOption{ config.WithServiceAccountKeyPath(os.Getenv("TF_ACC_SERVICE_ACCOUNT_FILE")), config.WithRegion(region), } if testutils.PostgresFlexCustomEndpoint != "" { apiClientConfigOptions = append( apiClientConfigOptions, config.WithEndpoint(testutils.PostgresFlexCustomEndpoint), ) } client, err = v3alpha1api.NewAPIClient(apiClientConfigOptions...) if err != nil { log.Fatalln(err) } instancesToDestroy := []string{} for _, rs := range s.RootModule().Resources { if rs.Type != "stackitprivatepreview_postgresflexalpha_instance" && rs.Type != "stackitprivatepreview_postgresflexbeta_instance" { continue } // instance terraform ID: = "[project_id],[region],[instance_id]" instanceID := strings.Split(rs.Primary.ID, core.Separator)[2] instancesToDestroy = append(instancesToDestroy, instanceID) } instancesResp, err := client.DefaultAPI.ListInstancesRequest(ctx, projectID, region). Size(100). Execute() if err != nil { return fmt.Errorf("getting instancesResp: %w", err) } items := instancesResp.GetInstances() for i := range items { if items[i].Id == "" { continue } if utils.Contains(instancesToDestroy, items[i].Id) { err := client.DefaultAPI.DeleteInstanceRequest(ctx, testutils.ProjectId, region, items[i].Id).Execute() if err != nil { return fmt.Errorf("deleting instance %s during CheckDestroy: %w", items[i].Id, err) } err = postgresflexalpha.DeleteInstanceWaitHandler( ctx, client.DefaultAPI, testutils.ProjectId, testutils.Region, items[i].Id, 15*time.Minute, 10*time.Second, ) if err != nil { return fmt.Errorf("deleting instance %s during CheckDestroy: waiting for deletion %w", items[i].Id, err) } } } return nil } func defaultNoEncInstanceTestChecks(testItemID string, data resData) resource.TestCheckFunc { return resource.ComposeAggregateTestCheckFunc( defaultInstanceTestChecks(testItemID, data), // on unencrypted instances we expect this to be empty resource.TestCheckResourceAttr(testItemID, "network.instance_address", ""), resource.TestCheckResourceAttr(testItemID, "network.router_address", ""), // check absent attr resource.TestCheckNoResourceAttr(testItemID, "encryption"), resource.TestCheckNoResourceAttr(testItemID, "encryption.kek_key_id"), resource.TestCheckNoResourceAttr(testItemID, "encryption.kek_key_ring_id"), resource.TestCheckNoResourceAttr(testItemID, "encryption.kek_key_version"), resource.TestCheckNoResourceAttr(testItemID, "encryption.service_account"), ) } func defaultEncInstanceTestChecks(testItemID string, data resData) resource.TestCheckFunc { return resource.ComposeAggregateTestCheckFunc( defaultInstanceTestChecks(testItemID, data), // on unencrypted instances we expect this to be empty resource.TestCheckResourceAttrSet(testItemID, "network.instance_address"), resource.TestCheckResourceAttrSet(testItemID, "network.router_address"), // check absent attr resource.TestCheckResourceAttrSet(testItemID, "encryption"), resource.TestCheckResourceAttrSet(testItemID, "encryption.kek_key_id"), resource.TestCheckResourceAttr(testItemID, "encryption.kek_key_id", data.KekKeyID), resource.TestCheckResourceAttrSet(testItemID, "encryption.kek_key_ring_id"), resource.TestCheckResourceAttr(testItemID, "encryption.kek_key_ring_id", data.KekKeyRingID), resource.TestCheckResourceAttrSet(testItemID, "encryption.kek_key_version"), resource.TestCheckResourceAttr(testItemID, "encryption.kek_key_version", strconv.Itoa(int(data.KekKeyVersion))), resource.TestCheckResourceAttrSet(testItemID, "encryption.service_account"), resource.TestCheckResourceAttr(testItemID, "encryption.service_account", data.KekServiceAccount), ) } func defaultInstanceTestChecks(testItemID string, data resData) resource.TestCheckFunc { return resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet(testItemID, "backup_schedule"), resource.TestCheckResourceAttr(testItemID, "backup_schedule", data.BackupSchedule), resource.TestCheckResourceAttr(testItemID, "connection_info.%", "1"), resource.TestCheckResourceAttr(testItemID, "connection_info.write.%", "2"), resource.TestCheckResourceAttrSet(testItemID, "connection_info.write.host"), resource.TestCheckResourceAttrSet(testItemID, "connection_info.write.port"), resource.TestCheckResourceAttrSet(testItemID, "flavor_id"), resource.TestCheckResourceAttr(testItemID, "flavor_id", data.FlavorID), resource.TestCheckResourceAttrSet(testItemID, "id"), resource.TestCheckResourceAttrSet(testItemID, "instance_id"), resource.TestCheckResourceAttrSet(testItemID, "is_deletable"), resource.TestCheckResourceAttr(testItemID, "is_deletable", "true"), resource.TestCheckResourceAttrSet(testItemID, "name"), resource.TestCheckResourceAttr(testItemID, "name", data.Name), // network params check resource.TestCheckResourceAttr(testItemID, "network.%", "4"), resource.TestCheckResourceAttrSet(testItemID, "network"), resource.TestCheckResourceAttrSet(testItemID, "network.access_scope"), resource.TestCheckResourceAttr(testItemID, "network.access_scope", data.AccessScope), // resource.TestCheckResourceAttrSet(testItemID, "network.acl"), resource.TestCheckResourceAttr(testItemID, "network.acl.#", strconv.Itoa(len(data.ACLStrings))), // instance_address and router_address are only checked in enc resource.TestCheckResourceAttrSet(testItemID, "project_id"), resource.TestCheckResourceAttr(testItemID, "project_id", data.ProjectID), resource.TestCheckResourceAttrSet(testItemID, "region"), resource.TestCheckResourceAttr(testItemID, "region", data.Region), resource.TestCheckResourceAttrSet(testItemID, "replicas"), resource.TestCheckResourceAttr(testItemID, "replicas", strconv.Itoa(int(data.Replicas))), resource.TestCheckResourceAttrSet(testItemID, "retention_days"), resource.TestCheckResourceAttr(testItemID, "retention_days", strconv.Itoa(int(data.RetentionDays))), resource.TestCheckResourceAttrSet(testItemID, "status"), resource.TestCheckResourceAttr(testItemID, "status", "READY"), // storage params check resource.TestCheckResourceAttr(testItemID, "storage.%", "2"), resource.TestCheckResourceAttrSet(testItemID, "storage.performance_class"), resource.TestCheckResourceAttr(testItemID, "storage.performance_class", data.PerformanceClass), resource.TestCheckResourceAttrSet(testItemID, "storage.size"), resource.TestCheckResourceAttr(testItemID, "storage.size", strconv.Itoa(int(data.Size))), resource.TestCheckResourceAttrSet(testItemID, "version"), resource.TestCheckResourceAttr(testItemID, "version", data.Version), ) }