From 2c6b42801e11d745861ef8054e29c914d4ff42ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20Schr=C3=B6der?= Date: Fri, 15 May 2026 16:45:53 +0200 Subject: [PATCH 01/12] fix(Docs): Adding `services` to path in utils creation step description. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 267a95238..4c9cfce7f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,7 +83,7 @@ If you want to onboard resources of a STACKIT service `foo` that was not yet in ```go setStringField(providerConfig.FooCustomEndpoint, func(v string) { providerData.FooCustomEndpoint = v }) ``` -4. Create a utils package, for service `foo` it would be `stackit/internal/foo/utils`. Add a `ConfigureClient()` func and use it in your resource and datasource implementations. +4. Create a utils package, for service `foo` it would be `stackit/internal/services/foo/utils`. Add a `ConfigureClient()` func and use it in your resource and datasource implementations. https://github.com/stackitcloud/terraform-provider-stackit/blob/main/.github/docs/contribution-guide/utils/util.go From 211a51a54554b7d32fe9a18ec3286c05e79bdd31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20Schr=C3=B6der?= Date: Fri, 15 May 2026 16:48:53 +0200 Subject: [PATCH 02/12] feat(dremio): Initial Commit for onboarding of the STACKIT Dremio service. Adding: - Dremio SDK integration - `.../services/dremio/` directory with packages and stubs for both dremio instances and users --- go.mod | 1 + go.sum | 2 ++ stackit/internal/core/core.go | 1 + .../services/dremio/instance/resource.go | 1 + .../internal/services/dremio/user/resource.go | 1 + .../internal/services/dremio/utils/util.go | 30 +++++++++++++++++++ stackit/provider.go | 3 ++ 7 files changed, 39 insertions(+) create mode 100644 stackit/internal/services/dremio/instance/resource.go create mode 100644 stackit/internal/services/dremio/user/resource.go create mode 100644 stackit/internal/services/dremio/utils/util.go diff --git a/go.mod b/go.mod index f0dd76e59..5ff027b45 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/cdn v1.16.0 github.com/stackitcloud/stackit-sdk-go/services/certificates v1.7.0 github.com/stackitcloud/stackit-sdk-go/services/dns v0.20.2 + github.com/stackitcloud/stackit-sdk-go/services/dremio v0.1.0 github.com/stackitcloud/stackit-sdk-go/services/edge v0.11.0 github.com/stackitcloud/stackit-sdk-go/services/git v0.13.0 github.com/stackitcloud/stackit-sdk-go/services/iaas v1.12.0 diff --git a/go.sum b/go.sum index c52202b3b..3e6e6ae71 100644 --- a/go.sum +++ b/go.sum @@ -680,6 +680,8 @@ github.com/stackitcloud/stackit-sdk-go/services/certificates v1.7.0 h1:J7BVVHjRT github.com/stackitcloud/stackit-sdk-go/services/certificates v1.7.0/go.mod h1:eJpB3/pukz+KzVPVHQ4g3DVtQkxGga18VbFBhq9ugdY= github.com/stackitcloud/stackit-sdk-go/services/dns v0.20.2 h1:nMJRg1dKioOlMwXJnZZgIRwfTWYCksVA9GyfAVmib1g= github.com/stackitcloud/stackit-sdk-go/services/dns v0.20.2/go.mod h1:FiYSv3D9rzgEVzi8Mpq5oYZBosrasa5uUYqVdEIbM1U= +github.com/stackitcloud/stackit-sdk-go/services/dremio v0.1.0 h1:yNFIU1+1dA2uK8ERdBb1Ut74Kt2szn4qgelBbM93JXA= +github.com/stackitcloud/stackit-sdk-go/services/dremio v0.1.0/go.mod h1:iMoiM8fM1mXC1Nz8FBiiQ08Yh+0C3yN0GPCdAbOlRXo= github.com/stackitcloud/stackit-sdk-go/services/edge v0.11.0 h1:/JUxaJSGmg+PRj90e4fngWkXNQkRKHOYpVykJ3zoy7w= github.com/stackitcloud/stackit-sdk-go/services/edge v0.11.0/go.mod h1:Ylse6gqGJtsd5TVmvha+hoLd1QQHLKvhY5dO15+q5kg= github.com/stackitcloud/stackit-sdk-go/services/git v0.13.0 h1:BdamSnGYhDkDqUWQQcJ8Kqik90laTK1IlG5CQqyLVgA= diff --git a/stackit/internal/core/core.go b/stackit/internal/core/core.go index fec9b4b91..856b05b5f 100644 --- a/stackit/internal/core/core.go +++ b/stackit/internal/core/core.go @@ -45,6 +45,7 @@ type ProviderData struct { AuthorizationCustomEndpoint string CdnCustomEndpoint string DnsCustomEndpoint string + DremioCustomEndpoint string EdgeCloudCustomEndpoint string GitCustomEndpoint string IaaSCustomEndpoint string diff --git a/stackit/internal/services/dremio/instance/resource.go b/stackit/internal/services/dremio/instance/resource.go new file mode 100644 index 000000000..8dc0f480d --- /dev/null +++ b/stackit/internal/services/dremio/instance/resource.go @@ -0,0 +1 @@ +package dremio diff --git a/stackit/internal/services/dremio/user/resource.go b/stackit/internal/services/dremio/user/resource.go new file mode 100644 index 000000000..8dc0f480d --- /dev/null +++ b/stackit/internal/services/dremio/user/resource.go @@ -0,0 +1 @@ +package dremio diff --git a/stackit/internal/services/dremio/utils/util.go b/stackit/internal/services/dremio/utils/util.go new file mode 100644 index 000000000..e32a25d7a --- /dev/null +++ b/stackit/internal/services/dremio/utils/util.go @@ -0,0 +1,30 @@ +package utils + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/stackitcloud/stackit-sdk-go/core/config" + dremio "github.com/stackitcloud/stackit-sdk-go/services/dremio/v1alphaapi" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *dremio.APIClient { + apiClientConfigOptions := []config.ConfigurationOption{ + config.WithCustomAuth(providerData.RoundTripper), + utils.UserAgentConfigOption(providerData.Version), + } + if providerData.DremioCustomEndpoint != "" { + apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.DremioCustomEndpoint)) + } + apiClient, err := dremio.NewAPIClient(apiClientConfigOptions...) + if err != nil { + core.LogAndAddError(ctx, diags, "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 nil + } + + return apiClient +} diff --git a/stackit/provider.go b/stackit/provider.go index 645b4925b..12f612fcf 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -175,6 +175,7 @@ type providerModel struct { CdnCustomEndpoint types.String `tfsdk:"cdn_custom_endpoint"` ALBCertificatesCustomEndpoint types.String `tfsdk:"alb_certificates_custom_endpoint"` DnsCustomEndpoint types.String `tfsdk:"dns_custom_endpoint"` + DremioCustomEndpoint types.String `tfsdk:"dremio_custom_endpoint"` EdgeCloudCustomEndpoint types.String `tfsdk:"edgecloud_custom_endpoint"` GitCustomEndpoint types.String `tfsdk:"git_custom_endpoint"` IaaSCustomEndpoint types.String `tfsdk:"iaas_custom_endpoint"` @@ -232,6 +233,7 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro "alb_custom_endpoint": "Custom endpoint for the Application Load Balancer service", "cdn_custom_endpoint": "Custom endpoint for the CDN service", "dns_custom_endpoint": "Custom endpoint for the DNS service", + "dremio_custom_endpoint": "Custom endpoint for the Dremio service", "edgecloud_custom_endpoint": "Custom endpoint for the Edge Cloud service", "git_custom_endpoint": "Custom endpoint for the Git service", "iaas_custom_endpoint": "Custom endpoint for the IaaS service", @@ -536,6 +538,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, setStringField(providerConfig.AuthorizationCustomEndpoint, func(v string) { providerData.AuthorizationCustomEndpoint = v }) setStringField(providerConfig.CdnCustomEndpoint, func(v string) { providerData.CdnCustomEndpoint = v }) setStringField(providerConfig.DnsCustomEndpoint, func(v string) { providerData.DnsCustomEndpoint = v }) + setStringField(providerConfig.DremioCustomEndpoint, func(v string) { providerData.DremioCustomEndpoint = v }) setStringField(providerConfig.EdgeCloudCustomEndpoint, func(v string) { providerData.EdgeCloudCustomEndpoint = v }) setStringField(providerConfig.GitCustomEndpoint, func(v string) { providerData.GitCustomEndpoint = v }) setStringField(providerConfig.IaaSCustomEndpoint, func(v string) { providerData.IaaSCustomEndpoint = v }) From e69901b55db9518fd9eb09936b39ed35a9d14c32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20Schr=C3=B6der?= Date: Fri, 22 May 2026 10:06:24 +0200 Subject: [PATCH 03/12] feat(dremio): Adding Dremio instance resource. First draft for the implementation of a Dremio instance resource. --- .../services/dremio/instance/resource.go | 780 ++++++++++++++++++ .../services/dremio/instance/resource_test.go | 341 ++++++++ stackit/provider.go | 6 + 3 files changed, 1127 insertions(+) create mode 100644 stackit/internal/services/dremio/instance/resource_test.go diff --git a/stackit/internal/services/dremio/instance/resource.go b/stackit/internal/services/dremio/instance/resource.go index 8dc0f480d..de604fa71 100644 --- a/stackit/internal/services/dremio/instance/resource.go +++ b/stackit/internal/services/dremio/instance/resource.go @@ -1 +1,781 @@ package dremio + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + + dremioSdk "github.com/stackitcloud/stackit-sdk-go/services/dremio/v1alphaapi" + dremioWaiter "github.com/stackitcloud/stackit-sdk-go/services/dremio/v1alphaapi/wait/wait" + dremioUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dremio/utils" +) + +var ( + _ resource.Resource = &instanceResource{} + _ resource.ResourceWithConfigure = &instanceResource{} + _ resource.ResourceWithImportState = &instanceResource{} + _ resource.ResourceWithModifyPlan = &instanceResource{} // not needed for global APIs +) + +// InstanceModel maps the resource schema data. +type InstanceModel struct { + Id types.String `tfsdk:"id"` + + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + InstanceId types.String `tfsdk:"instance_id"` + + // Required Fields + DisplayName types.String `tfsdk:"display_name"` + Authentication *AuthenticationModel `tfsdk:"authentication"` + + // Optional Fields + Description types.String `tfsdk:"description"` + + // Read-only Fields + State types.String `tfsdk:"state"` + ErrorMessage types.String `tfsdk:"error_message"` + Endpoints types.Object `tfsdk:"endpoints"` // see endpointsTypes below + + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +// AuthenticationModel maps the nested authentication block. +type AuthenticationModel struct { + // Required Fields + Type types.String `tfsdk:"type"` + + // Optional Fields + AzureAD *AzureADModel `tfsdk:"azuread"` + OAuth *OAuthModel `tfsdk:"oauth"` +} + +type AzureADModel struct { + // Required Fields + AuthorityUrl types.String `tfsdk:"authority_url"` + ClientId types.String `tfsdk:"client_id"` + ClientSecret types.String `tfsdk:"client_secret"` + + RedirectUrl types.String `tfsdk:"redirect_url"` +} + +type OAuthModel struct { + // Required Fields + AuthorityUrl types.String `tfsdk:"authority_url"` + ClientId types.String `tfsdk:"client_id"` + ClientSecret types.String `tfsdk:"client_secret"` + JwtClaims *JwtClaimsModel `tfsdk:"jwt_claims"` + + // Optional Fields + Scope types.String `tfsdk:"scope"` + Parameters []AuthParameterModel `tfsdk:"parameters"` + + // Read-only Fields + RedirectUrl types.String `tfsdk:"redirect_url"` +} + +type JwtClaimsModel struct { + // Required Fields + UserName types.String `tfsdk:"user_name"` +} + +type AuthParameterModel struct { + // Required Fields + Name types.String `tfsdk:"name"` + Value types.String `tfsdk:"value"` +} + +var endpointsTypes = map[string]attr.Type{ + "arrow_flight": basetypes.StringType{}, + "catalog": basetypes.StringType{}, + "ui": basetypes.StringType{}, +} + +func NewInstanceResource() resource.Resource { + return &instanceResource{} +} + +type instanceResource struct { + client *dremioSdk.APIClient + providerData core.ProviderData // not needed for global APIs +} + +func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dremio_instance" +} + +func (r *instanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel InstanceModel + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel InstanceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := dremioUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "Dremio instance client configured") +} + +func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + descriptions := map[string]string{ + "main": "Manages a STACKIT Dremio instance.", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`dremio_id`\".", + "project_id": "STACKIT Project ID to which the resource is associated.", + "instance_id": "The Dremio instance ID.", + "region": "The STACKIT region name the resource is located in. If not defined, the provider region is used.", + "display_name": "The display name is a short name chosen by the user to identify the resource.", + "description": "The description is a longer text chosen by the user to provide more context for the resource.", + "state": "The current state of the resource.", + "error_message": "A message describing an actionable error the user can resolve. This field is empty if no such error exists.", + "endpoints": "The available endpoints of the Dremio instance.", + "endpoints_arrow_flight": "The arrow flight endpoint of the Dremio instance.", + "endpoints_catalog": "The Apache Iceberg endpoint of the Dremio instance.", + "endpoints_ui": "The UI endpoint of the Dremio instance.", + "authentication": "Dremio instance authentication settings. A change here triggers a Dremio restart and will incur downtime.", + "authentication_type": "Type of authentication (local-only, azuread, oauth).", + "azuread": "Azure Active Directory authentication configuration.", + "azuread_authority_url": "The Azure AD authority URL.", + "azuread_client_id": "The Azure AD client ID.", + "azuread_client_secret": "The Azure AD client secret.", + "azuread_redirect_url": "The Azure AD redirect URL.", + "oauth": "OIDC authentication configuration.", + "oauth_authority_url": "The Issuer location URI, where the OIDC provider configuration can be found.", + "oauth_client_id": "The client ID assigned by the Identity Provider.", + "oauth_client_secret": "The client secret generated by the Identity Provider.", + "oauth_scope": "A list of space-separated scopes. The `openid` scope is always required; other scopes can vary by provider.", + "oauth_redirect_url": "The URL where the Dremio instance is hosted. The URL must match the redirect URL set in the Identity Provider.", + "oauth_jwt_claims": "Maps fields from the JWT token to fields Dremio requires.", + "oauth_jwt_claims_user_name": "Mapped user name claim (e.g. email).", + "oauth_parameters": "Any additional parameters the Identity Provider requires.", + "oauth_parameters_name": "Parameter name.", + "oauth_parameters_value": "Parameter value.", + } + + resp.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "instance_id": schema.StringAttribute{ + Description: descriptions["instance_id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "region": schema.StringAttribute{ + Required: true, + Description: descriptions["region"], + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "display_name": schema.StringAttribute{ + Description: descriptions["display_name"], + Required: true, + }, + "description": schema.StringAttribute{ + Description: descriptions["description"], + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "state": schema.StringAttribute{ + Description: descriptions["state"], + Computed: true, + }, + "error_message": schema.StringAttribute{ + Description: descriptions["error_message"], + Optional: true, + Computed: true, + }, + "endpoints": schema.SingleNestedAttribute{ + Description: descriptions["endpoints"], + Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, + Attributes: map[string]schema.Attribute{ + "arrow_flight": schema.StringAttribute{ + Description: descriptions["endpoints_arrow_flight"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "catalog": schema.StringAttribute{ + Description: descriptions["endpoints_catalog"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "ui": schema.StringAttribute{ + Description: descriptions["endpoints_ui"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + }, + "authentication": schema.SingleNestedAttribute{ + Description: descriptions["authentication"], + Required: true, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Description: descriptions["authentication_type"], + Required: true, + }, + "azuread": schema.SingleNestedAttribute{ + Description: descriptions["azuread"], + Optional: true, + Attributes: map[string]schema.Attribute{ + "authority_url": schema.StringAttribute{ + Description: descriptions["azuread_authority_url"], + Required: true, + }, + "client_id": schema.StringAttribute{ + Description: descriptions["azuread_client_id"], + Required: true, + }, + "client_secret": schema.StringAttribute{ + Description: descriptions["azuread_client_secret"], + Required: true, + Sensitive: true, + }, + "redirect_url": schema.StringAttribute{ + Description: descriptions["azuread_redirect_url"], + Computed: true, + }, + }, + }, + "oauth": schema.SingleNestedAttribute{ + Description: descriptions["oauth"], + Optional: true, + Attributes: map[string]schema.Attribute{ + "authority_url": schema.StringAttribute{ + Description: descriptions["oauth_authority_url"], + Required: true, + }, + "client_id": schema.StringAttribute{ + Description: descriptions["oauth_client_id"], + Required: true, + }, + "client_secret": schema.StringAttribute{ + Description: descriptions["oauth_client_secret"], + Required: true, + Sensitive: true, + }, + "scope": schema.StringAttribute{ + Description: descriptions["oauth_scope"], + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "redirect_url": schema.StringAttribute{ + Description: descriptions["oauth_redirect_url"], + Computed: true, + }, + "jwt_claims": schema.SingleNestedAttribute{ + Description: descriptions["oauth_jwt_claims"], + Required: true, + Attributes: map[string]schema.Attribute{ + "user_name": schema.StringAttribute{ + Description: descriptions["oauth_jwt_claims_user_name"], + Required: true, + }, + }, + }, + "parameters": schema.ListNestedAttribute{ + Description: descriptions["oauth_parameters"], + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: descriptions["oauth_parameters_name"], + Required: true, + }, + "value": schema.StringAttribute{ + Description: descriptions["oauth_parameters_value"], + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + "timeouts": timeouts.AttributesAll(ctx), + }, + } +} + +func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + var model InstanceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + waiterTimeout := dremioWaiter.CreateDremioWaitHandler(ctx, r.client.DefaultAPI, "", "", "").GetTimeout() + createTimeout, diags := model.Timeouts.Create(ctx, waiterTimeout+core.DefaultTimeoutMargin) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, createTimeout) + defer cancel() + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := model.Region.ValueString() // not needed for global APIs + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + + // prepare the payload struct for the create instance request + payload, err := toCreatePayload(&model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + // Create new Dremio instance + instanceResp, err := r.client.DefaultAPI.CreateDremioInstance(ctx, projectId, region).CreateDremioInstancePayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]interface{}{ + "project_id": projectId, + "region": region, + "instance_id": instanceResp.Id, + }) + if resp.Diagnostics.HasError() { + return + } + + _, err = dremioWaiter.CreateDremioWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceResp.Id).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating Dremio instance", fmt.Sprintf("Dremio instance creation waiting: %v", err)) + return + } + + err = mapFields(instanceResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating Dremio instance", fmt.Sprintf("Processing API payload: %v", err)) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Dremio instance created") +} + +func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model InstanceModel + resp.Diagnostics.Append(req.State.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + readTimeout, diags := model.Timeouts.Read(ctx, core.DefaultOperationTimeout) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, readTimeout) + defer cancel() + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + instanceId := model.InstanceId.ValueString() + if instanceId == "" { + // Resource not yet created; ID is unknown. + resp.State.RemoveResource(ctx) + return + } + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + + instanceResp, err := r.client.DefaultAPI.GetDremioInstance(ctx, projectId, region, instanceId).Execute() + if err != nil { + if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading dremio instance", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(instanceResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading dremio instance", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Dremio instance read") +} + +func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + var model, state InstanceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + waiterTimeout := dremioWaiter.UpdateDremioWaitHandler(ctx, r.client.DefaultAPI, "", "", "").GetTimeout() + updateTimeout, diags := model.Timeouts.Update(ctx, waiterTimeout+core.DefaultTimeoutMargin) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, updateTimeout) + defer cancel() + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := model.Region.ValueString() // not needed for global APIs + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + + payload, err := toUpdatePayload(&model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + instanceId := state.InstanceId.ValueString() + instanceResp, err := r.client.DefaultAPI.UpdateDremioInstance(ctx, projectId, region, instanceId).UpdateDremioInstancePayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]interface{}{ + "project_id": projectId, + "region": region, + "instance_id": instanceResp.Id, + }) + if resp.Diagnostics.HasError() { + return + } + + _, err = dremioWaiter.UpdateDremioWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceResp.Id).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating Dremio instance", fmt.Sprintf("Dremio instance updating waiting: %v", err)) + return + } + + err = mapFields(instanceResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating Dremio instance", fmt.Sprintf("Processing API payload: %v", err)) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Dremio instance updated") +} + +func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + var model InstanceModel + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + waiterTimeout := dremioWaiter.DeleteDremioWaitHandler(ctx, r.client.DefaultAPI, "", "", "").GetTimeout() + deleteTimeout, diags := model.Timeouts.Delete(ctx, waiterTimeout+core.DefaultTimeoutMargin) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, deleteTimeout) + defer cancel() + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := model.Region.ValueString() + instanceId := model.InstanceId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + + err := r.client.DefaultAPI.DeleteDremioInstance(ctx, projectId, region, instanceId).Execute() + if err != nil { + if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting Dremio instance", fmt.Sprintf("Calling API: %v", err)) + } + + ctx = core.LogResponse(ctx) + + _, err = dremioWaiter.DeleteDremioWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting Dremio instance", fmt.Sprintf("Dremio instance deletion waiting: %v", err)) + return + } + + tflog.Info(ctx, "Dremio instance deleted") +} + +func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing dremio instance", + fmt.Sprintf("Expected import identifier with format [project_id],[region],[instance_id], got %q", req.ID), + ) + return + } + + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "instance_id": idParts[2], + }) + + tflog.Info(ctx, "Dremio instance state imported") +} + +// Maps instance fields to the provider's internal model +func mapFields(instanceResp *dremioSdk.DremioResponse, model *InstanceModel) error { + if instanceResp == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + model.InstanceId = types.StringValue(instanceResp.Id) + + model.Id = utils.BuildInternalTerraformId( + model.ProjectId.ValueString(), + model.Region.ValueString(), + model.InstanceId.ValueString(), + ) + + model.DisplayName = types.StringValue(instanceResp.DisplayName) + model.State = types.StringValue(instanceResp.State) + + model.Description = types.StringPointerValue(instanceResp.Description) + model.ErrorMessage = types.StringPointerValue(instanceResp.ErrorMessage) + + endpoints, diags := types.ObjectValue(endpointsTypes, map[string]attr.Value{ + "arrow_flight": types.StringValue(instanceResp.Endpoints.ArrowFlight), + "catalog": types.StringValue(instanceResp.Endpoints.Catalog), + "ui": types.StringValue(instanceResp.Endpoints.Ui), + }) + if diags.HasError() { + return fmt.Errorf("error mapping endpoints: %v", diags) + } + model.Endpoints = endpoints + + authModel := &AuthenticationModel{ + Type: types.StringValue(instanceResp.Authentication.Type), + } + + if instanceResp.Authentication.Azuread != nil { + azureADResp := instanceResp.Authentication.Azuread + authModel.AzureAD = &AzureADModel{ + AuthorityUrl: types.StringValue(azureADResp.AuthorityUrl), + ClientId: types.StringValue(azureADResp.ClientId), + ClientSecret: types.StringValue(azureADResp.ClientSecret), + RedirectUrl: types.StringPointerValue(azureADResp.RedirectUrl), + } + } + + if instanceResp.Authentication.Oauth != nil { + oauthResp := instanceResp.Authentication.Oauth + oauthModel := &OAuthModel{ + AuthorityUrl: types.StringValue(oauthResp.AuthorityUrl), + ClientId: types.StringValue(oauthResp.ClientId), + ClientSecret: types.StringValue(oauthResp.ClientSecret), + Scope: types.StringPointerValue(oauthResp.Scope), + RedirectUrl: types.StringPointerValue(oauthResp.RedirectUrl), + JwtClaims: &JwtClaimsModel{ + UserName: types.StringValue(oauthResp.JwtClaims.UserName), + }, + } + + if len(oauthResp.Parameters) > 0 { + var params []AuthParameterModel + for _, p := range oauthResp.Parameters { + params = append(params, AuthParameterModel{ + Name: types.StringValue(p.Name), + Value: types.StringValue(p.Value), + }) + } + oauthModel.Parameters = params + } + + authModel.OAuth = oauthModel + } + + model.Authentication = authModel + + return nil +} + +// Build UpdateDremioInstancePayload from provider's model +func toUpdatePayload(model *InstanceModel) (*dremioSdk.UpdateDremioInstancePayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + return &dremioSdk.UpdateDremioInstancePayload{ + Authentication: parseAuthentication(model), + Description: model.Description.ValueStringPointer(), + DisplayName: model.DisplayName.ValueStringPointer(), + }, nil +} + +// Build CreateDremioInstancePayload from provider's model +func toCreatePayload(model *InstanceModel) (*dremioSdk.CreateDremioInstancePayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + return &dremioSdk.CreateDremioInstancePayload{ + Authentication: parseAuthentication(model), + Description: model.Description.ValueStringPointer(), + DisplayName: model.DisplayName.ValueString(), + }, nil +} + +func parseAuthentication(model *InstanceModel) *dremioSdk.Authentication { + var azureAdPayload *dremioSdk.Azuread + if model.Authentication.AzureAD != nil { + azureAdPayload = &dremioSdk.Azuread{ + AuthorityUrl: model.Authentication.AzureAD.AuthorityUrl.ValueString(), + ClientId: model.Authentication.AzureAD.ClientId.ValueString(), + ClientSecret: model.Authentication.AzureAD.ClientSecret.ValueString(), + RedirectUrl: model.Authentication.AzureAD.RedirectUrl.ValueStringPointer(), + } + } + + var oAuthPayload *dremioSdk.Oauth + if model.Authentication.OAuth != nil { + oAuthParams := []dremioSdk.AuthParameters{} + if len(model.Authentication.OAuth.Parameters) > 0 { + parameters := model.Authentication.OAuth.Parameters + for _, param := range parameters { + oAuthParams = append(oAuthParams, dremioSdk.AuthParameters{ + Name: param.Name.ValueString(), + Value: param.Value.ValueString(), + }) + } + } + + oAuthPayload = &dremioSdk.Oauth{ + AuthorityUrl: model.Authentication.OAuth.AuthorityUrl.ValueString(), + ClientId: model.Authentication.OAuth.ClientId.ValueString(), + ClientSecret: model.Authentication.OAuth.ClientSecret.ValueString(), + JwtClaims: dremioSdk.OauthJwtClaims{ + UserName: model.Authentication.OAuth.JwtClaims.UserName.ValueString(), + }, + RedirectUrl: model.Authentication.OAuth.RedirectUrl.ValueStringPointer(), + Scope: model.Authentication.OAuth.Scope.ValueStringPointer(), + Parameters: oAuthParams, + } + } + + return &dremioSdk.Authentication{ + Azuread: azureAdPayload, + Oauth: oAuthPayload, + Type: model.Authentication.Type.ValueString(), + } +} diff --git a/stackit/internal/services/dremio/instance/resource_test.go b/stackit/internal/services/dremio/instance/resource_test.go new file mode 100644 index 000000000..d26aee738 --- /dev/null +++ b/stackit/internal/services/dremio/instance/resource_test.go @@ -0,0 +1,341 @@ +package dremio + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + dremioSdk "github.com/stackitcloud/stackit-sdk-go/services/dremio/v1alphaapi" +) + +func TestMapFields(t *testing.T) { + instanceId := uuid.New().String() + tests := []struct { + description string + state *InstanceModel + input *dremioSdk.DremioResponse + expected *InstanceModel + wantErr bool + }{ + { + "all_fields_filled", + &InstanceModel{ + Region: types.StringValue("rid"), + ProjectId: types.StringValue("pid"), + }, + &dremioSdk.DremioResponse{ + Id: instanceId, + CreateTime: time.Now(), + Description: utils.Ptr("minimal-required-values"), + DisplayName: "greatName", + Authentication: dremioSdk.Authentication{ + Azuread: &dremioSdk.Azuread{ + AuthorityUrl: "azure-authority", + ClientId: "azure-client", + ClientSecret: "azure-secret", + RedirectUrl: utils.Ptr("azure-redirect"), + }, + Oauth: &dremioSdk.Oauth{ + AuthorityUrl: "oauth-authority", + ClientId: "oauth-client", + ClientSecret: "oauth-secret", + JwtClaims: dremioSdk.OauthJwtClaims{ + UserName: "oauth-username", + }, + Parameters: []dremioSdk.AuthParameters{ + { + Name: "oauth-parameter", + Value: "oauth-value", + }, + }, + RedirectUrl: utils.Ptr("oauth-redirect"), + Scope: utils.Ptr("oauth-scope"), + }, + Type: "local-only", + }, + Endpoints: dremioSdk.Endpoints{ + ArrowFlight: "flight", + Catalog: "catalog", + Ui: "ui", + }, + State: "active", + }, + &InstanceModel{ + Id: types.StringValue("pid,rid," + instanceId), + + ProjectId: types.StringValue("pid"), + Region: types.StringValue("rid"), + InstanceId: types.StringValue(instanceId), + + DisplayName: types.StringValue("greatName"), + Authentication: &AuthenticationModel{ + AzureAD: &AzureADModel{ + AuthorityUrl: types.StringValue("azure-authority"), + ClientId: types.StringValue("azure-client"), + ClientSecret: types.StringValue("azure-secret"), + RedirectUrl: types.StringValue("azure-redirect"), + }, + OAuth: &OAuthModel{ + AuthorityUrl: types.StringValue("oauth-authority"), + ClientId: types.StringValue("oauth-client"), + ClientSecret: types.StringValue("oauth-secret"), + JwtClaims: &JwtClaimsModel{ + UserName: types.StringValue("oauth-username"), + }, + Parameters: []AuthParameterModel{ + { + Name: types.StringValue("oauth-parameter"), + Value: types.StringValue("oauth-value"), + }, + }, + RedirectUrl: types.StringValue("oauth-redirect"), + Scope: types.StringValue("oauth-scope"), + }, + Type: types.StringValue("local-only"), + }, + Description: types.StringValue("minimal-required-values"), + + State: types.StringValue("active"), + ErrorMessage: types.StringNull(), + Endpoints: types.ObjectValueMust( + map[string]attr.Type{ + "arrow_flight": types.StringType, + "catalog": types.StringType, + "ui": types.StringType, + }, + map[string]attr.Value{ + "arrow_flight": types.StringValue("flight"), + "catalog": types.StringValue("catalog"), + "ui": types.StringValue("ui"), + }, + ), + }, + false, + }, + { + "nil response", + &InstanceModel{ + Region: types.StringValue("rid"), + ProjectId: types.StringValue("pid"), + }, + nil, + &InstanceModel{ + Id: types.StringValue("pid,rid,"), + ProjectId: types.StringValue("pid"), + Region: types.StringValue("rid"), + }, + true, + }, + { + "nil state", + nil, + &dremioSdk.DremioResponse{}, + nil, + true, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapFields(tt.input, tt.state) + if (err != nil) != tt.wantErr { + t.Errorf("mapFields error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if diff := cmp.Diff(tt.expected, tt.state); diff != "" { + t.Errorf("mapFields mismatch (-want +got):\n%s", diff) + } + } + }) + } +} + +func TestToCreatePayload(t *testing.T) { + tests := []struct { + description string + state *InstanceModel + expected *dremioSdk.CreateDremioInstancePayload + wantErr bool + }{ + { + "success", + &InstanceModel{ + Authentication: &AuthenticationModel{ + AzureAD: &AzureADModel{ + AuthorityUrl: types.StringValue("azure-authority"), + ClientId: types.StringValue("azure-client"), + ClientSecret: types.StringValue("azure-secret"), + RedirectUrl: types.StringValue("azure-redirect"), + }, + OAuth: &OAuthModel{ + AuthorityUrl: types.StringValue("oauth-authority"), + ClientId: types.StringValue("oauth-client"), + ClientSecret: types.StringValue("oauth-secret"), + JwtClaims: &JwtClaimsModel{ + UserName: types.StringValue("oauth-username"), + }, + Parameters: []AuthParameterModel{ + { + Name: types.StringValue("oauth-parameter"), + Value: types.StringValue("oauth-value"), + }, + }, + RedirectUrl: types.StringValue("oauth-redirect"), + Scope: types.StringValue("oauth-scope"), + }, + Type: types.StringValue("oauth"), + }, + Description: types.StringValue("test description"), + DisplayName: types.StringValue("displayName"), + }, + &dremioSdk.CreateDremioInstancePayload{ + Authentication: &dremioSdk.Authentication{ + Azuread: &dremioSdk.Azuread{ + AuthorityUrl: "azure-authority", + ClientId: "azure-client", + ClientSecret: "azure-secret", + RedirectUrl: utils.Ptr("azure-redirect"), + }, + Oauth: &dremioSdk.Oauth{ + AuthorityUrl: "oauth-authority", + ClientId: "oauth-client", + ClientSecret: "oauth-secret", + JwtClaims: dremioSdk.OauthJwtClaims{ + UserName: "oauth-username", + }, + Parameters: []dremioSdk.AuthParameters{ + { + Name: "oauth-parameter", + Value: "oauth-value", + }, + }, + RedirectUrl: utils.Ptr("oauth-redirect"), + Scope: utils.Ptr("oauth-scope"), + }, + Type: "oauth", + }, + Description: utils.Ptr("test description"), + DisplayName: "displayName", + }, + false, + }, + { + "nil model", + nil, + nil, + true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + payload, err := toCreatePayload(tt.state) + if (err != nil) != tt.wantErr { + t.Errorf("toCreatePayload error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if diff := cmp.Diff(tt.expected, payload); diff != "" { + t.Errorf("toCreatePayload mismatch (-want +got):\n%s", diff) + } + } + }) + } +} + +func TestToUpdatePayload(t *testing.T) { + tests := []struct { + description string + state *InstanceModel + expected *dremioSdk.UpdateDremioInstancePayload + wantErr bool + }{ + { + "success", + &InstanceModel{ + Authentication: &AuthenticationModel{ + AzureAD: &AzureADModel{ + AuthorityUrl: types.StringValue("azure-authority"), + ClientId: types.StringValue("azure-client"), + ClientSecret: types.StringValue("azure-secret"), + RedirectUrl: types.StringValue("azure-redirect"), + }, + OAuth: &OAuthModel{ + AuthorityUrl: types.StringValue("oauth-authority"), + ClientId: types.StringValue("oauth-client"), + ClientSecret: types.StringValue("oauth-secret"), + JwtClaims: &JwtClaimsModel{ + UserName: types.StringValue("oauth-username"), + }, + Parameters: []AuthParameterModel{ + { + Name: types.StringValue("oauth-parameter"), + Value: types.StringValue("oauth-value"), + }, + }, + RedirectUrl: types.StringValue("oauth-redirect"), + Scope: types.StringValue("oauth-scope"), + }, + Type: types.StringValue("oauth"), + }, + Description: types.StringValue("test description"), + DisplayName: types.StringValue("displayName"), + }, + &dremioSdk.UpdateDremioInstancePayload{ + Authentication: &dremioSdk.Authentication{ + Azuread: &dremioSdk.Azuread{ + AuthorityUrl: "azure-authority", + ClientId: "azure-client", + ClientSecret: "azure-secret", + RedirectUrl: utils.Ptr("azure-redirect"), + }, + Oauth: &dremioSdk.Oauth{ + AuthorityUrl: "oauth-authority", + ClientId: "oauth-client", + ClientSecret: "oauth-secret", + JwtClaims: dremioSdk.OauthJwtClaims{ + UserName: "oauth-username", + }, + Parameters: []dremioSdk.AuthParameters{ + { + Name: "oauth-parameter", + Value: "oauth-value", + }, + }, + RedirectUrl: utils.Ptr("oauth-redirect"), + Scope: utils.Ptr("oauth-scope"), + }, + Type: "oauth", + }, + Description: utils.Ptr("test description"), + DisplayName: utils.Ptr("displayName"), + }, + false, + }, + { + "nil model", + nil, + nil, + true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + payload, err := toUpdatePayload(tt.state) + if (err != nil) != tt.wantErr { + t.Errorf("toUpdatePayload error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if diff := cmp.Diff(tt.expected, payload); diff != "" { + t.Errorf("toUpdatePayload mismatch (-want +got):\n%s", diff) + } + } + }) + } +} diff --git a/stackit/provider.go b/stackit/provider.go index 12f612fcf..d9996e6b3 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -30,6 +30,7 @@ import ( cdn "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/distribution" dnsRecordSet "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/recordset" dnsZone "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/zone" + dremioInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dremio/instance" edgeCloudInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/edgecloud/instance" edgeCloudInstances "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/edgecloud/instances" edgeCloudKubeconfig "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/edgecloud/kubeconfig" @@ -362,6 +363,10 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro Optional: true, Description: descriptions["dns_custom_endpoint"], }, + "dremio_custom_endpoint": schema.StringAttribute{ + Optional: true, + Description: descriptions["dremio_custom_endpoint"], + }, "edgecloud_custom_endpoint": schema.StringAttribute{ Optional: true, Description: descriptions["edgecloud_custom_endpoint"], @@ -748,6 +753,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { cdnCustomDomain.NewCustomDomainResource, dnsZone.NewZoneResource, dnsRecordSet.NewRecordSetResource, + dremioInstance.NewInstanceResource, edgeCloudInstance.NewInstanceResource, edgeCloudKubeconfig.NewKubeconfigResource, edgeCloudToken.NewTokenResource, From a40a5a5459ba74ca73a812de0578513ed35dc6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20Schr=C3=B6der?= Date: Fri, 22 May 2026 10:10:20 +0200 Subject: [PATCH 04/12] chore(dremio): Linting --- stackit/internal/services/dremio/instance/resource.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stackit/internal/services/dremio/instance/resource.go b/stackit/internal/services/dremio/instance/resource.go index de604fa71..d1c82471d 100644 --- a/stackit/internal/services/dremio/instance/resource.go +++ b/stackit/internal/services/dremio/instance/resource.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" @@ -26,6 +27,7 @@ import ( dremioSdk "github.com/stackitcloud/stackit-sdk-go/services/dremio/v1alphaapi" dremioWaiter "github.com/stackitcloud/stackit-sdk-go/services/dremio/v1alphaapi/wait/wait" + dremioUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dremio/utils" ) From 0f201493e0098f81704922c1a6149cc3187555d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20Schr=C3=B6der?= Date: Fri, 22 May 2026 10:38:31 +0200 Subject: [PATCH 05/12] fix(dremio): Providing default region in SDK config. Not propagating it renders the provider unusable for Dremio, because Dremio is no global STACKIT API yet. --- stackit/internal/services/dremio/utils/util.go | 1 + 1 file changed, 1 insertion(+) diff --git a/stackit/internal/services/dremio/utils/util.go b/stackit/internal/services/dremio/utils/util.go index e32a25d7a..912af6e39 100644 --- a/stackit/internal/services/dremio/utils/util.go +++ b/stackit/internal/services/dremio/utils/util.go @@ -16,6 +16,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags apiClientConfigOptions := []config.ConfigurationOption{ config.WithCustomAuth(providerData.RoundTripper), utils.UserAgentConfigOption(providerData.Version), + config.WithRegion(providerData.DefaultRegion), } if providerData.DremioCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.DremioCustomEndpoint)) From fc04498943de868d856a30d8d6765fbe031a29c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20Schr=C3=B6der?= Date: Fri, 22 May 2026 14:23:12 +0200 Subject: [PATCH 06/12] feat(dremio): Adding example for Dremio instance --- .../stackit_dremio_instance/resource.tf | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 examples/resources/stackit_dremio_instance/resource.tf diff --git a/examples/resources/stackit_dremio_instance/resource.tf b/examples/resources/stackit_dremio_instance/resource.tf new file mode 100644 index 000000000..5cf5d6c1a --- /dev/null +++ b/examples/resources/stackit_dremio_instance/resource.tf @@ -0,0 +1,34 @@ +resource "stackit_dremio_instance" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + region = "eu01" + display_name = "exampleName" + description = "Example description" + authentication = { + type = "local-only" // "oauth" or "azuread" for IDP config + + oauth = { // only needed if "oauth" is given as type + authority_url = "authority" + client_id = "client-id" + client_secret = "client-secret" + jwt_claims = { + user_name = "example" + } + scope = "idp-scope" + parameters = [ + {"name": "example", "value": "example-value"} + ] + } + + azuread = { // only needed if "azuread" is given as type + authority_url = "authority" + client_id = "client-id" + client_secret = "client-secret" + } + } +} + +# Only use the import statement, if you want to import an existing dns zone +import { + to = stackit_dremio_instance.import_example + id = "${var.project_id},${var.region},${var.instance_id}" +} \ No newline at end of file From 2acec3f731fb8914a9db98d4045480b9a8774b42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20Schr=C3=B6der?= Date: Fri, 22 May 2026 19:43:41 +0200 Subject: [PATCH 07/12] feat(dremio): Preparing resource methods for data source usage. The aim of this commit was to dry up and atomize common fields which are both used by the instance resource and data resource. By drying up the methods and model constellation we can make use of one implementation. Also removed the endpoints object. --- .../services/dremio/instance/resource.go | 74 +++++++++++++------ .../services/dremio/instance/resource_test.go | 72 +++++++++--------- 2 files changed, 88 insertions(+), 58 deletions(-) diff --git a/stackit/internal/services/dremio/instance/resource.go b/stackit/internal/services/dremio/instance/resource.go index d1c82471d..d95fa35d7 100644 --- a/stackit/internal/services/dremio/instance/resource.go +++ b/stackit/internal/services/dremio/instance/resource.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" @@ -16,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" @@ -38,8 +36,7 @@ var ( _ resource.ResourceWithModifyPlan = &instanceResource{} // not needed for global APIs ) -// InstanceModel maps the resource schema data. -type InstanceModel struct { +type Model struct { Id types.String `tfsdk:"id"` ProjectId types.String `tfsdk:"project_id"` @@ -47,16 +44,23 @@ type InstanceModel struct { InstanceId types.String `tfsdk:"instance_id"` // Required Fields - DisplayName types.String `tfsdk:"display_name"` - Authentication *AuthenticationModel `tfsdk:"authentication"` + DisplayName types.String `tfsdk:"display_name"` // Optional Fields Description types.String `tfsdk:"description"` // Read-only Fields - State types.String `tfsdk:"state"` - ErrorMessage types.String `tfsdk:"error_message"` - Endpoints types.Object `tfsdk:"endpoints"` // see endpointsTypes below + State types.String `tfsdk:"state"` + ErrorMessage types.String `tfsdk:"error_message"` + Endpoints *EndpointsModel `tfsdk:"endpoints"` +} + +// InstanceModel maps the resource schema data. +type InstanceModel struct { + Model + + // Required Fields + Authentication *AuthenticationModel `tfsdk:"authentication"` Timeouts timeouts.Value `tfsdk:"timeouts"` } @@ -106,10 +110,10 @@ type AuthParameterModel struct { Value types.String `tfsdk:"value"` } -var endpointsTypes = map[string]attr.Type{ - "arrow_flight": basetypes.StringType{}, - "catalog": basetypes.StringType{}, - "ui": basetypes.StringType{}, +type EndpointsModel struct { + ArrowFlight types.String `tfsdk:"arrow_flight"` + Catalog types.String `tfsdk:"catalog"` + Ui types.String `tfsdk:"ui"` } func NewInstanceResource() resource.Resource { @@ -561,6 +565,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating Dremio instance", fmt.Sprintf("Processing API payload: %v", err)) return } + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) if resp.Diagnostics.HasError() { return @@ -633,7 +638,6 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS tflog.Info(ctx, "Dremio instance state imported") } -// Maps instance fields to the provider's internal model func mapFields(instanceResp *dremioSdk.DremioResponse, model *InstanceModel) error { if instanceResp == nil { return fmt.Errorf("response input is nil") @@ -642,6 +646,24 @@ func mapFields(instanceResp *dremioSdk.DremioResponse, model *InstanceModel) err return fmt.Errorf("model input is nil") } + err := mapModelFields(instanceResp, &model.Model) + if err != nil { + return fmt.Errorf("failed to map Model fields") + } + err = mapAuthentication(instanceResp, model) + if err != nil { + return fmt.Errorf("failed to map Authentication fields") + } + + return nil +} + +// Maps instance fields to the provider's internal model +func mapModelFields(instanceResp *dremioSdk.DremioResponse, model *Model) error { + if model == nil { + return fmt.Errorf("model input is nil") + } + model.InstanceId = types.StringValue(instanceResp.Id) model.Id = utils.BuildInternalTerraformId( @@ -656,17 +678,21 @@ func mapFields(instanceResp *dremioSdk.DremioResponse, model *InstanceModel) err model.Description = types.StringPointerValue(instanceResp.Description) model.ErrorMessage = types.StringPointerValue(instanceResp.ErrorMessage) - endpoints, diags := types.ObjectValue(endpointsTypes, map[string]attr.Value{ - "arrow_flight": types.StringValue(instanceResp.Endpoints.ArrowFlight), - "catalog": types.StringValue(instanceResp.Endpoints.Catalog), - "ui": types.StringValue(instanceResp.Endpoints.Ui), - }) - if diags.HasError() { - return fmt.Errorf("error mapping endpoints: %v", diags) + model.Endpoints = &EndpointsModel{ + ArrowFlight: types.StringValue(instanceResp.Endpoints.ArrowFlight), + Catalog: types.StringValue(instanceResp.Endpoints.Catalog), + Ui: types.StringValue(instanceResp.Endpoints.Ui), + } + + return nil +} + +func mapAuthentication(instanceResp *dremioSdk.DremioResponse, model *InstanceModel) error { + if model == nil { + return fmt.Errorf("model input is nil") } - model.Endpoints = endpoints - authModel := &AuthenticationModel{ + authModel := AuthenticationModel{ Type: types.StringValue(instanceResp.Authentication.Type), } @@ -707,7 +733,7 @@ func mapFields(instanceResp *dremioSdk.DremioResponse, model *InstanceModel) err authModel.OAuth = oauthModel } - model.Authentication = authModel + model.Authentication = &authModel return nil } diff --git a/stackit/internal/services/dremio/instance/resource_test.go b/stackit/internal/services/dremio/instance/resource_test.go index d26aee738..accdb6174 100644 --- a/stackit/internal/services/dremio/instance/resource_test.go +++ b/stackit/internal/services/dremio/instance/resource_test.go @@ -6,7 +6,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" dremioSdk "github.com/stackitcloud/stackit-sdk-go/services/dremio/v1alphaapi" @@ -24,8 +23,10 @@ func TestMapFields(t *testing.T) { { "all_fields_filled", &InstanceModel{ - Region: types.StringValue("rid"), - ProjectId: types.StringValue("pid"), + Model: Model{ + Region: types.StringValue("rid"), + ProjectId: types.StringValue("pid"), + }, }, &dremioSdk.DremioResponse{ Id: instanceId, @@ -65,13 +66,24 @@ func TestMapFields(t *testing.T) { State: "active", }, &InstanceModel{ - Id: types.StringValue("pid,rid," + instanceId), + Model: Model{ + Id: types.StringValue("pid,rid," + instanceId), + + ProjectId: types.StringValue("pid"), + Region: types.StringValue("rid"), + InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue("pid"), - Region: types.StringValue("rid"), - InstanceId: types.StringValue(instanceId), + DisplayName: types.StringValue("greatName"), + Description: types.StringValue("minimal-required-values"), - DisplayName: types.StringValue("greatName"), + State: types.StringValue("active"), + ErrorMessage: types.StringNull(), + Endpoints: &EndpointsModel{ + ArrowFlight: types.StringValue("flight"), + Catalog: types.StringValue("catalog"), + Ui: types.StringValue("ui"), + }, + }, Authentication: &AuthenticationModel{ AzureAD: &AzureADModel{ AuthorityUrl: types.StringValue("azure-authority"), @@ -97,36 +109,24 @@ func TestMapFields(t *testing.T) { }, Type: types.StringValue("local-only"), }, - Description: types.StringValue("minimal-required-values"), - - State: types.StringValue("active"), - ErrorMessage: types.StringNull(), - Endpoints: types.ObjectValueMust( - map[string]attr.Type{ - "arrow_flight": types.StringType, - "catalog": types.StringType, - "ui": types.StringType, - }, - map[string]attr.Value{ - "arrow_flight": types.StringValue("flight"), - "catalog": types.StringValue("catalog"), - "ui": types.StringValue("ui"), - }, - ), }, false, }, { "nil response", &InstanceModel{ - Region: types.StringValue("rid"), - ProjectId: types.StringValue("pid"), + Model: Model{ + Region: types.StringValue("rid"), + ProjectId: types.StringValue("pid"), + }, }, nil, &InstanceModel{ - Id: types.StringValue("pid,rid,"), - ProjectId: types.StringValue("pid"), - Region: types.StringValue("rid"), + Model: Model{ + Id: types.StringValue("pid,rid,"), + ProjectId: types.StringValue("pid"), + Region: types.StringValue("rid"), + }, }, true, }, @@ -147,7 +147,7 @@ func TestMapFields(t *testing.T) { } if !tt.wantErr { if diff := cmp.Diff(tt.expected, tt.state); diff != "" { - t.Errorf("mapFields mismatch (-want +got):\n%s", diff) + t.Errorf("mapping mismatch (-want +got):\n%s", diff) } } }) @@ -164,6 +164,10 @@ func TestToCreatePayload(t *testing.T) { { "success", &InstanceModel{ + Model: Model{ + Description: types.StringValue("test description"), + DisplayName: types.StringValue("displayName"), + }, Authentication: &AuthenticationModel{ AzureAD: &AzureADModel{ AuthorityUrl: types.StringValue("azure-authority"), @@ -189,8 +193,6 @@ func TestToCreatePayload(t *testing.T) { }, Type: types.StringValue("oauth"), }, - Description: types.StringValue("test description"), - DisplayName: types.StringValue("displayName"), }, &dremioSdk.CreateDremioInstancePayload{ Authentication: &dremioSdk.Authentication{ @@ -257,6 +259,10 @@ func TestToUpdatePayload(t *testing.T) { { "success", &InstanceModel{ + Model: Model{ + Description: types.StringValue("test description"), + DisplayName: types.StringValue("displayName"), + }, Authentication: &AuthenticationModel{ AzureAD: &AzureADModel{ AuthorityUrl: types.StringValue("azure-authority"), @@ -282,8 +288,6 @@ func TestToUpdatePayload(t *testing.T) { }, Type: types.StringValue("oauth"), }, - Description: types.StringValue("test description"), - DisplayName: types.StringValue("displayName"), }, &dremioSdk.UpdateDremioInstancePayload{ Authentication: &dremioSdk.Authentication{ From 2e95b5bc7a92921a36b6413271a1c05ec06368bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20Schr=C3=B6der?= Date: Fri, 22 May 2026 19:46:16 +0200 Subject: [PATCH 08/12] feat(dremio): Adding dremio instance data resource. The data resource does not read all the fields. There is only one authentication setup read, because only one can be used at a time. Also we are not reading the client secret here. --- .../stackit_dremio_instance/data-source.tf | 5 + .../services/dremio/instance/datasource.go | 338 ++++++++++++++++++ stackit/provider.go | 2 + 3 files changed, 345 insertions(+) create mode 100644 examples/data-sources/stackit_dremio_instance/data-source.tf create mode 100644 stackit/internal/services/dremio/instance/datasource.go diff --git a/examples/data-sources/stackit_dremio_instance/data-source.tf b/examples/data-sources/stackit_dremio_instance/data-source.tf new file mode 100644 index 000000000..4f17e8514 --- /dev/null +++ b/examples/data-sources/stackit_dremio_instance/data-source.tf @@ -0,0 +1,5 @@ +data "stackit_dremio_instance" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + region = "eu01" + instance_id = "example-instance-id" +} \ No newline at end of file diff --git a/stackit/internal/services/dremio/instance/datasource.go b/stackit/internal/services/dremio/instance/datasource.go new file mode 100644 index 000000000..0e5d3b8c2 --- /dev/null +++ b/stackit/internal/services/dremio/instance/datasource.go @@ -0,0 +1,338 @@ +package dremio + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + dremioSdk "github.com/stackitcloud/stackit-sdk-go/services/dremio/v1alphaapi" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + dremioUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dremio/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ datasource.DataSource = &instanceDataSource{} + _ datasource.DataSourceWithConfigure = &instanceDataSource{} +) + +type InstanceDataSourceModel struct { + Model + + // Required Fields + Authentication *DataSourceAuthenticationModel `tfsdk:"authentication"` +} + +type DataSourceAuthenticationModel struct { + Type types.String `tfsdk:"type"` + AuthorityUrl types.String `tfsdk:"authority_url"` + ClientId types.String `tfsdk:"client_id"` + JwtClaims *JwtClaimsModel `tfsdk:"jwt_claims"` + Scope types.String `tfsdk:"scope"` + Parameters []AuthParameterModel `tfsdk:"parameters"` + RedirectUrl types.String `tfsdk:"redirect_url"` +} + +type instanceDataSource struct { + client *dremioSdk.APIClient +} + +func NewInstanceDataSource() datasource.DataSource { + return &instanceDataSource{} +} + +// Metadata should return the full name of the data source, such as +// examplecloud_thing. +func (d *instanceDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dremio_instance" +} + +// Configure enables provider-level data or clients to be set in the +// provider-defined DataSource type. It is separately executed for each +// ReadDataSource RPC. +func (d *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := dremioUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.client = apiClient + tflog.Info(ctx, "Dremio instance client configured for data source") +} + +// Schema should return the schema for this data source. +func (d *instanceDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + descriptions := map[string]string{ + "main": "Manages a STACKIT Dremio instance.", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`dremio_id`\".", + "project_id": "STACKIT Project ID to which the resource is associated.", + "instance_id": "The Dremio instance ID.", + "region": "The STACKIT region name the resource is located in. If not defined, the provider region is used.", + "display_name": "The display name is a short name chosen by the user to identify the resource.", + "description": "The description is a longer text chosen by the user to provide more context for the resource.", + "state": "The current state of the resource.", + "error_message": "A message describing an actionable error the user can resolve. This field is empty if no such error exists.", + "endpoints": "The available endpoints of the Dremio instance.", + "endpoints_arrow_flight": "The arrow flight endpoint of the Dremio instance.", + "endpoints_catalog": "The Apache Iceberg endpoint of the Dremio instance.", + "endpoints_ui": "The UI endpoint of the Dremio instance.", + "authentication": "Dremio instance authentication settings. A change here triggers a Dremio restart and will incur downtime.", + "authentication_type": "Type of authentication (local-only, azuread, oauth).", + "authentication_authority_url": "The Issuer location URI, where the OIDC provider configuration can be found.", + "authentication_client_id": "The client ID assigned by the Identity Provider.", + "authentication_scope": "A list of space-separated scopes. The `openid` scope is always required; other scopes can vary by provider.", + "authentication_redirect_url": "The URL where the Dremio instance is hosted. The URL must match the redirect URL set in the Identity Provider.", + "authentication_jwt_claims": "Maps fields from the JWT token to fields Dremio requires.", + "authentication_jwt_claims_user_name": "Mapped user name claim (e.g. email).", + "authentication_parameters": "Any additional parameters the Identity Provider requires.", + "authentication_parameters_name": "Parameter name.", + "authentication_parameters_value": "Parameter value.", + } + + resp.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "instance_id": schema.StringAttribute{ + Description: descriptions["instance_id"], + Required: true, + }, + "region": schema.StringAttribute{ + Description: descriptions["region"], + Required: true, + }, + "display_name": schema.StringAttribute{ + Description: descriptions["display_name"], + Computed: true, + }, + "description": schema.StringAttribute{ + Description: descriptions["description"], + Computed: true, + Optional: true, + }, + "state": schema.StringAttribute{ + Description: descriptions["state"], + Computed: true, + }, + "error_message": schema.StringAttribute{ + Description: descriptions["error_message"], + Computed: true, + Optional: true, + }, + "endpoints": schema.SingleNestedAttribute{ + Description: descriptions["endpoints"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "arrow_flight": schema.StringAttribute{ + Description: descriptions["endpoints_arrow_flight"], + Computed: true, + }, + "catalog": schema.StringAttribute{ + Description: descriptions["endpoints_catalog"], + Computed: true, + }, + "ui": schema.StringAttribute{ + Description: descriptions["endpoints_ui"], + Computed: true, + }, + }, + }, + "authentication": schema.SingleNestedAttribute{ + Description: descriptions["authentication"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Description: descriptions["authentication_type"], + Computed: true, + }, + "authority_url": schema.StringAttribute{ + Description: descriptions["oauth_authority_url"], + Computed: true, + Optional: true, + }, + "client_id": schema.StringAttribute{ + Description: descriptions["oauth_client_id"], + Computed: true, + Optional: true, + }, + "scope": schema.StringAttribute{ + Description: descriptions["oauth_scope"], + Computed: true, + Optional: true, + }, + "redirect_url": schema.StringAttribute{ + Description: descriptions["oauth_redirect_url"], + Computed: true, + Optional: true, + }, + "jwt_claims": schema.SingleNestedAttribute{ + Description: descriptions["oauth_jwt_claims"], + Computed: true, + Optional: true, + Attributes: map[string]schema.Attribute{ + "user_name": schema.StringAttribute{ + Description: descriptions["oauth_jwt_claims_user_name"], + Computed: true, + }, + }, + }, + "parameters": schema.ListNestedAttribute{ + Description: descriptions["oauth_parameters"], + Computed: true, + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: descriptions["oauth_parameters_name"], + Computed: true, + }, + "value": schema.StringAttribute{ + Description: descriptions["oauth_parameters_value"], + Computed: true, + }, + }, + }, + }, + }, + }, + }, + } +} + +// Read is called when the provider must read data source values in +// order to update state. Config values should be read from the +// ReadRequest and new state values set on the ReadResponse. +func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + // nolint:gocritic // function signature required by Terraform + var model InstanceDataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := model.Region.ValueString() + instanceId := model.InstanceId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + + instanceResp, err := d.client.DefaultAPI.GetDremioInstance(ctx, projectId, region, instanceId).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) { + if oapiErr.StatusCode == http.StatusNotFound { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading runner", fmt.Sprintf("Dremio instance with ID %s not found in project %s and region %s", instanceId, projectId, region)) + return + } + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading Dremio instance", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + err = mapDataSourceFields(instanceResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading Dremio instance", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set refreshed state + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Dremio instance read") +} + +func mapDataSourceFields(instanceResp *dremioSdk.DremioResponse, model *InstanceDataSourceModel) error { + if instanceResp == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + err := mapModelFields(instanceResp, &model.Model) + if err != nil { + return fmt.Errorf("failed to map Model fields") + } + err = mapDataSourceAuthentication(instanceResp, model) + if err != nil { + return fmt.Errorf("failed to map Authentication fields") + } + + return nil +} + +func mapDataSourceAuthentication(instanceResp *dremioSdk.DremioResponse, model *InstanceDataSourceModel) error { + authResp := instanceResp.Authentication + + authModel := DataSourceAuthenticationModel{} + + authModel.Type = types.StringValue(authResp.Type) + + if instanceResp.Authentication.Type == "local-only" { + // On local auth we don't need to map IDP fields + return nil + } + + if authResp.Type == "azuread" { + azureADResp := authResp.Azuread + authModel.AuthorityUrl = types.StringValue(azureADResp.AuthorityUrl) + authModel.ClientId = types.StringValue(azureADResp.ClientId) + authModel.RedirectUrl = types.StringPointerValue(azureADResp.RedirectUrl) + } + + if authResp.Type == "oauth" { + oauthResp := authResp.Oauth + authModel.AuthorityUrl = types.StringValue(oauthResp.AuthorityUrl) + authModel.ClientId = types.StringValue(oauthResp.ClientId) + authModel.Scope = types.StringPointerValue(oauthResp.Scope) + authModel.RedirectUrl = types.StringPointerValue(oauthResp.RedirectUrl) + authModel.JwtClaims = &JwtClaimsModel{ + UserName: types.StringValue(oauthResp.JwtClaims.UserName), + } + + if len(oauthResp.Parameters) > 0 { + var params []AuthParameterModel + for _, p := range oauthResp.Parameters { + params = append(params, AuthParameterModel{ + Name: types.StringValue(p.Name), + Value: types.StringValue(p.Value), + }) + } + authModel.Parameters = params + } + } + + model.Authentication = &authModel + + return nil +} diff --git a/stackit/provider.go b/stackit/provider.go index d9996e6b3..ec776b73b 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -30,6 +30,7 @@ import ( cdn "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/distribution" dnsRecordSet "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/recordset" dnsZone "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/zone" + dremio "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dremio/instance" dremioInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dremio/instance" edgeCloudInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/edgecloud/instance" edgeCloudInstances "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/edgecloud/instances" @@ -653,6 +654,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource cdnCustomDomain.NewCustomDomainDataSource, dnsZone.NewZoneDataSource, dnsRecordSet.NewRecordSetDataSource, + dremio.NewInstanceDataSource, edgeCloudInstances.NewInstancesDataSource, edgeCloudPlans.NewPlansDataSource, gitInstance.NewGitDataSource, From 59b7cb39f72f4cdc86461c2bd20242879fbe85d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20Schr=C3=B6der?= Date: Fri, 22 May 2026 19:54:19 +0200 Subject: [PATCH 09/12] fix(dremio): Linting & Formatting --- .../stackit_dremio_instance/data-source.tf | 6 +++--- .../resources/stackit_dremio_instance/resource.tf | 12 ++++++------ .../internal/services/dremio/instance/datasource.go | 13 ++++++++----- .../internal/services/dremio/instance/resource.go | 2 +- stackit/provider.go | 3 +-- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/examples/data-sources/stackit_dremio_instance/data-source.tf b/examples/data-sources/stackit_dremio_instance/data-source.tf index 4f17e8514..e39553403 100644 --- a/examples/data-sources/stackit_dremio_instance/data-source.tf +++ b/examples/data-sources/stackit_dremio_instance/data-source.tf @@ -1,5 +1,5 @@ data "stackit_dremio_instance" "example" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - region = "eu01" - instance_id = "example-instance-id" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + region = "eu01" + instance_id = "example-instance-id" } \ No newline at end of file diff --git a/examples/resources/stackit_dremio_instance/resource.tf b/examples/resources/stackit_dremio_instance/resource.tf index 5cf5d6c1a..7f8042576 100644 --- a/examples/resources/stackit_dremio_instance/resource.tf +++ b/examples/resources/stackit_dremio_instance/resource.tf @@ -1,27 +1,27 @@ resource "stackit_dremio_instance" "example" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - region = "eu01" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + region = "eu01" display_name = "exampleName" - description = "Example description" + description = "Example description" authentication = { type = "local-only" // "oauth" or "azuread" for IDP config oauth = { // only needed if "oauth" is given as type authority_url = "authority" - client_id = "client-id" + client_id = "client-id" client_secret = "client-secret" jwt_claims = { user_name = "example" } scope = "idp-scope" parameters = [ - {"name": "example", "value": "example-value"} + { "name" : "example", "value" : "example-value" } ] } azuread = { // only needed if "azuread" is given as type authority_url = "authority" - client_id = "client-id" + client_id = "client-id" client_secret = "client-secret" } } diff --git a/stackit/internal/services/dremio/instance/datasource.go b/stackit/internal/services/dremio/instance/datasource.go index 0e5d3b8c2..ed64c3b36 100644 --- a/stackit/internal/services/dremio/instance/datasource.go +++ b/stackit/internal/services/dremio/instance/datasource.go @@ -12,11 +12,14 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - dremioSdk "github.com/stackitcloud/stackit-sdk-go/services/dremio/v1alphaapi" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - dremioUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dremio/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + + dremioSdk "github.com/stackitcloud/stackit-sdk-go/services/dremio/v1alphaapi" + + dremioUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dremio/utils" ) var ( @@ -51,7 +54,7 @@ func NewInstanceDataSource() datasource.DataSource { // Metadata should return the full name of the data source, such as // examplecloud_thing. -func (d *instanceDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { +func (d *instanceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_dremio_instance" } @@ -73,7 +76,7 @@ func (d *instanceDataSource) Configure(ctx context.Context, req datasource.Confi } // Schema should return the schema for this data source. -func (d *instanceDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { descriptions := map[string]string{ "main": "Manages a STACKIT Dremio instance.", "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`dremio_id`\".", @@ -225,7 +228,7 @@ func (d *instanceDataSource) Schema(ctx context.Context, req datasource.SchemaRe // Read is called when the provider must read data source values in // order to update state. Config values should be read from the // ReadRequest and new state values set on the ReadResponse. -func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { +func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform // nolint:gocritic // function signature required by Terraform var model InstanceDataSourceModel resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) diff --git a/stackit/internal/services/dremio/instance/resource.go b/stackit/internal/services/dremio/instance/resource.go index d95fa35d7..85874ee5e 100644 --- a/stackit/internal/services/dremio/instance/resource.go +++ b/stackit/internal/services/dremio/instance/resource.go @@ -172,7 +172,7 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure } func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ + descriptions := map[string]string{ //nolint:gosec // no hardcoded credentials in here "main": "Manages a STACKIT Dremio instance.", "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`dremio_id`\".", "project_id": "STACKIT Project ID to which the resource is associated.", diff --git a/stackit/provider.go b/stackit/provider.go index ec776b73b..a181cbbdf 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -30,7 +30,6 @@ import ( cdn "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/distribution" dnsRecordSet "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/recordset" dnsZone "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/zone" - dremio "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dremio/instance" dremioInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dremio/instance" edgeCloudInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/edgecloud/instance" edgeCloudInstances "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/edgecloud/instances" @@ -654,7 +653,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource cdnCustomDomain.NewCustomDomainDataSource, dnsZone.NewZoneDataSource, dnsRecordSet.NewRecordSetDataSource, - dremio.NewInstanceDataSource, + dremioInstance.NewInstanceDataSource, edgeCloudInstances.NewInstancesDataSource, edgeCloudPlans.NewPlansDataSource, gitInstance.NewGitDataSource, From add31d7eba1ae1f721b4b9fecf571fb690f00b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20Schr=C3=B6der?= Date: Mon, 1 Jun 2026 11:14:55 +0200 Subject: [PATCH 10/12] feat(dremio): Adding acceptance tests for Dremio instance. --- .../services/dremio/dremio_acc_test.go | 374 ++++++++++++++++++ .../services/dremio/instance/datasource.go | 5 - .../services/dremio/instance/resource.go | 106 +++-- .../services/dremio/instance/resource_test.go | 244 ++++++++++-- .../services/dremio/testdata/resource-max.tf | 60 +++ .../services/dremio/testdata/resource-min.tf | 20 + stackit/internal/testutil/testutil.go | 1 + 7 files changed, 750 insertions(+), 60 deletions(-) create mode 100644 stackit/internal/services/dremio/dremio_acc_test.go create mode 100644 stackit/internal/services/dremio/testdata/resource-max.tf create mode 100644 stackit/internal/services/dremio/testdata/resource-min.tf diff --git a/stackit/internal/services/dremio/dremio_acc_test.go b/stackit/internal/services/dremio/dremio_acc_test.go new file mode 100644 index 000000000..8550bde99 --- /dev/null +++ b/stackit/internal/services/dremio/dremio_acc_test.go @@ -0,0 +1,374 @@ +package dremio + +import ( + "context" + _ "embed" + "fmt" + "maps" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/stackitcloud/stackit-sdk-go/core/utils" + dremioSdk "github.com/stackitcloud/stackit-sdk-go/services/dremio/v1alphaapi" + dremioWaiter "github.com/stackitcloud/stackit-sdk-go/services/dremio/v1alphaapi/wait/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" +) + +//go:embed testdata/resource-min.tf +var resourceDremioInstanceMin string + +//go:embed testdata/resource-max.tf +var resourceDremioInstanceMax string + +const dremioInstanceResource = "stackit_dremio_instance.example" +const dremioInstanceDataResource = "data.stackit_dremio_instance.example" + +var testDremioInstanceConfigVarsMin = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "region": config.StringVariable(testutil.Region), + "display_name": config.StringVariable("dremioMinInstance"), + "authentication_type": config.StringVariable("local-only"), +} + +var testDremioInstanceConfigVarsMax = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "region": config.StringVariable("eu01"), + "display_name": config.StringVariable("dremioMaxInstance"), + "description": config.StringVariable("description"), + + "authentication_type": config.StringVariable("oauth"), + + "authentication_oauth_authority_url": config.StringVariable("oauth-authority-url"), + "authentication_oauth_client_id": config.StringVariable("oauth-client-id"), + "authentication_oauth_client_secret": config.StringVariable("oauth-client-secret"), + "authentication_oauth_client_jwt_claims_user_name": config.StringVariable("oauth-jwt-claim-user"), + "authentication_oauth_scope": config.StringVariable("oauth-scope"), + "authentication_oauth_parameter_name": config.StringVariable("oauth-parameter-name"), + "authentication_oauth_parameter_value": config.StringVariable("oauth-parameter-value"), +} + +func testDremioInstanceConfigVarsMinUpdated() config.Variables { + tempConfig := make(config.Variables, len(testDremioInstanceConfigVarsMin)) + maps.Copy(tempConfig, testDremioInstanceConfigVarsMin) + tempConfig["display_name"] = config.StringVariable("dremioMinInstanceUpd") + return tempConfig +} + +func testDremioInstanceConfigVarsMaxUpdated() config.Variables { + tempConfig := make(config.Variables, len(testDremioInstanceConfigVarsMax)) + maps.Copy(tempConfig, testDremioInstanceConfigVarsMax) + tempConfig["display_name"] = config.StringVariable("dremioMaxInstanceUpd") + tempConfig["description"] = config.StringVariable("description-upd") + + // switching idp to azuread + tempConfig["authentication_type"] = config.StringVariable("azuread") + + tempConfig["authentication_azuread_authority_url"] = config.StringVariable("azuread-authority-url-upd") + tempConfig["authentication_azuread_client_id"] = config.StringVariable("azuread-client-id-upd") + tempConfig["authentication_azuread_client_secret"] = config.StringVariable("azuread-client-secret-upd") + + return tempConfig +} + +func TestDremioInstanceMin(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccDremioInstanceDestroy, + Steps: []resource.TestStep{ + // 1) Creation + { + Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + resourceDremioInstanceMin, + ConfigVariables: testDremioInstanceConfigVarsMin, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(dremioInstanceResource, "project_id", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMin["project_id"])), + resource.TestCheckResourceAttr(dremioInstanceResource, "region", testutil.Region), + resource.TestCheckResourceAttr(dremioInstanceResource, "display_name", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMin["display_name"])), + resource.TestCheckResourceAttr(dremioInstanceResource, "authentication.type", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMin["authentication_type"])), + + resource.TestCheckResourceAttrSet(dremioInstanceResource, "instance_id"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "id"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "state"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "endpoints.ui"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "endpoints.arrow_flight"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "endpoints.catalog"), + ), + }, + // 2) Data Source + { + Config: testutil.NewConfigBuilder().BuildProviderConfig() + resourceDremioInstanceMin, + ConfigVariables: testDremioInstanceConfigVarsMin, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "project_id", + dremioInstanceDataResource, "project_id", + ), + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "region", + dremioInstanceDataResource, "region", + ), + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "instance_id", + dremioInstanceDataResource, "instance_id", + ), + + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "display_name", + dremioInstanceDataResource, "display_name", + ), + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "authentication.type", + dremioInstanceDataResource, "authentication.type", + ), + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "endpoints.arrow_flight", + dremioInstanceDataResource, "endpoints.arrow_flight", + ), + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "catalog", + dremioInstanceDataResource, "catalog", + ), + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "endpoints.ui", + dremioInstanceDataResource, "endpoints.ui", + ), + ), + }, + // 3) Import + { + ConfigVariables: testDremioInstanceConfigVarsMin, + ResourceName: dremioInstanceResource, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources[dremioInstanceResource] + if !ok { + return "", fmt.Errorf("couldn't find resource %s", dremioInstanceResource) + } + instanceId, ok := r.Primary.Attributes["instance_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute instanceId") + } + + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, instanceId), nil + }, + }, + // 4) Update + { + Config: testutil.NewConfigBuilder().BuildProviderConfig() + resourceDremioInstanceMin, + ConfigVariables: testDremioInstanceConfigVarsMinUpdated(), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(dremioInstanceResource, "project_id", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMin["project_id"])), + resource.TestCheckResourceAttr(dremioInstanceResource, "region", testutil.Region), + resource.TestCheckResourceAttr(dremioInstanceResource, "display_name", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMin["display_name"])), + resource.TestCheckResourceAttr(dremioInstanceResource, "authentication.type", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMin["authentication_type"])), + + resource.TestCheckResourceAttrSet(dremioInstanceResource, "instance_id"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "id"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "state"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "endpoints.ui"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "endpoints.arrow_flight"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "endpoints.catalog"), + ), + }, + }, + }) +} + +func TestDremioInstanceMax(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccDremioInstanceDestroy, + Steps: []resource.TestStep{ + // 1) Creation + { + ConfigVariables: testDremioInstanceConfigVarsMax, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + resourceDremioInstanceMax, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(dremioInstanceResource, "project_id", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMax["project_id"])), + resource.TestCheckResourceAttr(dremioInstanceResource, "region", testutil.Region), + resource.TestCheckResourceAttr(dremioInstanceResource, "display_name", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMax["display_name"])), + resource.TestCheckResourceAttr(dremioInstanceResource, "description", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMax["description"])), + + resource.TestCheckResourceAttr(dremioInstanceResource, "authentication.type", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMax["authentication_type"])), + + resource.TestCheckResourceAttr(dremioInstanceResource, "authentication.oauth.authority_url", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMax["authentication_oauth_authority_url"])), + resource.TestCheckResourceAttr(dremioInstanceResource, "authentication.oauth.client_id", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMax["authentication_oauth_client_id"])), + resource.TestCheckResourceAttr(dremioInstanceResource, "authentication.oauth.client_secret", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMax["authentication_oauth_client_secret"])), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "authentication.oauth.redirect_url"), + resource.TestCheckResourceAttr(dremioInstanceResource, "authentication.oauth.jwt_claims.user_name", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMax["authentication_oauth_client_jwt_claims_user_name"])), + resource.TestCheckResourceAttr(dremioInstanceResource, "authentication.oauth.scope", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMax["authentication_oauth_scope"])), + resource.TestCheckResourceAttr(dremioInstanceResource, "authentication.oauth.parameters.0.name", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMax["authentication_oauth_parameter_name"])), + resource.TestCheckResourceAttr(dremioInstanceResource, "authentication.oauth.parameters.0.value", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMax["authentication_oauth_parameter_value"])), + + resource.TestCheckResourceAttrSet(dremioInstanceResource, "instance_id"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "id"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "state"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "endpoints.ui"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "endpoints.arrow_flight"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "endpoints.catalog"), + ), + }, + // 2) Data Source + { + Config: testutil.NewConfigBuilder().BuildProviderConfig() + resourceDremioInstanceMax, + ConfigVariables: testDremioInstanceConfigVarsMax, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "project_id", + dremioInstanceDataResource, "project_id", + ), + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "region", + dremioInstanceDataResource, "region", + ), + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "instance_id", + dremioInstanceDataResource, "instance_id", + ), + + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "display_name", + dremioInstanceDataResource, "display_name", + ), + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "description", + dremioInstanceDataResource, "description", + ), + + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "authentication.type", + dremioInstanceDataResource, "authentication.type", + ), + // Authentication on the data source only shows the currently set IDP config, + // which is oauth for the config here. Hence why we test for the oauth value here. + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "authentication.oauth.authority_url", + dremioInstanceDataResource, "authentication.authority_url", + ), + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "authentication.oauth.client_id", + dremioInstanceDataResource, "authentication.client_id", + ), + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "authentication.oauth.scope", + dremioInstanceDataResource, "authentication.scope", + ), + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "authentication.oauth.parameters", + dremioInstanceDataResource, "authentication.parameters", + ), + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "authentication.oauth.redirect_url", + dremioInstanceDataResource, "authentication.redirect_url", + ), + + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "endpoints.arrow_flight", + dremioInstanceDataResource, "endpoints.arrow_flight", + ), + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "catalog", + dremioInstanceDataResource, "catalog", + ), + resource.TestCheckResourceAttrPair( + dremioInstanceResource, "endpoints.ui", + dremioInstanceDataResource, "endpoints.ui", + ), + ), + }, + // 3) Import + { + ConfigVariables: testDremioInstanceConfigVarsMax, + ResourceName: dremioInstanceResource, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources[dremioInstanceResource] + if !ok { + return "", fmt.Errorf("couldn't find resource %s", dremioInstanceResource) + } + instanceId, ok := r.Primary.Attributes["instance_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute instanceId") + } + + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, instanceId), nil + }}, + // 4) Update + { + Config: testutil.NewConfigBuilder().BuildProviderConfig() + resourceDremioInstanceMax, + ConfigVariables: testDremioInstanceConfigVarsMaxUpdated(), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(dremioInstanceResource, "project_id", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMaxUpdated()["project_id"])), + resource.TestCheckResourceAttr(dremioInstanceResource, "region", testutil.Region), + resource.TestCheckResourceAttr(dremioInstanceResource, "display_name", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMaxUpdated()["display_name"])), + resource.TestCheckResourceAttr(dremioInstanceResource, "description", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMaxUpdated()["description"])), + + resource.TestCheckResourceAttr(dremioInstanceResource, "authentication.type", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMaxUpdated()["authentication_type"])), + + resource.TestCheckResourceAttr(dremioInstanceResource, "authentication.azuread.authority_url", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMaxUpdated()["authentication_azuread_authority_url"])), + resource.TestCheckResourceAttr(dremioInstanceResource, "authentication.azuread.client_id", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMaxUpdated()["authentication_azuread_client_id"])), + resource.TestCheckResourceAttr(dremioInstanceResource, "authentication.azuread.client_secret", testutil.ConvertConfigVariable(testDremioInstanceConfigVarsMaxUpdated()["authentication_azuread_client_secret"])), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "authentication.azuread.redirect_url"), + + resource.TestCheckResourceAttrSet(dremioInstanceResource, "instance_id"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "id"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "state"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "endpoints.ui"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "endpoints.arrow_flight"), + resource.TestCheckResourceAttrSet(dremioInstanceResource, "endpoints.catalog"), + ), + }, + }, + }) +} + +func testAccDremioInstanceDestroy(s *terraform.State) error { + ctx := context.Background() + client, err := dremioSdk.NewAPIClient( + testutil.NewConfigBuilder().BuildClientOptions(testutil.DremioCustomEndpoint, true)...) + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + instancesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_dremio_instance" { + continue + } + // Dremio internal ID: "[project_id],[region],[instance_id]" + instanceId := strings.Split(rs.Primary.ID, core.Separator)[2] + instancesToDestroy = append(instancesToDestroy, instanceId) + } + + // List all resources in the project/region to see what's left + instancesResp, err := client.DefaultAPI.ListDremioInstances(ctx, testutil.ProjectId, testutil.Region).Execute() + if err != nil { + return fmt.Errorf("getting instancesResp: %w", err) + } + + // If the API returns a list of runners, check if our deleted ones are still there + items := instancesResp.Dremios + for i := range items { + // If a runner we thought we deleted is found in the list + if utils.Contains(instancesToDestroy, items[i].Id) { + // Attempt a final delete and wait, just like Postgres + err := client.DefaultAPI.DeleteDremioInstance(ctx, testutil.ProjectId, testutil.Region, items[i].Id).Execute() + if err != nil { + return fmt.Errorf("deleting Dremio instance %s during CheckDestroy: %w", items[i].Id, err) + } + + // Using the wait handler for destruction verification + _, err = dremioWaiter.DeleteDremioWaitHandler(ctx, client.DefaultAPI, testutil.ProjectId, testutil.Region, items[i].Id).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("deleting Dremio instance %s during CheckDestroy: waiting for deletion %w", items[i].Id, err) + } + } + } + return nil +} diff --git a/stackit/internal/services/dremio/instance/datasource.go b/stackit/internal/services/dremio/instance/datasource.go index ed64c3b36..410776b08 100644 --- a/stackit/internal/services/dremio/instance/datasource.go +++ b/stackit/internal/services/dremio/instance/datasource.go @@ -301,11 +301,6 @@ func mapDataSourceAuthentication(instanceResp *dremioSdk.DremioResponse, model * authModel.Type = types.StringValue(authResp.Type) - if instanceResp.Authentication.Type == "local-only" { - // On local auth we don't need to map IDP fields - return nil - } - if authResp.Type == "azuread" { azureADResp := authResp.Azuread authModel.AuthorityUrl = types.StringValue(azureADResp.AuthorityUrl) diff --git a/stackit/internal/services/dremio/instance/resource.go b/stackit/internal/services/dremio/instance/resource.go index 85874ee5e..91b16e95a 100644 --- a/stackit/internal/services/dremio/instance/resource.go +++ b/stackit/internal/services/dremio/instance/resource.go @@ -8,10 +8,12 @@ import ( "strings" "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -50,9 +52,9 @@ type Model struct { Description types.String `tfsdk:"description"` // Read-only Fields - State types.String `tfsdk:"state"` - ErrorMessage types.String `tfsdk:"error_message"` - Endpoints *EndpointsModel `tfsdk:"endpoints"` + State types.String `tfsdk:"state"` + ErrorMessage types.String `tfsdk:"error_message"` + Endpoints types.Object `tfsdk:"endpoints"` // see EdnpointsModel } // InstanceModel maps the resource schema data. @@ -116,6 +118,12 @@ type EndpointsModel struct { Ui types.String `tfsdk:"ui"` } +var endpointsAttrTypes = map[string]attr.Type{ + "arrow_flight": types.StringType, + "catalog": types.StringType, + "ui": types.StringType, +} + func NewInstanceResource() resource.Resource { return &instanceResource{} } @@ -248,6 +256,8 @@ func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest, "description": schema.StringAttribute{ Description: descriptions["description"], Optional: true, + Computed: true, // Must be computed if a default is applied + Default: stringdefault.StaticString(""), PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, @@ -314,7 +324,7 @@ func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest, "client_secret": schema.StringAttribute{ Description: descriptions["azuread_client_secret"], Required: true, - Sensitive: true, + // Sensitive: true, }, "redirect_url": schema.StringAttribute{ Description: descriptions["azuread_redirect_url"], @@ -337,7 +347,7 @@ func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest, "client_secret": schema.StringAttribute{ Description: descriptions["oauth_client_secret"], Required: true, - Sensitive: true, + // Sensitive: true, }, "scope": schema.StringAttribute{ Description: descriptions["oauth_scope"], @@ -678,11 +688,16 @@ func mapModelFields(instanceResp *dremioSdk.DremioResponse, model *Model) error model.Description = types.StringPointerValue(instanceResp.Description) model.ErrorMessage = types.StringPointerValue(instanceResp.ErrorMessage) - model.Endpoints = &EndpointsModel{ + endpoints := &EndpointsModel{ ArrowFlight: types.StringValue(instanceResp.Endpoints.ArrowFlight), Catalog: types.StringValue(instanceResp.Endpoints.Catalog), Ui: types.StringValue(instanceResp.Endpoints.Ui), } + endpointsObj, diags := types.ObjectValueFrom(context.Background(), endpointsAttrTypes, endpoints) + if diags.HasError() { + return fmt.Errorf("failed to parse endpoints") + } + model.Endpoints = endpointsObj return nil } @@ -744,8 +759,13 @@ func toUpdatePayload(model *InstanceModel) (*dremioSdk.UpdateDremioInstancePaylo return nil, fmt.Errorf("nil model") } + authentication, err := parseAuthentication(model) + if err != nil { + return nil, fmt.Errorf("failed to parse authentication: %v", err) + } + return &dremioSdk.UpdateDremioInstancePayload{ - Authentication: parseAuthentication(model), + Authentication: authentication, Description: model.Description.ValueStringPointer(), DisplayName: model.DisplayName.ValueStringPointer(), }, nil @@ -757,26 +777,41 @@ func toCreatePayload(model *InstanceModel) (*dremioSdk.CreateDremioInstancePaylo return nil, fmt.Errorf("nil model") } + authentication, err := parseAuthentication(model) + if err != nil { + return nil, fmt.Errorf("failed to parse authentication: %v", err) + } + return &dremioSdk.CreateDremioInstancePayload{ - Authentication: parseAuthentication(model), + Authentication: authentication, Description: model.Description.ValueStringPointer(), DisplayName: model.DisplayName.ValueString(), }, nil } -func parseAuthentication(model *InstanceModel) *dremioSdk.Authentication { - var azureAdPayload *dremioSdk.Azuread - if model.Authentication.AzureAD != nil { - azureAdPayload = &dremioSdk.Azuread{ - AuthorityUrl: model.Authentication.AzureAD.AuthorityUrl.ValueString(), - ClientId: model.Authentication.AzureAD.ClientId.ValueString(), - ClientSecret: model.Authentication.AzureAD.ClientSecret.ValueString(), - RedirectUrl: model.Authentication.AzureAD.RedirectUrl.ValueStringPointer(), +func parseAuthentication(model *InstanceModel) (*dremioSdk.Authentication, error) { + // API only saves the block of the stated type. The other one is omitted. + // Keeping the block in TF leads to inconsistent state. Therefore we have + // make sure the type matches the existing block. + + switch model.Authentication.Type.ValueString() { + case "local-only": + if !(model.Authentication.OAuth == nil) || !(model.Authentication.AzureAD == nil) { + return nil, fmt.Errorf("can't state idp config if auth type is local-only") + } + return &dremioSdk.Authentication{ + Azuread: nil, + Oauth: nil, + Type: model.Authentication.Type.ValueString(), + }, nil + case "oauth": + if !(model.Authentication.AzureAD == nil) { + return nil, fmt.Errorf("can't state azure idp config if auth type is oauth") + } + if model.Authentication.OAuth == nil { + return nil, fmt.Errorf("missing oauth idp config") } - } - var oAuthPayload *dremioSdk.Oauth - if model.Authentication.OAuth != nil { oAuthParams := []dremioSdk.AuthParameters{} if len(model.Authentication.OAuth.Parameters) > 0 { parameters := model.Authentication.OAuth.Parameters @@ -788,7 +823,7 @@ func parseAuthentication(model *InstanceModel) *dremioSdk.Authentication { } } - oAuthPayload = &dremioSdk.Oauth{ + oAuthPayload := &dremioSdk.Oauth{ AuthorityUrl: model.Authentication.OAuth.AuthorityUrl.ValueString(), ClientId: model.Authentication.OAuth.ClientId.ValueString(), ClientSecret: model.Authentication.OAuth.ClientSecret.ValueString(), @@ -799,11 +834,32 @@ func parseAuthentication(model *InstanceModel) *dremioSdk.Authentication { Scope: model.Authentication.OAuth.Scope.ValueStringPointer(), Parameters: oAuthParams, } - } - return &dremioSdk.Authentication{ - Azuread: azureAdPayload, - Oauth: oAuthPayload, - Type: model.Authentication.Type.ValueString(), + return &dremioSdk.Authentication{ + Azuread: nil, + Oauth: oAuthPayload, + Type: model.Authentication.Type.ValueString(), + }, nil + case "azuread": + if !(model.Authentication.OAuth == nil) { + return nil, fmt.Errorf("can't state oauth idp config if auth type is azuread") + } + if model.Authentication.AzureAD == nil { + return nil, fmt.Errorf("missing azuread config") + } + + azureAdPayload := &dremioSdk.Azuread{ + AuthorityUrl: model.Authentication.AzureAD.AuthorityUrl.ValueString(), + ClientId: model.Authentication.AzureAD.ClientId.ValueString(), + ClientSecret: model.Authentication.AzureAD.ClientSecret.ValueString(), + RedirectUrl: model.Authentication.AzureAD.RedirectUrl.ValueStringPointer(), + } + return &dremioSdk.Authentication{ + Azuread: azureAdPayload, + Oauth: nil, + Type: model.Authentication.Type.ValueString(), + }, nil + default: + return nil, fmt.Errorf("unknown authentication type: %s", model.Authentication.Type) } } diff --git a/stackit/internal/services/dremio/instance/resource_test.go b/stackit/internal/services/dremio/instance/resource_test.go index accdb6174..1bd1845e1 100644 --- a/stackit/internal/services/dremio/instance/resource_test.go +++ b/stackit/internal/services/dremio/instance/resource_test.go @@ -6,6 +6,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" dremioSdk "github.com/stackitcloud/stackit-sdk-go/services/dremio/v1alphaapi" @@ -78,11 +79,18 @@ func TestMapFields(t *testing.T) { State: types.StringValue("active"), ErrorMessage: types.StringNull(), - Endpoints: &EndpointsModel{ - ArrowFlight: types.StringValue("flight"), - Catalog: types.StringValue("catalog"), - Ui: types.StringValue("ui"), - }, + Endpoints: types.ObjectValueMust( + map[string]attr.Type{ + "arrow_flight": types.StringType, + "catalog": types.StringType, + "ui": types.StringType, + }, + map[string]attr.Value{ + "arrow_flight": types.StringValue("flight"), + "catalog": types.StringValue("catalog"), + "ui": types.StringValue("ui"), + }, + ), }, Authentication: &AuthenticationModel{ AzureAD: &AzureADModel{ @@ -162,19 +170,33 @@ func TestToCreatePayload(t *testing.T) { wantErr bool }{ { - "success", + "success-local", + &InstanceModel{ + Model: Model{ + Description: types.StringValue("test description"), + DisplayName: types.StringValue("displayName"), + }, + Authentication: &AuthenticationModel{ + Type: types.StringValue("local-only"), + }, + }, + &dremioSdk.CreateDremioInstancePayload{ + Authentication: &dremioSdk.Authentication{ + Type: "local-only", + }, + Description: utils.Ptr("test description"), + DisplayName: "displayName", + }, + false, + }, + { + "success-oauth", &InstanceModel{ Model: Model{ Description: types.StringValue("test description"), DisplayName: types.StringValue("displayName"), }, Authentication: &AuthenticationModel{ - AzureAD: &AzureADModel{ - AuthorityUrl: types.StringValue("azure-authority"), - ClientId: types.StringValue("azure-client"), - ClientSecret: types.StringValue("azure-secret"), - RedirectUrl: types.StringValue("azure-redirect"), - }, OAuth: &OAuthModel{ AuthorityUrl: types.StringValue("oauth-authority"), ClientId: types.StringValue("oauth-client"), @@ -196,12 +218,6 @@ func TestToCreatePayload(t *testing.T) { }, &dremioSdk.CreateDremioInstancePayload{ Authentication: &dremioSdk.Authentication{ - Azuread: &dremioSdk.Azuread{ - AuthorityUrl: "azure-authority", - ClientId: "azure-client", - ClientSecret: "azure-secret", - RedirectUrl: utils.Ptr("azure-redirect"), - }, Oauth: &dremioSdk.Oauth{ AuthorityUrl: "oauth-authority", ClientId: "oauth-client", @@ -225,6 +241,86 @@ func TestToCreatePayload(t *testing.T) { }, false, }, + { + "success-azuread", + &InstanceModel{ + Model: Model{ + Description: types.StringValue("test description"), + DisplayName: types.StringValue("displayName"), + }, + Authentication: &AuthenticationModel{ + AzureAD: &AzureADModel{ + AuthorityUrl: types.StringValue("azure-authority"), + ClientId: types.StringValue("azure-client"), + ClientSecret: types.StringValue("azure-secret"), + RedirectUrl: types.StringValue("azure-redirect"), + }, + Type: types.StringValue("azuread"), + }, + }, + &dremioSdk.CreateDremioInstancePayload{ + Authentication: &dremioSdk.Authentication{ + Azuread: &dremioSdk.Azuread{ + AuthorityUrl: "azure-authority", + ClientId: "azure-client", + ClientSecret: "azure-secret", + RedirectUrl: utils.Ptr("azure-redirect"), + }, + Type: "azuread", + }, + Description: utils.Ptr("test description"), + DisplayName: "displayName", + }, + false, + }, + { + "idp-config-mismatch-local", + &InstanceModel{ + Model: Model{ + Description: types.StringValue("test description"), + DisplayName: types.StringValue("displayName"), + }, + Authentication: &AuthenticationModel{ + AzureAD: &AzureADModel{ + AuthorityUrl: types.StringValue("azure-authority"), + ClientId: types.StringValue("azure-client"), + ClientSecret: types.StringValue("azure-secret"), + RedirectUrl: types.StringValue("azure-redirect"), + }, + Type: types.StringValue("local-only"), + }, + }, + nil, + true, + }, + { + "missing-idp-config-oauth", + &InstanceModel{ + Model: Model{ + Description: types.StringValue("test description"), + DisplayName: types.StringValue("displayName"), + }, + Authentication: &AuthenticationModel{ + Type: types.StringValue("oauth"), + }, + }, + nil, + true, + }, + { + "missing-idp-config-azuread", + &InstanceModel{ + Model: Model{ + Description: types.StringValue("test description"), + DisplayName: types.StringValue("displayName"), + }, + Authentication: &AuthenticationModel{ + Type: types.StringValue("azuread"), + }, + }, + nil, + true, + }, { "nil model", nil, @@ -264,12 +360,26 @@ func TestToUpdatePayload(t *testing.T) { DisplayName: types.StringValue("displayName"), }, Authentication: &AuthenticationModel{ - AzureAD: &AzureADModel{ - AuthorityUrl: types.StringValue("azure-authority"), - ClientId: types.StringValue("azure-client"), - ClientSecret: types.StringValue("azure-secret"), - RedirectUrl: types.StringValue("azure-redirect"), - }, + Type: types.StringValue("local-only"), + }, + }, + &dremioSdk.UpdateDremioInstancePayload{ + Authentication: &dremioSdk.Authentication{ + Type: "local-only", + }, + Description: utils.Ptr("test description"), + DisplayName: utils.Ptr("displayName"), + }, + false, + }, + { + "success-oauth", + &InstanceModel{ + Model: Model{ + Description: types.StringValue("test description"), + DisplayName: types.StringValue("displayName"), + }, + Authentication: &AuthenticationModel{ OAuth: &OAuthModel{ AuthorityUrl: types.StringValue("oauth-authority"), ClientId: types.StringValue("oauth-client"), @@ -291,12 +401,6 @@ func TestToUpdatePayload(t *testing.T) { }, &dremioSdk.UpdateDremioInstancePayload{ Authentication: &dremioSdk.Authentication{ - Azuread: &dremioSdk.Azuread{ - AuthorityUrl: "azure-authority", - ClientId: "azure-client", - ClientSecret: "azure-secret", - RedirectUrl: utils.Ptr("azure-redirect"), - }, Oauth: &dremioSdk.Oauth{ AuthorityUrl: "oauth-authority", ClientId: "oauth-client", @@ -320,6 +424,86 @@ func TestToUpdatePayload(t *testing.T) { }, false, }, + { + "success-azuread", + &InstanceModel{ + Model: Model{ + Description: types.StringValue("test description"), + DisplayName: types.StringValue("displayName"), + }, + Authentication: &AuthenticationModel{ + AzureAD: &AzureADModel{ + AuthorityUrl: types.StringValue("azure-authority"), + ClientId: types.StringValue("azure-client"), + ClientSecret: types.StringValue("azure-secret"), + RedirectUrl: types.StringValue("azure-redirect"), + }, + Type: types.StringValue("azuread"), + }, + }, + &dremioSdk.UpdateDremioInstancePayload{ + Authentication: &dremioSdk.Authentication{ + Azuread: &dremioSdk.Azuread{ + AuthorityUrl: "azure-authority", + ClientId: "azure-client", + ClientSecret: "azure-secret", + RedirectUrl: utils.Ptr("azure-redirect"), + }, + Type: "azuread", + }, + Description: utils.Ptr("test description"), + DisplayName: utils.Ptr("displayName"), + }, + false, + }, + { + "idp-config-mismatch-local", + &InstanceModel{ + Model: Model{ + Description: types.StringValue("test description"), + DisplayName: types.StringValue("displayName"), + }, + Authentication: &AuthenticationModel{ + AzureAD: &AzureADModel{ + AuthorityUrl: types.StringValue("azure-authority"), + ClientId: types.StringValue("azure-client"), + ClientSecret: types.StringValue("azure-secret"), + RedirectUrl: types.StringValue("azure-redirect"), + }, + Type: types.StringValue("local-only"), + }, + }, + nil, + true, + }, + { + "missing-idp-config-oauth", + &InstanceModel{ + Model: Model{ + Description: types.StringValue("test description"), + DisplayName: types.StringValue("displayName"), + }, + Authentication: &AuthenticationModel{ + Type: types.StringValue("oauth"), + }, + }, + nil, + true, + }, + { + "missing-idp-config-azuread", + &InstanceModel{ + Model: Model{ + Description: types.StringValue("test description"), + DisplayName: types.StringValue("displayName"), + }, + Authentication: &AuthenticationModel{ + Type: types.StringValue("azuread"), + }, + }, + nil, + true, + }, { "nil model", nil, diff --git a/stackit/internal/services/dremio/testdata/resource-max.tf b/stackit/internal/services/dremio/testdata/resource-max.tf new file mode 100644 index 000000000..57943490b --- /dev/null +++ b/stackit/internal/services/dremio/testdata/resource-max.tf @@ -0,0 +1,60 @@ + +variable "project_id"{} +variable "region" {} +variable "display_name" {} +variable "description" {} + +// authentication +variable "authentication_type" {} + +// oauth +variable "authentication_oauth_authority_url" {} +variable "authentication_oauth_client_id" {} +variable "authentication_oauth_client_secret" {} +variable "authentication_oauth_client_jwt_claims_user_name" {} +variable "authentication_oauth_scope" {} +variable "authentication_oauth_parameter_name" {} +variable "authentication_oauth_parameter_value" {} + +// azuread +variable "authentication_type_azuread" {default=null} +variable "authentication_azuread_authority_url" {default=null} +variable "authentication_azuread_client_id" {default=null} +variable "authentication_azuread_client_secret" {default=null} + +resource "stackit_dremio_instance" "example" { + project_id = var.project_id + region = var.region + display_name = var.display_name + description = var.description + authentication = { + type = var.authentication_type + + oauth = var.authentication_type == "oauth" ? { + authority_url = var.authentication_oauth_authority_url + client_id = var.authentication_oauth_client_id + client_secret = var.authentication_oauth_client_secret + jwt_claims = { + user_name = var.authentication_oauth_client_jwt_claims_user_name + } + scope = var.authentication_oauth_scope + parameters = [ + { + "name": var.authentication_oauth_parameter_name, + "value": var.authentication_oauth_parameter_value + } + ] + } : null + azuread = var.authentication_type == "azuread" ? { + authority_url = var.authentication_azuread_authority_url + client_id = var.authentication_azuread_client_id + client_secret = var.authentication_azuread_client_secret + } : null + } +} + +data "stackit_dremio_instance" "example" { + project_id = var.project_id + region = var.region + instance_id = stackit_dremio_instance.example.instance_id +} \ No newline at end of file diff --git a/stackit/internal/services/dremio/testdata/resource-min.tf b/stackit/internal/services/dremio/testdata/resource-min.tf new file mode 100644 index 000000000..5a3bf4fb3 --- /dev/null +++ b/stackit/internal/services/dremio/testdata/resource-min.tf @@ -0,0 +1,20 @@ + +variable "project_id"{} +variable "region" {} +variable "display_name" {} +variable "authentication_type" {} + +resource "stackit_dremio_instance" "example" { + project_id = var.project_id + region = var.region + display_name = var.display_name + authentication = { + type = var.authentication_type + } +} + +data "stackit_dremio_instance" "example" { + project_id = var.project_id + region = var.region + instance_id = stackit_dremio_instance.example.instance_id +} \ No newline at end of file diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index 9e057574e..d47fe925b 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -71,6 +71,7 @@ var ( ALBCertCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_ALB_CERT_CUSTOM_ENDPOINT", providerName: "alb_certificates_custom_endpoint"} CdnCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_CDN_CUSTOM_ENDPOINT", providerName: "cdn_custom_endpoint"} DnsCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_DNS_CUSTOM_ENDPOINT", providerName: "dns_custom_endpoint"} + DremioCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_DREMIO_CUSTOM_ENDPOINT", providerName: "dremio_custom_endpoint"} EdgeCloudCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_EDGECLOUD_CUSTOM_ENDPOINT", providerName: "edgecloud_custom_endpoint"} GitCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_GIT_CUSTOM_ENDPOINT", providerName: "git_custom_endpoint"} IaaSCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_IAAS_CUSTOM_ENDPOINT", providerName: "iaas_custom_endpoint"} From 160f605e697837ea96b89085fc5e704d020b9c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20Schr=C3=B6der?= Date: Wed, 3 Jun 2026 18:22:22 +0200 Subject: [PATCH 11/12] feat(dremio): First draft for Dremio user resource --- .../internal/services/dremio/user/resource.go | 449 ++++++++++++++++++ 1 file changed, 449 insertions(+) diff --git a/stackit/internal/services/dremio/user/resource.go b/stackit/internal/services/dremio/user/resource.go index 8dc0f480d..528733a64 100644 --- a/stackit/internal/services/dremio/user/resource.go +++ b/stackit/internal/services/dremio/user/resource.go @@ -1 +1,450 @@ package dremio + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + dremioSdk "github.com/stackitcloud/stackit-sdk-go/services/dremio/v1alphaapi" + dremioUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dremio/utils" + + dremioWaiter "github.com/stackitcloud/stackit-sdk-go/services/dremio/v1alphaapi/wait/wait" +) + +var ( + _ resource.Resource = &userResource{} + _ resource.ResourceWithConfigure = &userResource{} + _ resource.ResourceWithImportState = &userResource{} + _ resource.ResourceWithModifyPlan = &userResource{} +) + +type Model struct { + ID types.String `tfsdk:"id"` + + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + InstanceId types.String `tfsdk:"instance_id"` + UserId types.String `tfsdk:"user_id"` + + Description types.String `tfsdk:"description"` + Email types.String `tfsdk:"email"` + FirstName types.String `tfsdk:"first_name"` + LastName types.String `tfsdk:"last_name"` + Name types.String `tfsdk:"name"` + Password types.String `tfsdk:"password"` +} + +type UserModel struct { + Model + + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +type userResource struct { + client *dremioSdk.APIClient + providerData core.ProviderData +} + +func NewUserResource() resource.Resource { + return &userResource{} +} + +func (r *userResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dremio_user" +} + +func (r *userResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel UserModel + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := dremioUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "Dremio user client configured") +} + +func (r *userResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + descriptions := map[string]string{ + "main": "Manages a STACKIT Dremio instances user.", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`instance_id`,`user_id`\".", + "project_id": "STACKIT Project ID to which the resource is associated.", + "instance_id": "The Dremio instance ID.", + "region": "The STACKIT region name the resource is located in. If not defined, the provider region is used.", + "user_id": "The Dremio user ID.", + "description": "The description of the user.", + "email": "The email address of the user.", + "first_name": "The first name of the user.", + "last_name": "The last name of the user.", + "name": "The username of the user.", + "password": "The password of the user. Only used for creation and updates. Must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number and one special character.", + } + + resp.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "instance_id": schema.StringAttribute{ + Description: descriptions["instance_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "region": schema.StringAttribute{ + Description: descriptions["region"], + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "user_id": schema.StringAttribute{ + Description: descriptions["user_id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "description": schema.StringAttribute{ + Description: descriptions["description"], + Optional: true, + }, + "email": schema.StringAttribute{ + Description: descriptions["email"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "first_name": schema.StringAttribute{ + Description: descriptions["first_name"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "last_name": schema.StringAttribute{ + Description: descriptions["last_name"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "password": schema.StringAttribute{ + Description: descriptions["password"], + Optional: true, + Sensitive: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + var model UserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + waiterTimeout := dremioWaiter.CreateDremioUserWaitHandler(ctx, r.client.DefaultAPI, "", "", "", "").GetTimeout() + createTimeout, diags := model.Timeouts.Create(ctx, waiterTimeout+core.DefaultTimeoutMargin) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, createTimeout) + defer cancel() + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := model.Region.ValueString() // not needed for global APIs + instanceId := model.InstanceId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + + // prepare the payload struct for the create user request + payload, err := toCreatePayload(&model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + // Create new Dremio user + userResp, err := r.client.DefaultAPI.CreateDremioUser(ctx, projectId, region, instanceId).CreateDremioUserPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]interface{}{ + "project_id": projectId, + "region": region, + "user_id": userResp.Id, + }) + if resp.Diagnostics.HasError() { + return + } + + _, err = dremioWaiter.CreateDremioUserWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceId, userResp.Id).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating Dremio user", fmt.Sprintf("Dremio user creation waiting: %v", err)) + return + } + + err = mapFields(userResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating Dremio user", fmt.Sprintf("Processing API payload: %v", err)) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Dremio user created") +} + +func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model UserModel + resp.Diagnostics.Append(req.State.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + readTimeout, diags := model.Timeouts.Read(ctx, core.DefaultOperationTimeout) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, readTimeout) + defer cancel() + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + instanceId := model.InstanceId.ValueString() + userId := model.UserId.ValueString() + if userId == "" { + // Resource not yet created; ID is unknown. + resp.State.RemoveResource(ctx) + return + } + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + ctx = tflog.SetField(ctx, "user_id", userId) + + userResp, err := r.client.DefaultAPI.GetDremioUser(ctx, projectId, region, instanceId, userId).Execute() + if err != nil { + if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading dremio user", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(userResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading dremio user", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Dremio user read") +} + +func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // We don't allow updates on Dremio users. +} + +func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + var model UserModel + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + waiterTimeout := dremioWaiter.DeleteDremioUserWaitHandler(ctx, r.client.DefaultAPI, "", "", "", "").GetTimeout() + deleteTimeout, diags := model.Timeouts.Delete(ctx, waiterTimeout+core.DefaultTimeoutMargin) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, deleteTimeout) + defer cancel() + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := model.Region.ValueString() + instanceId := model.InstanceId.ValueString() + userId := model.UserId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + ctx = tflog.SetField(ctx, "user_id", userId) + + err := r.client.DefaultAPI.DeleteDremioUser(ctx, projectId, region, instanceId, userId).Execute() + if err != nil { + if oapiErr, ok := errors.AsType[*oapierror.GenericOpenAPIError](err); ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting Dremio user", fmt.Sprintf("Calling API: %v", err)) + } + + ctx = core.LogResponse(ctx) + + _, err = dremioWaiter.DeleteDremioUserWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceId, userId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting Dremio user", fmt.Sprintf("Dremio user deletion waiting: %v", err)) + return + } + + tflog.Info(ctx, "Dremio user deleted") +} + +func (r *userResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + 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 dremio user", + fmt.Sprintf("Expected import identifier with format [project_id],[region],[instance_id],[user_id] got %q", req.ID), + ) + return + } + + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "instance_id": idParts[2], + "user_id": idParts[3], + }) + + tflog.Info(ctx, "Dremio user state imported") +} + +func mapFields(userResp *dremioSdk.DremioUserResponse, model *UserModel) error { + if userResp == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + model.UserId = types.StringValue(userResp.Id) + model.Description = types.StringPointerValue(userResp.Description) + model.Email = types.StringValue(userResp.Email) + model.FirstName = types.StringValue(userResp.FirstName) + model.LastName = types.StringValue(userResp.LastName) + model.Name = types.StringValue(userResp.Name) + + return nil +} + +func toCreatePayload(model *UserModel) (*dremioSdk.CreateDremioUserPayload, error) { + if model == nil { + return nil, fmt.Errorf("model input is nil") + } + + payload := &dremioSdk.CreateDremioUserPayload{ + Description: model.Description.ValueStringPointer(), + Email: model.Email.ValueString(), + FirstName: model.FirstName.ValueString(), + LastName: model.LastName.ValueString(), + Name: model.Name.ValueString(), + Password: model.Password.ValueString(), + } + + return payload, nil +} + +// func toUpdatePayload(model *UserModel) (*dremioSdk.UpdateDremioUserPayload, error) { +// return nil, nil +// } From 5a57e0b9bd63d73113750af63957b83cc59558b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarne=20Schr=C3=B6der?= Date: Wed, 3 Jun 2026 18:23:07 +0200 Subject: [PATCH 12/12] fix(dremio): Removing some obsolete comments & fixing typos in Dremio instance. --- stackit/internal/services/dremio/instance/resource.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stackit/internal/services/dremio/instance/resource.go b/stackit/internal/services/dremio/instance/resource.go index 91b16e95a..8d870defd 100644 --- a/stackit/internal/services/dremio/instance/resource.go +++ b/stackit/internal/services/dremio/instance/resource.go @@ -35,7 +35,7 @@ var ( _ resource.Resource = &instanceResource{} _ resource.ResourceWithConfigure = &instanceResource{} _ resource.ResourceWithImportState = &instanceResource{} - _ resource.ResourceWithModifyPlan = &instanceResource{} // not needed for global APIs + _ resource.ResourceWithModifyPlan = &instanceResource{} ) type Model struct { @@ -130,7 +130,7 @@ func NewInstanceResource() resource.Resource { type instanceResource struct { client *dremioSdk.APIClient - providerData core.ProviderData // not needed for global APIs + providerData core.ProviderData } func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -182,7 +182,7 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { descriptions := map[string]string{ //nolint:gosec // no hardcoded credentials in here "main": "Manages a STACKIT Dremio instance.", - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`dremio_id`\".", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`instance_id`\".", "project_id": "STACKIT Project ID to which the resource is associated.", "instance_id": "The Dremio instance ID.", "region": "The STACKIT region name the resource is located in. If not defined, the provider region is used.", @@ -422,7 +422,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques // prepare the payload struct for the create instance request payload, err := toCreatePayload(&model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Creating API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) return }