diff --git a/cmd/cmd/build/build.go b/cmd/cmd/build/build.go index 7b11f214..eb9f0053 100644 --- a/cmd/cmd/build/build.go +++ b/cmd/cmd/build/build.go @@ -41,6 +41,8 @@ type Builder struct { SkipClone bool SkipCleanup bool PackagesOnly bool + Verbose bool + Debug bool } func (b *Builder) Build() error { @@ -56,13 +58,17 @@ func (b *Builder) Build() error { if root == nil || *root == "" { return fmt.Errorf("unable to determine root directory from git") } - slog.Info(" ... using root directory", "dir", *root) + if b.Verbose { + slog.Info(" ... using root directory", "dir", *root) + } if !b.PackagesOnly { - slog.Info(" ... Checking needed commands available") - err := checkCommands([]string{}) - if err != nil { - return err + if b.Verbose { + slog.Info(" ... Checking needed commands available") + } + chkErr := checkCommands([]string{}) + if chkErr != nil { + return chkErr } } @@ -92,14 +98,13 @@ func (b *Builder) Build() error { } slog.Info("Creating oas repo dir", "dir", fmt.Sprintf("%s/%s", *root, OAS_REPO_NAME)) - repoDir, err := createRepoDir(genDir, OAS_REPO, OAS_REPO_NAME, b.SkipClone) + repoDir, err := b.createRepoDir(genDir, OAS_REPO, OAS_REPO_NAME, b.SkipClone) if err != nil { return fmt.Errorf("%s", err.Error()) } - slog.Info("Retrieving versions from subdirs") // TODO - major - verMap, err := getVersions(repoDir) + verMap, err := b.getVersions(repoDir) if err != nil { return fmt.Errorf("%s", err.Error()) } @@ -111,7 +116,7 @@ func (b *Builder) Build() error { } slog.Info("Creating OAS dir") - err = os.MkdirAll(path.Join(genDir, "oas"), 0o755) //nolint:gosec // this dir is not sensitive, so we can use 0755 + err = os.MkdirAll(path.Join(genDir, "oas", "legacy"), 0o755) //nolint:gosec // this dir is not sensitive, so we can use 0755 if err != nil { return err } @@ -131,7 +136,7 @@ func (b *Builder) Build() error { itemVersion, fmt.Sprintf("%s.json", baseService), ) - dstFile := path.Join(genDir, "oas", fmt.Sprintf("%s.json", service)) + dstFile := path.Join(genDir, "oas", "legacy", fmt.Sprintf("%s.json", service)) _, err = copyFile(srcFile, dstFile) if err != nil { return fmt.Errorf("%s", err.Error()) @@ -534,6 +539,7 @@ func generateServiceFiles(rootDir, generatorDir string) error { oasFile := path.Join( generatorDir, "oas", + "legacy", fmt.Sprintf("%s%s.json", service.Name(), svcVersion.Name()), ) if _, oasErr := os.Stat(oasFile); os.IsNotExist(oasErr) { @@ -919,22 +925,32 @@ func getOnlyLatest(m map[string]version) (map[string]version, error) { return tmpMap, nil } -func getVersions(dir string) (map[string]version, error) { +func (b *Builder) getVersions(dir string) (map[string]version, error) { + slog.Info("Retrieving versions from subdirs", "func", "getVersions") + res := make(map[string]version) children, err := os.ReadDir(path.Join(dir, "services")) if err != nil { return nil, err } + if len(children) < 1 { + slog.Error("found no children", "dir", path.Join(dir, "services")) + } for _, entry := range children { if !entry.IsDir() { + slog.Info("entry is no dir", "entry", entry.Name()) continue } + if b.Verbose { + slog.Info("getting versions", "svc", entry.Name()) + } versions, err := os.ReadDir(path.Join(dir, "services", entry.Name())) if err != nil { return nil, err } - m, err2 := extractVersions(entry.Name(), versions) + + m, err2 := b.extractVersions(entry.Name(), versions) if err2 != nil { return m, err2 } @@ -945,8 +961,12 @@ func getVersions(dir string) (map[string]version, error) { return res, nil } -func extractVersions(service string, versionDirs []os.DirEntry) (map[string]version, error) { +func (b *Builder) extractVersions(service string, versionDirs []os.DirEntry) (map[string]version, error) { res := make(map[string]version) + if len(versionDirs) < 1 { + slog.Error("list of version directories is empty") + return nil, nil + } for _, vDir := range versionDirs { if !vDir.IsDir() { continue @@ -954,6 +974,9 @@ func extractVersions(service string, versionDirs []os.DirEntry) (map[string]vers r := regexp.MustCompile(`v(\d+)([a-z]+)(\d*)`) matches := r.FindAllStringSubmatch(vDir.Name(), -1) if matches == nil { + if b.Debug { + slog.Warn("item did not fulfill regex", "item", vDir.Name()) + } continue } svc, ver, err := handleVersion(service, matches[0]) @@ -992,20 +1015,23 @@ func handleVersion(service string, match []string) (*string, *version, error) { return &resStr, &version{verString: verString, major: majVer, minor: minVer}, nil } -func createRepoDir(root, repoUrl, repoName string, skipClone bool) (string, error) { +func (b *Builder) createRepoDir(root, repoUrl, repoName string, skipClone bool) (string, error) { targetDir := path.Join(root, repoName) if !skipClone { if fileExists(targetDir) { slog.Warn("target dir exists - skipping", "targetDir", targetDir) return targetDir, nil } - _, err := git.Clone( + out, err := git.Clone( clone.Repository(repoUrl), clone.Directory(targetDir), ) if err != nil { return "", err } + if b.Verbose { + slog.Info("git clone result", "output", out) + } } return targetDir, nil } diff --git a/cmd/cmd/buildCmd.go b/cmd/cmd/buildCmd.go index e3e8e7ae..0331f147 100644 --- a/cmd/cmd/buildCmd.go +++ b/cmd/cmd/buildCmd.go @@ -10,6 +10,8 @@ var ( skipCleanup bool skipClone bool packagesOnly bool + verbose bool + debug bool ) var buildCmd = &cobra.Command{ @@ -21,6 +23,8 @@ var buildCmd = &cobra.Command{ SkipClone: skipClone, SkipCleanup: skipCleanup, PackagesOnly: packagesOnly, + Verbose: verbose, + Debug: debug, } return b.Build() }, @@ -32,6 +36,8 @@ func NewBuildCmd() *cobra.Command { func init() { //nolint:gochecknoinits // This is the standard way to set up Cobra commands buildCmd.Flags().BoolVarP(&skipCleanup, "skip-clean", "c", false, "Skip cleanup steps") + buildCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable debug output") buildCmd.Flags().BoolVarP(&skipClone, "skip-clone", "g", false, "Skip cloning from git") buildCmd.Flags().BoolVarP(&packagesOnly, "packages-only", "p", false, "Only generate packages") + buildCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "verbose - show more logs") } diff --git a/stackit/internal/services/postgresflexalpha/instance/datasources_gen/instance_data_source_gen.go b/stackit/internal/services/postgresflexalpha/instance/datasources_gen/instance_data_source_gen.go index 844d494e..58f88e01 100644 --- a/stackit/internal/services/postgresflexalpha/instance/datasources_gen/instance_data_source_gen.go +++ b/stackit/internal/services/postgresflexalpha/instance/datasources_gen/instance_data_source_gen.go @@ -28,8 +28,8 @@ func InstanceDataSourceSchema(ctx context.Context) schema.Schema { }, "backup_schedule": schema.StringAttribute{ Computed: true, - Description: "The schedule for on what time and how often the database backup will be created. The schedule is written as a cron schedule.", - MarkdownDescription: "The schedule for on what time and how often the database backup will be created. The schedule is written as a cron schedule.", + Description: "The schedule for when the database backup will be created. Currently, ONLY daily schedules are supported (every 24 hours). The schedule is written as a cron schedule.", + MarkdownDescription: "The schedule for when the database backup will be created. Currently, ONLY daily schedules are supported (every 24 hours). The schedule is written as a cron schedule.", }, "connection_info": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ diff --git a/stackit/internal/services/postgresflexalpha/instance/resources_gen/instance_resource_gen.go b/stackit/internal/services/postgresflexalpha/instance/resources_gen/instance_resource_gen.go index 25ad092b..7d7969a6 100644 --- a/stackit/internal/services/postgresflexalpha/instance/resources_gen/instance_resource_gen.go +++ b/stackit/internal/services/postgresflexalpha/instance/resources_gen/instance_resource_gen.go @@ -30,8 +30,8 @@ func InstanceResourceSchema(ctx context.Context) schema.Schema { }, "backup_schedule": schema.StringAttribute{ Required: true, - Description: "The schedule for on what time and how often the database backup will be created. The schedule is written as a cron schedule.", - MarkdownDescription: "The schedule for on what time and how often the database backup will be created. The schedule is written as a cron schedule.", + Description: "The schedule for when the database backup will be created. Currently, ONLY daily schedules are supported (every 24 hours). The schedule is written as a cron schedule.", + MarkdownDescription: "The schedule for when the database backup will be created. Currently, ONLY daily schedules are supported (every 24 hours). The schedule is written as a cron schedule.", }, "connection_info": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ diff --git a/stackit/internal/services/postgresflexalpha/postgresflex_acc_test.go b/stackit/internal/services/postgresflexalpha/postgresflex_acc_test.go index 7b16f03d..800b862b 100644 --- a/stackit/internal/services/postgresflexalpha/postgresflex_acc_test.go +++ b/stackit/internal/services/postgresflexalpha/postgresflex_acc_test.go @@ -9,13 +9,18 @@ import ( "strconv" "strings" "testing" + "time" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/utils" postgresflexalphaPkgGen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" + "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 @@ -26,52 +31,6 @@ import ( const pfx = "stackitprivatepreview_postgresflexalpha" -var testInstances []string - -func init() { - sweeperName := fmt.Sprintf("%s_%s", pfx, "sweeper") - resource.AddTestSweepers( - sweeperName, &resource.Sweeper{ - Name: sweeperName, - F: func(_ string) error { // region is passed by the testing framework - ctx := context.Background() - apiClientConfigOptions := []config.ConfigurationOption{} - apiClient, err := postgresflexalphaPkgGen.NewAPIClient(apiClientConfigOptions...) - if err != nil { - log.Fatalln(err) - } - - instances, err := apiClient.ListInstancesRequest(ctx, testutils.ProjectId, testutils.Region). - Size(100). - Execute() - if err != nil { - log.Fatalln(err) - } - - for _, inst := range instances.GetInstances() { - if strings.HasPrefix(inst.GetName(), "tf-acc-") { - for _, item := range testInstances { - if inst.GetName() == item { - delErr := apiClient.DeleteInstanceRequestExecute( - ctx, - testutils.ProjectId, - testutils.Region, - inst.GetId(), - ) - if delErr != nil { - // TODO: maybe just warn? - log.Fatalln(delErr) - } - } - } - } - } - return nil - }, - }, - ) -} - func TestInstanceResourceSchema(t *testing.T) { t.Parallel() @@ -115,27 +74,6 @@ func testAccPreCheck(t *testing.T) { } } -// func TestAccResourceExample_parallel(t *testing.T) { -// t.Parallel() -// -// exData := resData{ -// Region: "eu01", -// ServiceAccountFilePath: sa_file, -// ProjectID: project_id, -// Name: acctest.RandomWithPrefix("tf-acc"), -// } -// -// resource.Test(t, resource.TestCase{ -// ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories, -// Steps: []resource.TestStep{ -// { -// Config: testAccResourceEncryptionExampleConfig(exData), -// Check: resource.TestCheckResourceAttrSet("example_resource.test", "id"), -// }, -// }, -// }) -//} - type resData struct { ServiceAccountFilePath string ProjectId string @@ -202,28 +140,53 @@ func TestAccInstance(t *testing.T) { updSizeData := exData updSizeData.Size = 25 + updBackupSched := exData + updBackupSched.BackupSchedule = "0 0 1 * *" + + /* + { + "backupSchedule": "6 6 * * *", + "flavorId": "1.2", + "name": "postgres-instance", + "network": { + "acl": [ + "198.51.100.0/24" + ] + }, + "replicas": 1, + "retentionDays": 35, + "storage": { + "size": 10 + }, + "version": "string" + } + */ + + var testItemID string resource.ParallelTest( t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) t.Logf(" ... working on instance %s", exData.TfName) - testInstances = append(testInstances, exData.TfName) }, + CheckDestroy: testAccCheckPostgresFlexDestroy, ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Create and verify { + PreConfig: func() { + testItemID = testutils.ResStr(pfx, "instance", exData.TfName) + }, Config: testutils.StringFromTemplateMust( "testdata/instance_template.gompl", exData, ), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr( - testutils.ResStr(pfx, "instance", exData.TfName), - "name", - exData.Name, - ), - resource.TestCheckResourceAttrSet(testutils.ResStr(pfx, "instance", exData.TfName), "id"), + // check params are set + resource.TestCheckResourceAttrSet(testItemID, "id"), + + // check param values + resource.TestCheckResourceAttr(testItemID, "name", exData.Name), ), }, // Update name and verify @@ -254,6 +217,20 @@ func TestAccInstance(t *testing.T) { ), ), }, + // Update backup schedule + { + Config: testutils.StringFromTemplateMust( + "testdata/instance_template.gompl", + updBackupSched, + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + testutils.ResStr(pfx, "instance", exData.TfName), + "backup_schedule", + updBackupSched.BackupSchedule, + ), + ), + }, //// Import test //{ // ResourceName: "example_resource.test", @@ -284,6 +261,7 @@ func TestAccInstanceWithUsers(t *testing.T) { t.Logf(" ... working on instance %s", data.TfName) testInstances = append(testInstances, data.TfName) }, + CheckDestroy: testAccCheckPostgresFlexDestroy, ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Create and verify @@ -336,6 +314,7 @@ func TestAccInstanceWithDatabases(t *testing.T) { t.Logf(" ... working on instance %s", data.TfName) testInstances = append(testInstances, data.TfName) }, + CheckDestroy: testAccCheckPostgresFlexDestroy, ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Create and verify @@ -1015,3 +994,80 @@ func TestAccInstanceWithDatabases(t *testing.T) { // } // return nil //} + +func testAccCheckPostgresFlexDestroy(s *terraform.State) error { + ctx := context.Background() + var client *postgresflexalphaPkgGen.APIClient + var err error + + var region, projectId string + region = testutils.Region + if region == "" { + region = "eu01" + } + + projectId = testutils.ProjectId + 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 = postgresflexalphaPkgGen.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.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 == nil { + continue + } + if utils.Contains(instancesToDestroy, *items[i].Id) { + err := client.DeleteInstanceRequestExecute(ctx, testutils.ProjectId, region, *items[i].Id) + if err != nil { + return fmt.Errorf("deleting instance %s during CheckDestroy: %w", *items[i].Id, err) + } + err = postgresflexalpha.DeleteInstanceWaitHandler( + ctx, + client, + 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 +} diff --git a/stackit/internal/wait/postgresflexalpha/wait.go b/stackit/internal/wait/postgresflexalpha/wait.go index 4aea71e8..8d72f587 100644 --- a/stackit/internal/wait/postgresflexalpha/wait.go +++ b/stackit/internal/wait/postgresflexalpha/wait.go @@ -25,6 +25,7 @@ const ( InstanceStateTerminating = "TERMINATING" InstanceStateUnknown = "UNKNOWN" InstanceStatePending = "PENDING" + InstanceStateDeleted = "DELETED" ) // APIClientInstanceInterface Interface needed for tests @@ -296,3 +297,44 @@ func GetDatabaseByIdWaitHandler( ) return handler } + +func DeleteInstanceWaitHandler( + ctx context.Context, + a APIClientInstanceInterface, + projectId, + region, + instanceId string, + timeout, sleepBeforeWait time.Duration, +) error { + handler := wait.New( + func() (waitFinished bool, response *postgresflex.GetInstanceResponse, err error) { + s, err := a.GetInstanceRequestExecute(ctx, projectId, region, instanceId) + if err != nil { + oapiErr, ok := err.(*oapierror.GenericOpenAPIError) // nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped + if !ok { + return false, nil, fmt.Errorf("received error is no oapierror: %w", err) + } + if oapiErr.StatusCode == 404 { + return true, nil, nil + } + return false, nil, fmt.Errorf("api returned error: %w", err) + } + switch *s.Status { + case InstanceStateDeleted: + return true, nil, nil + case InstanceStateEmpty, InstanceStatePending, InstanceStateUnknown, InstanceStateProgressing, InstanceStateSuccess: + return false, nil, nil + case InstanceStateFailed: + return true, nil, fmt.Errorf("wait handler got status FAILURE for instance: %s", instanceId) + default: + return true, s, fmt.Errorf("instance with id %s has unexpected status %s", instanceId, *s.Status) + } + }, + ).SetTimeout(timeout).SetSleepBeforeWait(sleepBeforeWait) + + _, err := handler.WaitWithContext(ctx) + if err != nil { + return err + } + return nil +}