From 8d4513995363987de0e5f845ef6dd9a5382aecab 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 1/9] 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 872585070cfb90af9d3e6eff5f77fbf6fbf77505 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 2/9] 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 b96e520d1..6698ce014 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 6591543f3..2396854c5 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 3ecf92de1..8b077c132 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 c591c91fd..b91de97be 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -172,6 +172,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"` @@ -228,6 +229,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", @@ -527,6 +529,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 7a7ad74a13868ac12292c7d3037cc11ba2626724 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 3/9] 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 b91de97be..3a762f617 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" @@ -357,6 +358,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"], @@ -735,6 +740,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 09f3f3c534b6975dbb1246ceff12cdfe523ff713 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 4/9] 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 bcd5ca59f06ed1a8ae9dc2d735313b2a3b98b02e 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 5/9] 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 1594e4bead9cf830321e75b0b7d2acf98418785a 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 6/9] 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 eb94c1fa3769d6b9105fd73f8587be502b73db2d 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 7/9] 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 2c16486be84bad806e878b24afa927e0c9131783 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 8/9] 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 3a762f617..abd674c04 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" @@ -643,6 +644,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 f8c68bdc72ada730f53c1425f89ff41f05e56f66 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 9/9] 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 abd674c04..75ea1a864 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" @@ -644,7 +643,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,