package sqlserverflexbeta_test import ( "context" _ "embed" "errors" "fmt" "log" "net/http" "os" "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/oapierror" "github.com/stackitcloud/stackit-sdk-go/core/utils" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core" wait "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/wait/sqlserverflexbeta" "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v3beta1api" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/internal/testutils" sqlserverflexbeta "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexbeta/instance" // 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_sqlserverflexbeta" 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 sqlserverflexbeta.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 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 Collation string Compatibility 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: "4.16-Single", BackupSchedule: "0 0 * * *", UseEncryption: false, RetentionDays: 33, PerformanceClass: "premium-perf2-stackit", Size: 10, ACLStrings: []string{"0.0.0.0/0"}, AccessScope: "PUBLIC", Version: "2022", } } func TestAccInstance(t *testing.T) { exData := getExample() updNameData := exData updNameData.Name = "name-updated" updSizeData := exData updSizeData.Size = 25 testInstanceID := testutils.ResStr(pfx, "instance", exData.TfName) compareValuesSame := statecheck.CompareValue(compare.ValuesSame()) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) t.Logf(" ... %s - %s", t.Name(), exData.TfName) }, CheckDestroy: testAccCheckSQLServerFlexDestroy, ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Create and verify { PreConfig: func() { t.Logf("testing: %s - %s", t.Name(), "create and verify") }, ExpectNonEmptyPlan: true, Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", exData, ), ConfigStateChecks: []statecheck.StateCheck{ compareValuesSame.AddStateValue( testInstanceID, tfjsonpath.New("id"), ), statecheck.ExpectKnownValue( testInstanceID, tfjsonpath.New("is_deletable"), knownvalue.Bool(true), ), }, Check: defaultNoEncInstanceTestChecks(testInstanceID, exData), }, // Update name and verify { PreConfig: func() { t.Logf("testing: %s - %s", t.Name(), "update name and verify") }, ExpectNonEmptyPlan: true, Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", updNameData, ), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectNonEmptyPlan(), }, }, Check: resource.ComposeTestCheckFunc( defaultNoEncInstanceTestChecks(testInstanceID, updNameData), ), }, // Update size and verify { PreConfig: func() { t.Logf("testing: %s - %s", t.Name(), "update storage.size and verify") }, ExpectNonEmptyPlan: true, Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", updSizeData, ), Check: resource.ComposeTestCheckFunc( defaultNoEncInstanceTestChecks(testInstanceID, updSizeData), ), }, // Import test // test instance imports { PreConfig: func() { t.Logf("testing: %s - %s", t.Name(), "import instance") }, ResourceName: testInstanceID, // ImportStateIdPrefix: "", // ImportStateVerifyIdentifierAttribute: "id", ImportStateIdFunc: getInstanceTestID(exData.TfName), ImportStateKind: resource.ImportCommandWithID, ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccInstanceReApply(t *testing.T) { exData := getExample() testInstanceID := testutils.ResStr(pfx, "instance", exData.TfName) compareValuesSame := statecheck.CompareValue(compare.ValuesSame()) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) t.Logf(" ... %s - %s", t.Name(), exData.TfName) }, CheckDestroy: testAccCheckSQLServerFlexDestroy, ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Create and verify { PreConfig: func() { t.Logf("testing: %s - %s", t.Name(), "create and verify") }, Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", exData, ), ConfigStateChecks: []statecheck.StateCheck{ compareValuesSame.AddStateValue( testInstanceID, tfjsonpath.New("id"), ), statecheck.ExpectKnownValue( testInstanceID, tfjsonpath.New("is_deletable"), knownvalue.Bool(true), ), }, Check: defaultNoEncInstanceTestChecks(testInstanceID, exData), }, // Second apply should not have changes { PreConfig: func() { t.Logf("testing: %s - %s", t.Name(), "second apply") }, ExpectNonEmptyPlan: false, ResourceName: testInstanceID, Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", exData, ), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectEmptyPlan(), }, }, ConfigStateChecks: []statecheck.StateCheck{ compareValuesSame.AddStateValue( testInstanceID, tfjsonpath.New("id"), ), statecheck.ExpectKnownValue( testInstanceID, tfjsonpath.New("is_deletable"), knownvalue.Bool(true), ), }, }, // Refresh state test { PreConfig: func() { t.Logf("testing: %s - %s", t.Name(), "refresh state") }, RefreshState: true, }, }, }) } func TestAccInstanceNoEncryption(t *testing.T) { data := getExample() dbName := "testDb" userName := "testUser" data.Users = []User{ { Name: userName, ProjectID: os.Getenv("TF_ACC_PROJECT_ID"), Roles: []string{ "##STACKIT_DatabaseManager##", "##STACKIT_LoginManager##", "##STACKIT_ProcessManager##", "##STACKIT_SQLAgentManager##", "##STACKIT_SQLAgentUser##", "##STACKIT_ServerManager##", }, }, } data.Databases = []Database{ { Name: dbName, ProjectID: os.Getenv("TF_ACC_PROJECT_ID"), Owner: userName, }, } testInstanceID := testutils.ResStr(pfx, "instance", data.TfName) testDatabaseID := testutils.ResStr(pfx, "database", dbName) testUserID := testutils.ResStr(pfx, "user", userName) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) t.Logf(" ... %s - %s", t.Name(), data.TfName) }, CheckDestroy: testAccCheckSQLServerFlexDestroy, ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Create and verify { Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", data, ), Check: resource.ComposeAggregateTestCheckFunc( defaultNoEncInstanceTestChecks(testInstanceID, data), // check user values are correct resource.TestCheckResourceAttr(testUserID, "username", userName), resource.TestCheckResourceAttr(testUserID, "roles.#", strconv.Itoa(len(data.Users[0].Roles))), // check database values are set resource.TestCheckResourceAttrSet(testDatabaseID, "id"), resource.TestCheckResourceAttrSet(testDatabaseID, "name"), resource.TestCheckResourceAttrSet(testDatabaseID, "owner"), resource.TestCheckResourceAttrSet(testDatabaseID, "compatibility"), resource.TestCheckResourceAttrSet(testDatabaseID, "collation"), // check database values are correct resource.TestCheckResourceAttr(testDatabaseID, "name", dbName), resource.TestCheckResourceAttr(testDatabaseID, "owner", userName), ), }, }, }) } func TestAccInstanceEncryption(t *testing.T) { data := getExample() dbName := "testDb" userName := "testUser" data.Users = []User{ { Name: userName, ProjectID: os.Getenv("TF_ACC_PROJECT_ID"), Roles: []string{"##STACKIT_DatabaseManager##", "##STACKIT_LoginManager##"}, }, } data.Databases = []Database{ { Name: dbName, ProjectID: os.Getenv("TF_ACC_PROJECT_ID"), Owner: userName, }, } data.UseEncryption = true data.KekKeyID = "fe039bcf-8d7b-431a-801d-9e81371a6b7b" data.KekKeyRingID = "6a2d95ab-3c4c-4963-a2bb-08d17a320e27" data.KekKeyVersion = 1 data.KekServiceAccount = "henselinm-u2v3ex1@sa.stackit.cloud" testInstanceID := testutils.ResStr(pfx, "instance", data.TfName) testDatabaseID := testutils.ResStr(pfx, "database", dbName) testUserID := testutils.ResStr(pfx, "user", userName) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) t.Logf(" ... %s - %s", t.Name(), data.TfName) }, CheckDestroy: testAccCheckSQLServerFlexDestroy, ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Create and verify { Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", data, ), Check: resource.ComposeAggregateTestCheckFunc( defaultEncInstanceTestChecks(testInstanceID, data), // check user values are set resource.TestCheckResourceAttrSet(testUserID, "id"), resource.TestCheckResourceAttrSet(testUserID, "username"), // func(s *terraform.State) error { // return nil // }, // check user values are correct resource.TestCheckResourceAttr(testUserID, "username", userName), resource.TestCheckResourceAttr(testUserID, "roles.#", "2"), // check database values are set resource.TestCheckResourceAttrSet(testDatabaseID, "id"), resource.TestCheckResourceAttrSet(testDatabaseID, "name"), resource.TestCheckResourceAttrSet(testDatabaseID, "owner"), resource.TestCheckResourceAttrSet(testDatabaseID, "compatibility"), resource.TestCheckResourceAttrSet(testDatabaseID, "collation"), // check database values are correct resource.TestCheckResourceAttr(testDatabaseID, "name", dbName), resource.TestCheckResourceAttr(testDatabaseID, "owner", userName), ), }, }, }) } func defaultNoEncInstanceTestChecks(testItemID string, data resData) resource.TestCheckFunc { return resource.ComposeAggregateTestCheckFunc( defaultInstanceTestChecks(testItemID, data), // 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), // check absent attr resource.TestCheckResourceAttr(testItemID, "encryption.%", "4"), 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 { // if AccessScope == SNA these are set if data.AccessScope == "SNA" { return resource.ComposeAggregateTestCheckFunc( basicInstanceTestChecks(testItemID, data), resource.TestCheckResourceAttrSet(testItemID, "network.instance_address"), resource.TestCheckResourceAttrSet(testItemID, "network.router_address"), ) } // if AccessScope == PUBLIC these are empty - but they are set return resource.ComposeAggregateTestCheckFunc( basicInstanceTestChecks(testItemID, data), resource.TestCheckResourceAttr(testItemID, "network.instance_address", ""), resource.TestCheckResourceAttr(testItemID, "network.router_address", ""), ) } func basicInstanceTestChecks(testItemID string, data resData) resource.TestCheckFunc { return resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet(testItemID, "backup_schedule"), resource.TestCheckResourceAttr(testItemID, "backup_schedule", data.BackupSchedule), resource.TestCheckResourceAttrSet(testItemID, "flavor_id"), resource.TestCheckResourceAttr(testItemID, "flavor_id", data.FlavorID), resource.TestCheckResourceAttrSet(testItemID, "id"), resource.TestCheckResourceAttrSet(testItemID, "instance_id"), resource.TestCheckResourceAttrSet(testItemID, "edition"), 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.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, "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.class"), resource.TestCheckResourceAttr(testItemID, "storage.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), ) } func getInstanceTestID(name string) func(s *terraform.State) (string, error) { return func(s *terraform.State) (string, error) { r, ok := s.RootModule().Resources[testutils.ResStr(pfx, "instance", name)] if !ok { return "", fmt.Errorf("couldn't find resource stackitprivatepreview_postgresflexalpha_instance.%s", name) } projectID, ok := r.Primary.Attributes["project_id"] if !ok { return "", fmt.Errorf("couldn't find attribute project_id") } region, ok := r.Primary.Attributes["region"] if !ok { return "", fmt.Errorf("couldn't find attribute region") } instanceID, ok := r.Primary.Attributes["instance_id"] if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") } return fmt.Sprintf("%s,%s,%s", projectID, region, instanceID), nil } } /* func getDatabaseTestID(name string) func(s *terraform.State) (string, error) { return func(s *terraform.State) (string, error) { r, ok := s.RootModule().Resources[testutils.ResStr(pfx, "database", name)] if !ok { return "", fmt.Errorf("couldn't find resource stackitprivatepreview_postgresflexalpha_instance.%s", name) } projectID, ok := r.Primary.Attributes["project_id"] if !ok { return "", fmt.Errorf("couldn't find attribute project_id") } region, ok := r.Primary.Attributes["region"] if !ok { return "", fmt.Errorf("couldn't find attribute region") } instanceID, ok := r.Primary.Attributes["instance_id"] if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") } databaseID, ok := r.Primary.Attributes["database_id"] if !ok { return "", fmt.Errorf("couldn't find attribute database_id") } return fmt.Sprintf("%s,%s,%s,%s", projectID, region, instanceID, databaseID), nil } } func getUserTestID(name string) func(s *terraform.State) (string, error) { return func(s *terraform.State) (string, error) { r, ok := s.RootModule().Resources[testutils.ResStr(pfx, "user", name)] if !ok { return "", fmt.Errorf("couldn't find resource stackitprivatepreview_postgresflexalpha_instance.%s", name) } projectID, ok := r.Primary.Attributes["project_id"] if !ok { return "", fmt.Errorf("couldn't find attribute project_id") } region, ok := r.Primary.Attributes["region"] if !ok { return "", fmt.Errorf("couldn't find attribute region") } instanceID, ok := r.Primary.Attributes["instance_id"] if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") } userID, ok := r.Primary.Attributes["user_id"] if !ok { return "", fmt.Errorf("couldn't find attribute user_id") } return fmt.Sprintf("%s,%s,%s,%s", projectID, region, instanceID, userID), nil } } */ func testAccCheckSQLServerFlexDestroy(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 *v3beta1api.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 = v3beta1api.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, projectID, region, items[i].Id).Execute() if err != nil { return fmt.Errorf("deleting instance %s during CheckDestroy: %w", items[i].Id, err) } w := wait.DeleteInstanceWaitHandler( ctx, client.DefaultAPI, testutils.ProjectId, testutils.Region, items[i].Id, ) _, waitErr := w.SetTimeout(90 * time.Second).WaitWithContext(context.Background()) if waitErr != nil { var oapiErr *oapierror.GenericOpenAPIError isOapiErr := errors.As(waitErr, &oapiErr) if !isOapiErr { return fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError") } if oapiErr.StatusCode != http.StatusNotFound { return waitErr } } } } return nil }