From d8c63f848a71fd54662808e2fe8d797ac82c2d18 Mon Sep 17 00:00:00 2001 From: "Marcel S. Henselin" Date: Thu, 5 Feb 2026 16:08:16 +0100 Subject: [PATCH 1/2] fix: add databases generated file feat: add useful command --- cmd/cmd/build/build.go | 58 ++++++- .../templates/data_source_scaffold.gotmpl | 67 +++++--- cmd/cmd/getFieldsCmd.go | 147 +++++++++++++++++ cmd/main.go | 1 + .../sqlserverflex/beta/database_config.yml | 11 +- ...ion_config.yml.bak => versions_config.yml} | 0 .../sqlserverflexalpha/user/resource.go | 10 +- .../sqlserverflexbeta/database/datasource.go | 150 ------------------ ...rce_gen.go => database_data_source_fix.go} | 7 +- .../sqlserverflexbeta/database/resource.go | 115 +++++++++----- .../sqlserverflexbeta/flavor/datasource.go | 46 ++++-- .../internal/wait/sqlserverflexalpha/wait.go | 38 +++++ 12 files changed, 418 insertions(+), 232 deletions(-) create mode 100644 cmd/cmd/getFieldsCmd.go rename service_specs/sqlserverflex/beta/{version_config.yml.bak => versions_config.yml} (100%) delete mode 100644 stackit/internal/services/sqlserverflexbeta/database/datasource.go rename stackit/internal/services/sqlserverflexbeta/database/datasources_gen/{database_data_source_gen.go => database_data_source_fix.go} (95%) diff --git a/cmd/cmd/build/build.go b/cmd/cmd/build/build.go index 3351d5da..a6e0acf9 100644 --- a/cmd/cmd/build/build.go +++ b/cmd/cmd/build/build.go @@ -4,6 +4,9 @@ import ( "bytes" "errors" "fmt" + "go/ast" + "go/parser" + "go/token" "io" "log" "log/slog" @@ -260,6 +263,7 @@ type templateData struct { NameCamel string NamePascal string NameSnake string + Fields []string } func fileExists(path string) bool { @@ -317,11 +321,16 @@ func createBoilerplate(rootFolder, folder string) error { return errors.New("resource name is invalid") } + fields, tokenErr := getTokens(dsFile) + if tokenErr != nil { + return fmt.Errorf("error reading tokens: %w", tokenErr) + } + tplName := "data_source_scaffold.gotmpl" err = writeTemplateToFile( tplName, path.Join(rootFolder, "cmd", "cmd", "build", "templates", tplName), - path.Join(folder, svc.Name(), res.Name(), "datasource.go"), + dsGoFile, &templateData{ PackageName: svc.Name(), PackageNameCamel: ToCamelCase(svc.Name()), @@ -329,6 +338,7 @@ func createBoilerplate(rootFolder, folder string) error { NameCamel: ToCamelCase(resourceName), NamePascal: ToPascalCase(resourceName), NameSnake: resourceName, + Fields: fields, }, ) if err != nil { @@ -342,11 +352,16 @@ func createBoilerplate(rootFolder, folder string) error { return errors.New("resource name is invalid") } + fields, tokenErr := getTokens(resFile) + if tokenErr != nil { + return fmt.Errorf("error reading tokens: %w", tokenErr) + } + tplName := "resource_scaffold.gotmpl" err = writeTemplateToFile( tplName, path.Join(rootFolder, "cmd", "cmd", "build", "templates", tplName), - path.Join(folder, svc.Name(), res.Name(), "resource.go"), + resGoFile, &templateData{ PackageName: svc.Name(), PackageNameCamel: ToCamelCase(svc.Name()), @@ -354,6 +369,7 @@ func createBoilerplate(rootFolder, folder string) error { NameCamel: ToCamelCase(resourceName), NamePascal: ToPascalCase(resourceName), NameSnake: resourceName, + Fields: fields, }, ) if err != nil { @@ -813,3 +829,41 @@ func getRoot() (*string, error) { lines := strings.Split(string(out), "\n") return &lines[0], nil } + +func getTokens(fileName string) ([]string, error) { + fset := token.NewFileSet() + + var result []string + + node, err := parser.ParseFile(fset, fileName, nil, parser.ParseComments) + if err != nil { + return nil, err + } + + ast.Inspect(node, func(n ast.Node) bool { + // Suche nach Typ-Deklarationen (structs) + ts, ok := n.(*ast.TypeSpec) + if ok { + if strings.Contains(ts.Name.Name, "Model") { + // fmt.Printf("found model: %s\n", ts.Name.Name) + ast.Inspect(ts, func(sn ast.Node) bool { + tts, tok := sn.(*ast.Field) + if tok { + // fmt.Printf(" found: %+v\n", tts.Names[0]) + // spew.Dump(tts.Type) + + result = append(result, tts.Names[0].String()) + + //fld, fldOk := tts.Type.(*ast.Ident) + //if fldOk { + // fmt.Printf("type: %+v\n", fld) + //} + } + return true + }) + } + } + return true + }) + return result, nil +} diff --git a/cmd/cmd/build/templates/data_source_scaffold.gotmpl b/cmd/cmd/build/templates/data_source_scaffold.gotmpl index 74fc0f91..ba4e8095 100644 --- a/cmd/cmd/build/templates/data_source_scaffold.gotmpl +++ b/cmd/cmd/build/templates/data_source_scaffold.gotmpl @@ -6,15 +6,16 @@ import ( "net/http" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/config" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" {{.PackageName}}Pkg "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/{{.PackageName}}" - {{.PackageName}}Utils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/{{.PackageName}}/utils" - {{.PackageName}}Gen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/{{.PackageName}}/{{.NameSnake}}/datasources_gen" ) @@ -26,6 +27,11 @@ func New{{.NamePascal}}DataSource() datasource.DataSource { return &{{.NameCamel}}DataSource{} } +type dsModel struct { + {{.PackageName}}Gen.{{.NamePascal}}Model + TfId types.String `tfsdk:"id"` +} + type {{.NameCamel}}DataSource struct{ client *{{.PackageName}}Pkg.APIClient providerData core.ProviderData @@ -37,6 +43,11 @@ func (d *{{.NameCamel}}DataSource) Metadata(_ context.Context, req datasource.Me func (d *{{.NameCamel}}DataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = {{.PackageName}}Gen.{{.NamePascal}}DataSourceSchema(ctx) + resp.Schema.Attributes["id"] = schema.StringAttribute{ + Computed: true, + Description: "The terraform internal identifier.", + MarkdownDescription: "The terraform internal identifier.", + } } // Configure adds the provider configured client to the data source. @@ -51,8 +62,30 @@ func (d *{{.NameCamel}}DataSource) Configure( return } - apiClient := {{.PackageName}}Utils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { + apiClientConfigOptions := []config.ConfigurationOption{ + config.WithCustomAuth(d.providerData.RoundTripper), + utils.UserAgentConfigOption(d.providerData.Version), + } + if d.providerData.{{.PackageNamePascal}}CustomEndpoint != "" { + apiClientConfigOptions = append( + apiClientConfigOptions, + config.WithEndpoint(d.providerData.{{.PackageNamePascal}}CustomEndpoint), + ) + } else { + apiClientConfigOptions = append( + apiClientConfigOptions, + config.WithRegion(d.providerData.GetRegion()), + ) + } + apiClient, err := {{.PackageName}}Pkg.NewAPIClient(apiClientConfigOptions...) + if err != nil { + resp.Diagnostics.AddError( + "Error configuring API client", + fmt.Sprintf( + "Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", + err, + ), + ) return } d.client = apiClient @@ -60,7 +93,7 @@ func (d *{{.NameCamel}}DataSource) Configure( } func (d *{{.NameCamel}}DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var data {{.PackageName}}Gen.{{.NamePascal}}Model + var data dsModel // Read Terraform configuration data into the model resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) @@ -77,8 +110,11 @@ func (d *{{.NameCamel}}DataSource) Read(ctx context.Context, req datasource.Read ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) + + // TODO: implement needed fields ctx = tflog.SetField(ctx, "{{.NameCamel}}_id", {{.NameCamel}}Id) + // TODO: refactor to correct implementation {{.NameCamel}}Resp, err := d.client.Get{{.NamePascal}}Request(ctx, projectId, region, {{.NameCamel}}Id).Execute() if err != nil { utils.LogError( @@ -98,22 +134,15 @@ func (d *{{.NameCamel}}DataSource) Read(ctx context.Context, req datasource.Read ctx = core.LogResponse(ctx) - // Todo: Read API call logic + data.TfId = utils.BuildInternalTerraformId(projectId, region, ..) - // Example data value setting - // data.Id = types.StringValue("example-id") - - err = mapResponseToModel(ctx, {{.NameCamel}}Resp, &data, resp.Diagnostics) - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - fmt.Sprintf("%s Read", errorPrefix), - fmt.Sprintf("Processing API payload: %v", err), - ) - return - } + // TODO: fill remaining fields +{{- range .Fields }} + // data.{{.}} = types.Sometype(apiResponse.Get{{.}}()) +{{- end -}} // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + + tflog.Info(ctx, fmt.Sprintf("%s read successful", errorPrefix)) } diff --git a/cmd/cmd/getFieldsCmd.go b/cmd/cmd/getFieldsCmd.go new file mode 100644 index 00000000..f24db87d --- /dev/null +++ b/cmd/cmd/getFieldsCmd.go @@ -0,0 +1,147 @@ +package cmd + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "path" + "path/filepath" + "strings" + + "github.com/spf13/cobra" +) + +var ( + inFile string + svcName string + resName string + resType string + filePath string +) + +var getFieldsCmd = &cobra.Command{ + Use: "get-fields", + Short: "get fields from file", + Long: `...`, + PreRunE: func(cmd *cobra.Command, args []string) error { + typeStr := "data_source" + if resType != "resource" && resType != "datasource" { + return fmt.Errorf("--type can only be resource or datasource") + } + + if resType == "resource" { + typeStr = resType + } + + if inFile == "" && svcName == "" && resName == "" { + return fmt.Errorf("--infile or --service and --resource must be provided") + } + + if inFile != "" { + if svcName != "" || resName != "" { + return fmt.Errorf("--infile is provided and excludes --service and --resource") + } + p, err := filepath.Abs(inFile) + if err != nil { + return err + } + filePath = p + return nil + } + + if svcName != "" && resName == "" { + return fmt.Errorf("if --service is provided, you MUST also provide --resource") + } + + if svcName == "" && resName != "" { + return fmt.Errorf("if --resource is provided, you MUST also provide --service") + } + + p, err := filepath.Abs( + path.Join( + "stackit", + "internal", + "services", + svcName, + resName, + fmt.Sprintf("%ss_gen", resType), + fmt.Sprintf("%s_%s_gen.go", resName, typeStr), + ), + ) + if err != nil { + return err + } + filePath = p + + //// Enum check + //switch format { + //case "json", "yaml": + //default: + // return fmt.Errorf("invalid --format: %s (want json|yaml)", format) + //} + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return getFields(filePath) + }, +} + +func getFields(f string) error { + tokens, err := getTokens(f) + if err != nil { + return err + } + for _, item := range tokens { + fmt.Printf("%s \n", item) + } + return nil +} + +func getTokens(fileName string) ([]string, error) { + fset := token.NewFileSet() + var result []string + + node, err := parser.ParseFile(fset, fileName, nil, parser.ParseComments) + if err != nil { + return nil, err + } + + ast.Inspect(node, func(n ast.Node) bool { + // Suche nach Typ-Deklarationen (structs) + ts, ok := n.(*ast.TypeSpec) + if ok { + if strings.Contains(ts.Name.Name, "Model") { + // fmt.Printf("found model: %s\n", ts.Name.Name) + ast.Inspect(ts, func(sn ast.Node) bool { + tts, tok := sn.(*ast.Field) + if tok { + // fmt.Printf(" found: %+v\n", tts.Names[0]) + // spew.Dump(tts.Type) + + result = append(result, tts.Names[0].String()) + + //fld, fldOk := tts.Type.(*ast.Ident) + //if fldOk { + // fmt.Printf("type: %+v\n", fld) + //} + } + return true + }) + } + } + return true + }) + return result, nil +} + +func NewGetFieldsCmd() *cobra.Command { + return getFieldsCmd +} + +func init() { // nolint: gochecknoinits + getFieldsCmd.Flags().StringVarP(&inFile, "infile", "i", "", "input filename incl path") + getFieldsCmd.Flags().StringVarP(&svcName, "service", "s", "", "service name") + getFieldsCmd.Flags().StringVarP(&resName, "resource", "r", "", "resource name") + getFieldsCmd.Flags().StringVarP(&resType, "type", "t", "resource", "resource type (data-source or resource [default])") +} diff --git a/cmd/main.go b/cmd/main.go index 52753a18..1b80b034 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -28,6 +28,7 @@ func main() { rootCmd.AddCommand( cmd.NewBuildCmd(), cmd.NewPublishCmd(), + cmd.NewGetFieldsCmd(), ) err := rootCmd.Execute() diff --git a/service_specs/sqlserverflex/beta/database_config.yml b/service_specs/sqlserverflex/beta/database_config.yml index d886fc20..135010d2 100644 --- a/service_specs/sqlserverflex/beta/database_config.yml +++ b/service_specs/sqlserverflex/beta/database_config.yml @@ -1,4 +1,3 @@ - provider: name: stackitprivatepreview @@ -7,7 +6,7 @@ resources: schema: attributes: aliases: - id: databaseId + databaseId: id create: path: /v3beta1/projects/{projectId}/regions/{region}/instances/{instanceId}/databases method: POST @@ -18,7 +17,6 @@ resources: path: /v3beta1/projects/{projectId}/regions/{region}/instances/{instanceId}/databases/{databaseName} method: DELETE - data_sources: databases: read: @@ -26,9 +24,10 @@ data_sources: method: GET database: - attributes: - aliases: - id: database_id + schema: + attributes: + aliases: + databaseId: id read: path: /v3beta1/projects/{projectId}/regions/{region}/instances/{instanceId}/databases/{databaseName} method: GET diff --git a/service_specs/sqlserverflex/beta/version_config.yml.bak b/service_specs/sqlserverflex/beta/versions_config.yml similarity index 100% rename from service_specs/sqlserverflex/beta/version_config.yml.bak rename to service_specs/sqlserverflex/beta/versions_config.yml diff --git a/stackit/internal/services/sqlserverflexalpha/user/resource.go b/stackit/internal/services/sqlserverflexalpha/user/resource.go index 2d3978c4..c5cea986 100644 --- a/stackit/internal/services/sqlserverflexalpha/user/resource.go +++ b/stackit/internal/services/sqlserverflexalpha/user/resource.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha" sqlserverflexalphaUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/utils" + sqlserverflexalphaWait "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/wait/sqlserverflexalpha" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -405,9 +406,14 @@ func (r *userResource) Delete( ctx = tflog.SetField(ctx, "region", region) // Delete existing record set - err := r.client.DeleteUserRequest(ctx, projectId, region, instanceId, userId).Execute() + // err := r.client.DeleteUserRequest(ctx, projectId, region, instanceId, userId).Execute() + + // Delete existing record set + _, err := sqlserverflexalphaWait.DeleteUserWaitHandler(ctx, r.client, projectId, region, instanceId, userId). + WaitWithContext(ctx) + //err := r.client.DeleteUserRequest(ctx, arg.projectId, arg.region, arg.instanceId, userId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "User Delete Error", fmt.Sprintf("Calling API: %v", err)) return } diff --git a/stackit/internal/services/sqlserverflexbeta/database/datasource.go b/stackit/internal/services/sqlserverflexbeta/database/datasource.go deleted file mode 100644 index 70fbaca4..00000000 --- a/stackit/internal/services/sqlserverflexbeta/database/datasource.go +++ /dev/null @@ -1,150 +0,0 @@ -package sqlserverflexbeta - -import ( - "context" - "fmt" - "net/http" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" - - sqlserverflexbetaPkg "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta" - sqlserverflexbetaGen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexbeta/database/datasources_gen" -) - -var _ datasource.DataSource = (*databaseDataSource)(nil) - -const errorPrefix = "[Sqlserverflexbeta - Database]" - -func NewDatabaseDataSource() datasource.DataSource { - return &databaseDataSource{} -} - -type databaseDataSource struct { - client *sqlserverflexbetaPkg.APIClient - providerData core.ProviderData -} - -func (d *databaseDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_sqlserverflexbeta_database" -} - -func (d *databaseDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = sqlserverflexbetaGen.DatabaseDataSourceSchema(ctx) -} - -// Configure adds the provider configured client to the data source. -func (d *databaseDataSource) Configure( - ctx context.Context, - req datasource.ConfigureRequest, - resp *datasource.ConfigureResponse, -) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(d.providerData.RoundTripper), - utils.UserAgentConfigOption(d.providerData.Version), - } - if d.providerData.SQLServerFlexCustomEndpoint != "" { - apiClientConfigOptions = append( - apiClientConfigOptions, - config.WithEndpoint(d.providerData.SQLServerFlexCustomEndpoint), - ) - } else { - apiClientConfigOptions = append( - apiClientConfigOptions, - config.WithRegion(d.providerData.GetRegion()), - ) - } - apiClient, err := sqlserverflexbetaPkg.NewAPIClient(apiClientConfigOptions...) - if err != nil { - resp.Diagnostics.AddError( - "Error configuring API client", - fmt.Sprintf( - "Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", - err, - ), - ) - return - } - d.client = apiClient - tflog.Info(ctx, fmt.Sprintf("%s client configured", errorPrefix)) -} - -func (d *databaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var data sqlserverflexbetaGen.DatabaseModel - readErr := "Read DB error" - - // Read Terraform configuration data into the model - resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := data.ProjectId.ValueString() - region := d.providerData.GetRegionWithOverride(data.Region) - instanceId := data.InstanceId.ValueString() - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - databaseName := data.DatabaseName.ValueString() - - databaseResp, err := d.client.GetDatabaseRequest(ctx, projectId, region, instanceId, databaseName).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading database", - fmt.Sprintf("database with %q does not exist in project %q.", databaseName, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - dbId, ok := databaseResp.GetIdOk() - if !ok { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - readErr, - "Database creation waiting: returned id is nil", - ) - return - } - data.Id = types.Int64Value(dbId) - - owner, ok := databaseResp.GetOwnerOk() - if !ok { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - readErr, - "Database creation waiting: returned owner is nil", - ) - return - } - data.Owner = types.StringValue(owner) - - // Save data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) -} diff --git a/stackit/internal/services/sqlserverflexbeta/database/datasources_gen/database_data_source_gen.go b/stackit/internal/services/sqlserverflexbeta/database/datasources_gen/database_data_source_fix.go similarity index 95% rename from stackit/internal/services/sqlserverflexbeta/database/datasources_gen/database_data_source_gen.go rename to stackit/internal/services/sqlserverflexbeta/database/datasources_gen/database_data_source_fix.go index cfdc1a86..92b1064e 100644 --- a/stackit/internal/services/sqlserverflexbeta/database/datasources_gen/database_data_source_gen.go +++ b/stackit/internal/services/sqlserverflexbeta/database/datasources_gen/database_data_source_fix.go @@ -29,7 +29,7 @@ func DatabaseDataSourceSchema(ctx context.Context) schema.Schema { Description: "The name of the database.", MarkdownDescription: "The name of the database.", }, - "id": schema.Int64Attribute{ + "tf_original_api_id": schema.Int64Attribute{ Computed: true, Description: "The id of the database.", MarkdownDescription: "The id of the database.", @@ -55,8 +55,7 @@ func DatabaseDataSourceSchema(ctx context.Context) schema.Schema { MarkdownDescription: "The STACKIT project ID.", }, "region": schema.StringAttribute{ - Optional: true, - Computed: true, + Required: true, Description: "The region which should be addressed", MarkdownDescription: "The region which should be addressed", Validators: []validator.String{ @@ -73,7 +72,7 @@ type DatabaseModel struct { CollationName types.String `tfsdk:"collation_name"` CompatibilityLevel types.Int64 `tfsdk:"compatibility_level"` DatabaseName types.String `tfsdk:"database_name"` - Id types.Int64 `tfsdk:"id"` + Id types.Int64 `tfsdk:"tf_original_api_id"` InstanceId types.String `tfsdk:"instance_id"` Name types.String `tfsdk:"name"` Owner types.String `tfsdk:"owner"` diff --git a/stackit/internal/services/sqlserverflexbeta/database/resource.go b/stackit/internal/services/sqlserverflexbeta/database/resource.go index b28f5ea0..5ae1d6c4 100644 --- a/stackit/internal/services/sqlserverflexbeta/database/resource.go +++ b/stackit/internal/services/sqlserverflexbeta/database/resource.go @@ -244,6 +244,7 @@ func (r *databaseResource) Create(ctx context.Context, req resource.CreateReques func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data sqlserverflexbetaResGen.DatabaseModel + readErr := "[Database Read]" // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -265,22 +266,34 @@ func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, r ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) - // Todo: Read API call logic + instanceId := identityData.InstanceID.ValueString() + ctx = tflog.SetField(ctx, "instance_id", instanceId) - // Save updated data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + dbName := identityData.DatabaseName.ValueString() + ctx = tflog.SetField(ctx, "database_name", dbName) - // TODO: Set data returned by API in identity - identity := DatabaseResourceIdentityModel{ - ProjectID: types.StringValue(projectId), - Region: types.StringValue(region), - // InstanceID: types.StringValue(instanceId), - } - resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...) - if resp.Diagnostics.HasError() { + getResp, err := r.client.GetDatabaseRequest(ctx, projectId, region, instanceId, dbName).Execute() + if err != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + readErr, + fmt.Sprintf("Calling API: %v", err), + ) return } + ctx = core.LogResponse(ctx) + + data.Id = types.Int64Value(getResp.GetId()) + data.Owner = types.StringValue(getResp.GetOwner()) + data.Name = types.StringValue(getResp.GetName()) + data.DatabaseName = types.StringValue(getResp.GetName()) + data.CollationName = types.StringValue(getResp.GetCollationName()) + data.CompatibilityLevel = types.Int64Value(getResp.GetCompatibilityLevel()) + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) tflog.Info(ctx, "sqlserverflexbeta.Database read") } @@ -350,11 +363,13 @@ func (r *databaseResource) ModifyPlan( req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse, ) { // nolint:gocritic // function signature required by Terraform - var configModel sqlserverflexbetaResGen.DatabaseModel + // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + + var configModel sqlserverflexbetaResGen.DatabaseModel resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) if resp.Diagnostics.HasError() { return @@ -374,10 +389,14 @@ func (r *databaseResource) ModifyPlan( var identityModel DatabaseResourceIdentityModel identityModel.ProjectID = planModel.ProjectId identityModel.Region = planModel.Region - // TODO: complete - //if !planModel.InstanceId.IsNull() && !planModel.InstanceId.IsUnknown() { - // identityModel.InstanceID = planModel.InstanceId - //} + + if !planModel.InstanceId.IsNull() && !planModel.InstanceId.IsUnknown() { + identityModel.InstanceID = planModel.InstanceId + } + + if !planModel.Name.IsNull() && !planModel.Name.IsUnknown() { + identityModel.DatabaseName = planModel.Name + } resp.Diagnostics.Append(resp.Identity.Set(ctx, identityModel)...) if resp.Diagnostics.HasError() { @@ -397,30 +416,54 @@ func (r *databaseResource) ImportState( req resource.ImportStateRequest, resp *resource.ImportStateResponse, ) { - idParts := strings.Split(req.ID, core.Separator) + ctx = core.InitProviderContext(ctx) - // Todo: Import logic - if len(idParts) < 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError( - ctx, &resp.Diagnostics, - "Error importing database", - fmt.Sprintf( - "Expected import identifier with format [project_id],[region],..., got %q", - req.ID, - ), - ) + if req.ID != "" { + idParts := strings.Split(req.ID, core.Separator) + + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing database", + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[instance_id],[database_name] Got: %q", req.ID), + ) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("database_name"), idParts[3])...) + + var identityData DatabaseResourceIdentityModel + identityData.ProjectID = types.StringValue(idParts[0]) + identityData.Region = types.StringValue(idParts[1]) + identityData.InstanceID = types.StringValue(idParts[2]) + identityData.DatabaseName = types.StringValue(idParts[3]) + + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Info(ctx, "Sqlserverflexbeta database state imported") return } - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - // ... more ... + var identityData DatabaseResourceIdentityModel + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), identityData.ProjectID.ValueString())...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), identityData.Region.ValueString())...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), identityData.InstanceID.ValueString())...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("database_name"), identityData.DatabaseName.ValueString())...) + + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) + if resp.Diagnostics.HasError() { + return + } - core.LogAndAddWarning( - ctx, - &resp.Diagnostics, - "Sqlserverflexbeta database imported with empty password", - "The database password is not imported as it is only available upon creation of a new database. The password field will be empty.", - ) tflog.Info(ctx, "Sqlserverflexbeta database state imported") } diff --git a/stackit/internal/services/sqlserverflexbeta/flavor/datasource.go b/stackit/internal/services/sqlserverflexbeta/flavor/datasource.go index 55f12a9a..59f3982c 100644 --- a/stackit/internal/services/sqlserverflexbeta/flavor/datasource.go +++ b/stackit/internal/services/sqlserverflexbeta/flavor/datasource.go @@ -92,17 +92,47 @@ func (r *flavorDataSource) Configure(ctx context.Context, req datasource.Configu return } r.client = apiClient - tflog.Info(ctx, "Postgres Flex instance client configured") + tflog.Info(ctx, "SQL Server Flex instance client configured") } func (r *flavorDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{ + Required: true, + Description: "The project ID of the flavor.", + MarkdownDescription: "The project ID of the flavor.", + }, + "region": schema.StringAttribute{ + Required: true, + Description: "The region of the flavor.", + MarkdownDescription: "The region of the flavor.", + }, "cpu": schema.Int64Attribute{ - Computed: true, + Required: true, Description: "The cpu count of the instance.", MarkdownDescription: "The cpu count of the instance.", }, + "ram": schema.Int64Attribute{ + Required: true, + Description: "The memory of the instance in Gibibyte.", + MarkdownDescription: "The memory of the instance in Gibibyte.", + }, + "storage_class": schema.StringAttribute{ + Required: true, + Description: "The memory of the instance in Gibibyte.", + MarkdownDescription: "The memory of the instance in Gibibyte.", + }, + "node_type": schema.StringAttribute{ + Required: true, + Description: "defines the nodeType it can be either single or HA", + MarkdownDescription: "defines the nodeType it can be either single or HA", + }, + "flavor_id": schema.StringAttribute{ + Computed: true, + Description: "The id of the instance flavor.", + MarkdownDescription: "The id of the instance flavor.", + }, "description": schema.StringAttribute{ Computed: true, Description: "The flavor description.", @@ -118,21 +148,11 @@ func (r *flavorDataSource) Schema(ctx context.Context, _ datasource.SchemaReques Description: "maximum storage which can be ordered for the flavor in Gigabyte.", MarkdownDescription: "maximum storage which can be ordered for the flavor in Gigabyte.", }, - "memory": schema.Int64Attribute{ - Computed: true, - Description: "The memory of the instance in Gibibyte.", - MarkdownDescription: "The memory of the instance in Gibibyte.", - }, "min_gb": schema.Int64Attribute{ Computed: true, Description: "minimum storage which is required to order in Gigabyte.", MarkdownDescription: "minimum storage which is required to order in Gigabyte.", }, - "node_type": schema.StringAttribute{ - Computed: true, - Description: "defines the nodeType it can be either single or HA", - MarkdownDescription: "defines the nodeType it can be either single or HA", - }, "storage_classes": schema.ListNestedAttribute{ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ @@ -331,5 +351,5 @@ func (r *flavorDataSource) Read(ctx context.Context, req datasource.ReadRequest, if resp.Diagnostics.HasError() { return } - tflog.Info(ctx, "Postgres Flex flavors read") + tflog.Info(ctx, "SQL Server Flex flavors read") } diff --git a/stackit/internal/wait/sqlserverflexalpha/wait.go b/stackit/internal/wait/sqlserverflexalpha/wait.go index 21bb8f70..542b06cb 100644 --- a/stackit/internal/wait/sqlserverflexalpha/wait.go +++ b/stackit/internal/wait/sqlserverflexalpha/wait.go @@ -32,6 +32,11 @@ type APIClientInstanceInterface interface { GetInstanceRequestExecute(ctx context.Context, projectId, region, instanceId string) (*sqlserverflex.GetInstanceResponse, error) } +// APIClientUserInterface Interface needed for tests +type APIClientUserInterface interface { + DeleteUserRequestExecute(ctx context.Context, projectId string, region string, instanceId string, userId int64) error +} + // CreateInstanceWaitHandler will wait for instance creation func CreateInstanceWaitHandler(ctx context.Context, a APIClientInstanceInterface, projectId, instanceId, region string) *wait.AsyncActionHandler[sqlserverflex.GetInstanceResponse] { handler := wait.New(func() (waitFinished bool, response *sqlserverflex.GetInstanceResponse, err error) { @@ -131,3 +136,36 @@ func DeleteInstanceWaitHandler(ctx context.Context, a APIClientInstanceInterface handler.SetTimeout(15 * time.Minute) return handler } + +// DeleteUserWaitHandler will wait for instance deletion +func DeleteUserWaitHandler( + ctx context.Context, + a APIClientUserInterface, + projectId, instanceId, region string, + userId int64, +) *wait.AsyncActionHandler[struct{}] { + handler := wait.New(func() (waitFinished bool, response *struct{}, err error) { + err = a.DeleteUserRequestExecute(ctx, projectId, region, instanceId, userId) + if err == nil { + return false, nil, nil + } + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + if !ok { + return false, nil, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError") + } + + switch oapiErr.StatusCode { + case http.StatusNotFound: + return true, nil, nil + case http.StatusInternalServerError: + tflog.Warn(ctx, "Wait handler got error 500") + return false, nil, nil + default: + return false, nil, err + } + }) + handler.SetTimeout(15 * time.Minute) + handler.SetSleepBeforeWait(15 * time.Second) + return handler +} -- 2.49.1 From 73a8274c5d20223b1139fa8e97331dfc05fb4761 Mon Sep 17 00:00:00 2001 From: "Marcel S. Henselin" Date: Thu, 5 Feb 2026 16:09:34 +0100 Subject: [PATCH 2/2] fix: add databases generated file feat: add useful command --- .../sqlserverflexbeta_database.md | 6 +- docs/data-sources/sqlserverflexbeta_flavor.md | 13 +- .../sqlserverflexbeta/database/datasource.go | 163 ++++++++++++++++++ 3 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 stackit/internal/services/sqlserverflexbeta/database/datasource.go diff --git a/docs/data-sources/sqlserverflexbeta_database.md b/docs/data-sources/sqlserverflexbeta_database.md index 98a29d9f..9322049f 100644 --- a/docs/data-sources/sqlserverflexbeta_database.md +++ b/docs/data-sources/sqlserverflexbeta_database.md @@ -28,15 +28,13 @@ data "stackitprivatepreview_sqlserverflexbeta_database" "example" { - `database_name` (String) The name of the database. - `instance_id` (String) The ID of the instance. - `project_id` (String) The STACKIT project ID. - -### Optional - - `region` (String) The region which should be addressed ### Read-Only - `collation_name` (String) The collation of the database. This database collation should match the *collation_name* of one of the collations given by the **Get database collation list** endpoint. - `compatibility_level` (Number) CompatibilityLevel of the Database. -- `id` (Number) The id of the database. +- `id` (String) The terraform internal identifier. - `name` (String) The name of the database. - `owner` (String) The owner of the database. +- `tf_original_api_id` (Number) The id of the database. diff --git a/docs/data-sources/sqlserverflexbeta_flavor.md b/docs/data-sources/sqlserverflexbeta_flavor.md index 6c1569be..4d2a32f3 100644 --- a/docs/data-sources/sqlserverflexbeta_flavor.md +++ b/docs/data-sources/sqlserverflexbeta_flavor.md @@ -26,15 +26,22 @@ data "stackitprivatepreview_sqlserverflexbeta_flavor" "flavor" { ## Schema -### Read-Only +### Required - `cpu` (Number) The cpu count of the instance. +- `node_type` (String) defines the nodeType it can be either single or HA +- `project_id` (String) The project ID of the flavor. +- `ram` (Number) The memory of the instance in Gibibyte. +- `region` (String) The region of the flavor. +- `storage_class` (String) The memory of the instance in Gibibyte. + +### Read-Only + - `description` (String) The flavor description. +- `flavor_id` (String) The id of the instance flavor. - `id` (String) The id of the instance flavor. - `max_gb` (Number) maximum storage which can be ordered for the flavor in Gigabyte. -- `memory` (Number) The memory of the instance in Gibibyte. - `min_gb` (Number) minimum storage which is required to order in Gigabyte. -- `node_type` (String) defines the nodeType it can be either single or HA - `storage_classes` (Attributes List) maximum storage which can be ordered for the flavor in Gigabyte. (see [below for nested schema](#nestedatt--storage_classes)) diff --git a/stackit/internal/services/sqlserverflexbeta/database/datasource.go b/stackit/internal/services/sqlserverflexbeta/database/datasource.go new file mode 100644 index 00000000..4cc56ea2 --- /dev/null +++ b/stackit/internal/services/sqlserverflexbeta/database/datasource.go @@ -0,0 +1,163 @@ +package sqlserverflexbeta + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" + "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core" + "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" + + sqlserverflexbetaPkg "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta" + sqlserverflexbetaGen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexbeta/database/datasources_gen" +) + +var _ datasource.DataSource = (*databaseDataSource)(nil) + +const errorPrefix = "[Sqlserverflexbeta - Database]" + +func NewDatabaseDataSource() datasource.DataSource { + return &databaseDataSource{} +} + +type databaseDataSource struct { + client *sqlserverflexbetaPkg.APIClient + providerData core.ProviderData +} + +type dsModel struct { + sqlserverflexbetaGen.DatabaseModel + TfId types.String `tfsdk:"id"` +} + +func (d *databaseDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_sqlserverflexbeta_database" +} + +func (d *databaseDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = sqlserverflexbetaGen.DatabaseDataSourceSchema(ctx) + resp.Schema.Attributes["id"] = schema.StringAttribute{ + Computed: true, + Description: "The terraform internal identifier.", + MarkdownDescription: "The terraform internal identifier.", + } +} + +// Configure adds the provider configured client to the data source. +func (d *databaseDataSource) Configure( + ctx context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClientConfigOptions := []config.ConfigurationOption{ + config.WithCustomAuth(d.providerData.RoundTripper), + utils.UserAgentConfigOption(d.providerData.Version), + } + if d.providerData.SQLServerFlexCustomEndpoint != "" { + apiClientConfigOptions = append( + apiClientConfigOptions, + config.WithEndpoint(d.providerData.SQLServerFlexCustomEndpoint), + ) + } else { + apiClientConfigOptions = append( + apiClientConfigOptions, + config.WithRegion(d.providerData.GetRegion()), + ) + } + apiClient, err := sqlserverflexbetaPkg.NewAPIClient(apiClientConfigOptions...) + if err != nil { + resp.Diagnostics.AddError( + "Error configuring API client", + fmt.Sprintf( + "Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", + err, + ), + ) + return + } + d.client = apiClient + tflog.Info(ctx, fmt.Sprintf("%s client configured", errorPrefix)) +} + +func (d *databaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data dsModel + readErr := "Read DB error" + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := data.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(data.Region) + instanceId := data.InstanceId.ValueString() + + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + + databaseName := data.DatabaseName.ValueString() + + databaseResp, err := d.client.GetDatabaseRequest(ctx, projectId, region, instanceId, databaseName).Execute() + if err != nil { + utils.LogError( + ctx, + &resp.Diagnostics, + err, + "Reading database", + fmt.Sprintf("database with %q does not exist in project %q.", databaseName, projectId), + map[int]string{ + http.StatusForbidden: fmt.Sprintf("Project with %q not found or forbidden access", projectId), + }, + ) + resp.State.RemoveResource(ctx) + return + } + + ctx = core.LogResponse(ctx) + + dbId, ok := databaseResp.GetIdOk() + if !ok { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + readErr, + "Database creation waiting: returned id is nil", + ) + return + } + + data.Id = types.Int64Value(dbId) + data.TfId = utils.BuildInternalTerraformId(projectId, region, instanceId, databaseName) + data.Owner = types.StringValue(databaseResp.GetOwner()) + + // TODO: fill remaining fields + // data.CollationName = types.Sometype(apiResponse.GetCollationName()) + // data.CompatibilityLevel = types.Sometype(apiResponse.GetCompatibilityLevel()) + // data.DatabaseName = types.Sometype(apiResponse.GetDatabaseName()) + // data.Id = types.Sometype(apiResponse.GetId()) + // data.InstanceId = types.Sometype(apiResponse.GetInstanceId()) + // data.Name = types.Sometype(apiResponse.GetName()) + // data.Owner = types.Sometype(apiResponse.GetOwner()) + // data.ProjectId = types.Sometype(apiResponse.GetProjectId()) + // data.Region = types.Sometype(apiResponse.GetRegion()) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} -- 2.49.1