diff --git a/docs/data-sources/intakes.md b/docs/data-sources/intakes.md new file mode 100644 index 000000000..b68311212 --- /dev/null +++ b/docs/data-sources/intakes.md @@ -0,0 +1,51 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_intakes Data Source - stackit" +subcategory: "" +description: |- + Datasource for STACKIT Intake. +--- + +# stackit_intakes (Data Source) + +Datasource for STACKIT Intake. + +## Example Usage + +```terraform +data "stackit_intakes" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + intake_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `intake_id` (String) The intake ID. +- `project_id` (String) STACKIT Project ID to which the intake is associated. + +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `catalog_auth_type` (String) The catalog authentication type. +- `catalog_namespace` (String) The catalog namespace. +- `catalog_partition_by` (List of String) The catalog partition by. +- `catalog_partitioning` (String) The catalog partitioning. +- `catalog_table_name` (String) The catalog table name. +- `catalog_uri` (String) The catalog URI. +- `catalog_warehouse` (String) The catalog warehouse. +- `create_time` (String) The creation time of the intake. +- `description` (String) The description of the intake. +- `dremio_personal_access_token` (String, Sensitive) The Dremio personal access token. +- `dremio_token_endpoint` (String) The Dremio token endpoint. +- `id` (String) Terraform's internal resource identifier. It is structured as `project_id`,`region`,`intake_id`. +- `labels` (Map of String) User-defined labels. +- `name` (String) The name of the intake. +- `runner_id` (String) The runner ID. +- `uri` (String) The URI of the intake. diff --git a/docs/resources/intakes.md b/docs/resources/intakes.md new file mode 100644 index 000000000..c98d960e9 --- /dev/null +++ b/docs/resources/intakes.md @@ -0,0 +1,62 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_intakes Resource - stackit" +subcategory: "" +description: |- + Manages STACKIT Intake. +--- + +# stackit_intakes (Resource) + +Manages STACKIT Intake. + +## Example Usage + +```terraform +resource "stackit_intakes" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + runner_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-intake" + description = "An example intake for STACKIT Intake service" + catalog_auth_type = "dremio" + catalog_warehouse = "default" + catalog_uri = "https://dremio.eu01.onstackit.cloud/iceberg" + dremio_token_endpoint = "https://dremio.eu01.onstackit.cloud/oauth/token" + dremio_personal_access_token = "my-dremio-pat-token" + + labels = { + "env" = "development" + } +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the intake. +- `project_id` (String) STACKIT Project ID to which the intake is associated. +- `runner_id` (String) The runner ID. + +### Optional + +- `catalog_auth_type` (String) The catalog authentication type. +- `catalog_namespace` (String) The catalog namespace. +- `catalog_partition_by` (List of String) The catalog partition by. +- `catalog_partitioning` (String) The catalog partitioning. +- `catalog_table_name` (String) The catalog table name. +- `catalog_uri` (String) The catalog URI. +- `catalog_warehouse` (String) The catalog warehouse. +- `description` (String) The description of the intake. +- `dremio_personal_access_token` (String, Sensitive) The Dremio personal access token. +- `dremio_token_endpoint` (String) The Dremio token endpoint. +- `labels` (Map of String) User-defined labels. +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `create_time` (String) The creation time of the intake. +- `id` (String) Terraform's internal resource identifier. It is structured as `project_id`,`region`,`intake_id`. +- `intake_id` (String) The intake ID. +- `uri` (String) The URI of the intake. diff --git a/examples/data-sources/stackit_intakes/data-source.tf b/examples/data-sources/stackit_intakes/data-source.tf new file mode 100644 index 000000000..1e47ffb57 --- /dev/null +++ b/examples/data-sources/stackit_intakes/data-source.tf @@ -0,0 +1,4 @@ +data "stackit_intakes" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + intake_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} diff --git a/examples/resources/stackit_intakes/resource.tf b/examples/resources/stackit_intakes/resource.tf new file mode 100644 index 000000000..40a4fc965 --- /dev/null +++ b/examples/resources/stackit_intakes/resource.tf @@ -0,0 +1,15 @@ +resource "stackit_intakes" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + runner_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-intake" + description = "An example intake for STACKIT Intake service" + catalog_auth_type = "dremio" + catalog_warehouse = "default" + catalog_uri = "https://dremio.eu01.onstackit.cloud/iceberg" + dremio_token_endpoint = "https://dremio.eu01.onstackit.cloud/oauth/token" + dremio_personal_access_token = "my-dremio-pat-token" + + labels = { + "env" = "development" + } +} diff --git a/stackit/internal/services/intake/intake_acc_test.go b/stackit/internal/services/intake/intake_acc_test.go index e7d2f9235..655bc5cec 100644 --- a/stackit/internal/services/intake/intake_acc_test.go +++ b/stackit/internal/services/intake/intake_acc_test.go @@ -1,16 +1,24 @@ package intake_test import ( + "bytes" "context" + "crypto/tls" _ "embed" + "encoding/json" "fmt" + "io" "maps" + "net/http" + "net/url" "strings" "testing" + "time" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" @@ -20,70 +28,317 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) -//go:embed testdata/resource-min.tf +//go:embed testdata/resource-runner-min.tf var resourceIntakeRunnerMin string -//go:embed testdata/resource-max.tf +//go:embed testdata/resource-runner-max.tf var resourceIntakeRunnerMax string +//go:embed testdata/resource-intake-min.tf +var resourceIntakesMin string + +//go:embed testdata/resource-intake-max.tf +var resourceIntakesMax string + const intakeRunnerResource = "stackit_intake_runner.example" +const intakesResource = "stackit_intakes.example" + +var runnerNameMin = fmt.Sprintf("tf-acc-runner-%s", acctest.RandStringFromCharSet(8, acctest.CharSetAlphaNum)) +var runnerNameMinUpd = fmt.Sprintf("tf-acc-runner-%s", acctest.RandStringFromCharSet(8, acctest.CharSetAlphaNum)) +var runnerNameMax = fmt.Sprintf("tf-acc-runner-%s", acctest.RandStringFromCharSet(8, acctest.CharSetAlphaNum)) +var runnerNameMaxUpd = fmt.Sprintf("tf-acc-runner-%s", acctest.RandStringFromCharSet(8, acctest.CharSetAlphaNum)) +var runnerNameMaxPrereq = fmt.Sprintf("tf-acc-runner-%s", acctest.RandStringFromCharSet(8, acctest.CharSetAlphaNum)) +var intakeNameMin = fmt.Sprintf("tf-acc-intake-%s", acctest.RandStringFromCharSet(8, acctest.CharSetAlphaNum)) +var intakeNameMinUpd = fmt.Sprintf("tf-acc-intake-%s", acctest.RandStringFromCharSet(8, acctest.CharSetAlphaNum)) +var intakeNameMax = fmt.Sprintf("tf-acc-intake-%s", acctest.RandStringFromCharSet(8, acctest.CharSetAlphaNum)) +var intakeNameMaxUpd = fmt.Sprintf("tf-acc-intake-%s", acctest.RandStringFromCharSet(8, acctest.CharSetAlphaNum)) +var dremioUserMin = fmt.Sprintf("tfAcc%s", acctest.RandStringFromCharSet(8, acctest.CharSetAlphaNum)) +var dremioUserMax = fmt.Sprintf("tfAcc%s", acctest.RandStringFromCharSet(8, acctest.CharSetAlphaNum)) + +func testIntakeRunnerConfigVarsMin() config.Variables { + return config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "name": config.StringVariable(runnerNameMin), + "max_message_size_kib": config.IntegerVariable(1024), + "max_messages_per_hour": config.IntegerVariable(1000), + } +} -var testIntakeRunnerConfigVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable("intake-min-runner"), - "max_message_size_kib": config.IntegerVariable(1024), - "max_messages_per_hour": config.IntegerVariable(1000), +func testIntakeRunnerConfigVarsMax() config.Variables { + return config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "name": config.StringVariable(runnerNameMax), + "region": config.StringVariable(testutil.Region), + "description": config.StringVariable("An example runner for Intake"), + "max_message_size_kib": config.IntegerVariable(1024), + "max_messages_per_hour": config.IntegerVariable(1100), + } +} + +func testIntakesConfigVarsMin() config.Variables { + return config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "runner_name": config.StringVariable(runnerNameMin), + "intake_name": config.StringVariable(intakeNameMin), + "max_message_size_kib": config.IntegerVariable(1024), + "max_messages_per_hour": config.IntegerVariable(1000), + "dremio_display_name": config.StringVariable(fmt.Sprintf("tfAccDremio%s", acctest.RandStringFromCharSet(6, acctest.CharSetAlphaNum))), + "dremio_user_email": config.StringVariable(fmt.Sprintf("tf-acc-%s@example.com", acctest.RandStringFromCharSet(8, acctest.CharSetAlphaNum))), + "dremio_user_first_name": config.StringVariable("Intake"), + "dremio_user_last_name": config.StringVariable("Min"), + "dremio_user_name": config.StringVariable(dremioUserMin), + "dremio_user_password": config.StringVariable(fmt.Sprintf("TestAcc!@%s", acctest.RandStringFromCharSet(6, acctest.CharSetAlphaNum))), + "dremio_personal_access_token": config.StringVariable("pending-dremio-pat"), + } } -var testIntakeRunnerConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable("intake-max-runner"), - "region": config.StringVariable(testutil.Region), - "description": config.StringVariable("An example runner for Intake"), - "max_message_size_kib": config.IntegerVariable(1024), - "max_messages_per_hour": config.IntegerVariable(1100), +func testIntakesConfigVarsMax() config.Variables { + return config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "region": config.StringVariable(testutil.Region), + "runner_name": config.StringVariable(runnerNameMaxPrereq), + "intake_name": config.StringVariable(intakeNameMax), + "description": config.StringVariable("An example full intake with dynamic Dremio"), + "max_message_size_kib": config.IntegerVariable(1024), + "max_messages_per_hour": config.IntegerVariable(1000), + "dremio_display_name": config.StringVariable(fmt.Sprintf("tfAccDremio%s", acctest.RandStringFromCharSet(6, acctest.CharSetAlphaNum))), + "dremio_user_email": config.StringVariable(fmt.Sprintf("tf-acc-%s@example.com", acctest.RandStringFromCharSet(8, acctest.CharSetAlphaNum))), + "dremio_user_first_name": config.StringVariable("Acc"), + "dremio_user_last_name": config.StringVariable("Test"), + "dremio_user_name": config.StringVariable(dremioUserMax), + "dremio_user_password": config.StringVariable(fmt.Sprintf("TestAcceptance12345!@%s", acctest.RandStringFromCharSet(8, acctest.CharSetAlphaNum))), + "dremio_personal_access_token": config.StringVariable("pending-dremio-pat"), + } } func testIntakeRunnerConfigVarsMinUpdated() config.Variables { - tempConfig := make(config.Variables, len(testIntakeRunnerConfigVarsMin)) - maps.Copy(tempConfig, testIntakeRunnerConfigVarsMin) - tempConfig["name"] = config.StringVariable("intake-min-runner-upd") + tempConfig := make(config.Variables, len(testIntakeRunnerConfigVarsMin())) + maps.Copy(tempConfig, testIntakeRunnerConfigVarsMin()) + tempConfig["name"] = config.StringVariable(runnerNameMinUpd) return tempConfig } func testIntakeRunnerConfigVarsMaxUpdated() config.Variables { - tempConfig := make(config.Variables, len(testIntakeRunnerConfigVarsMax)) - maps.Copy(tempConfig, testIntakeRunnerConfigVarsMax) - tempConfig["name"] = config.StringVariable("intake-max-runner-upd") + tempConfig := make(config.Variables, len(testIntakeRunnerConfigVarsMax())) + maps.Copy(tempConfig, testIntakeRunnerConfigVarsMax()) + tempConfig["name"] = config.StringVariable(runnerNameMaxUpd) + return tempConfig +} + +func testIntakesConfigVarsMinUpdated() config.Variables { + tempConfig := make(config.Variables, len(testIntakesConfigVarsMin())) + maps.Copy(tempConfig, testIntakesConfigVarsMin()) + tempConfig["intake_name"] = config.StringVariable(intakeNameMinUpd) + return tempConfig +} + +func testIntakesConfigVarsMaxUpdated() config.Variables { + tempConfig := make(config.Variables, len(testIntakesConfigVarsMax())) + maps.Copy(tempConfig, testIntakesConfigVarsMax()) + tempConfig["intake_name"] = config.StringVariable(intakeNameMaxUpd) + tempConfig["description"] = config.StringVariable("Updated full intake description") + tempConfig["max_messages_per_hour"] = config.IntegerVariable(1100) + tempConfig["dremio_user_email"] = config.StringVariable(fmt.Sprintf("tf-acc-%s@example.com", acctest.RandStringFromCharSet(8, acctest.CharSetAlphaNum))) + tempConfig["dremio_user_name"] = config.StringVariable(fmt.Sprintf("tfAcc%s", acctest.RandStringFromCharSet(8, acctest.CharSetAlphaNum))) return tempConfig } +// getDremioPAT authenticates against Dremio UI API, enables PAT support key, resolves user UUID, and issues a PAT +func getDremioPAT(ctx context.Context, uiEndpoint, username, password string) (string, error) { + if !strings.HasPrefix(uiEndpoint, "http://") && !strings.HasPrefix(uiEndpoint, "https://") { + uiEndpoint = "https://" + uiEndpoint + } + uiEndpoint = strings.TrimSuffix(uiEndpoint, "/") + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // acceptance test TLS skip + } + httpClient := &http.Client{ + Timeout: 30 * time.Second, + Transport: tr, + } + + // 1. authenticate with retry loop for service startup readiness POST /oauth/token + tokenURL := fmt.Sprintf("%s/oauth/token", uiEndpoint) + form := url.Values{} + form.Set("grant_type", "password") + form.Set("scope", "dremio.all") + form.Set("username", username) + form.Set("password", password) + + var accessToken string + var lastErr error + + maxRetries := 18 // 3 minutes total retry time + for i := 0; i < maxRetries; i++ { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode())) + if err != nil { + return "", fmt.Errorf("creating login request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := httpClient.Do(req) + if err == nil { + respBytes, _ := io.ReadAll(resp.Body) + _ = resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + var tokenResp struct { + AccessToken string `json:"access_token"` + } + if jsonErr := json.Unmarshal(respBytes, &tokenResp); jsonErr == nil && tokenResp.AccessToken != "" { + accessToken = tokenResp.AccessToken + break + } + } else { + lastErr = fmt.Errorf("login status %d: %s", resp.StatusCode, string(respBytes)) + } + } else { + lastErr = err + } + + time.Sleep(10 * time.Second) + } + + if accessToken == "" { + return "", fmt.Errorf("failed to authenticate against Dremio at %s after retries: %w", tokenURL, lastErr) + } + + // 2. enable PAT support key PUT /apiv2/settings/auth.personal-access-tokens.enabled + settingsURL := fmt.Sprintf("%s/apiv2/settings/auth.personal-access-tokens.enabled", uiEndpoint) + settingsBodyMap := map[string]interface{}{ + "type": "BOOLEAN", + "id": "auth.personal-access-tokens.enabled", + "value": true, + } + settingsJSON, err := json.Marshal(settingsBodyMap) + if err != nil { + return "", fmt.Errorf("marshaling settings body: %w", err) + } + + settingsReq, err := http.NewRequestWithContext(ctx, http.MethodPut, settingsURL, bytes.NewBuffer(settingsJSON)) + if err != nil { + return "", fmt.Errorf("creating settings request: %w", err) + } + settingsReq.Header.Set("Authorization", "Bearer "+accessToken) + settingsReq.Header.Set("Content-Type", "application/json") + + settingsResp, err := httpClient.Do(settingsReq) + if err != nil { + return "", fmt.Errorf("enabling PAT support key at %s: %w", settingsURL, err) + } + _ = settingsResp.Body.Close() + + // 3. resolve user UUID GET /api/v3/user/by-name/{username} + userURL := fmt.Sprintf("%s/api/v3/user/by-name/%s", uiEndpoint, url.PathEscape(username)) + userReq, err := http.NewRequestWithContext(ctx, http.MethodGet, userURL, http.NoBody) + if err != nil { + return "", fmt.Errorf("creating user lookup request: %w", err) + } + userReq.Header.Set("Authorization", "Bearer "+accessToken) + + userResp, err := httpClient.Do(userReq) + if err != nil { + return "", fmt.Errorf("looking up user UUID at %s: %w", userURL, err) + } + defer func() { _ = userResp.Body.Close() }() + + userBytes, err := io.ReadAll(userResp.Body) + if err != nil { + return "", fmt.Errorf("reading user response: %w", err) + } + + if userResp.StatusCode != http.StatusOK { + return "", fmt.Errorf("user lookup failed with status %d: %s", userResp.StatusCode, string(userBytes)) + } + + var userObj struct { + ID string `json:"id"` + } + if err := json.Unmarshal(userBytes, &userObj); err != nil || userObj.ID == "" { + return "", fmt.Errorf("failed to parse user UUID from response: %s", string(userBytes)) + } + + // 4. issue Personal Access Token POST /api/v3/user/{id}/token + patURL := fmt.Sprintf("%s/api/v3/user/%s/token", uiEndpoint, userObj.ID) + patBodyMap := map[string]interface{}{ + "label": "acceptance-test", + "millisecondsToExpire": 86400000, // 24 hours + } + patBodyJSON, err := json.Marshal(patBodyMap) + if err != nil { + return "", fmt.Errorf("marshaling PAT request body: %w", err) + } + + patReq, err := http.NewRequestWithContext(ctx, http.MethodPost, patURL, bytes.NewBuffer(patBodyJSON)) + if err != nil { + return "", fmt.Errorf("creating PAT request: %w", err) + } + patReq.Header.Set("Authorization", "Bearer "+accessToken) + patReq.Header.Set("Content-Type", "application/json") + + patResp, err := httpClient.Do(patReq) + if err != nil { + return "", fmt.Errorf("requesting PAT at %s: %w", patURL, err) + } + defer func() { _ = patResp.Body.Close() }() + + patBytes, err := io.ReadAll(patResp.Body) + if err != nil { + return "", fmt.Errorf("reading PAT response: %w", err) + } + + if patResp.StatusCode != http.StatusOK { + return "", fmt.Errorf("PAT creation failed with status %d: %s", patResp.StatusCode, string(patBytes)) + } + + rawToken := strings.TrimSpace(string(patBytes)) + rawToken = strings.Trim(rawToken, `"`) + + var patObj struct { + Token string `json:"token"` + PAT string `json:"pat"` + } + if err := json.Unmarshal(patBytes, &patObj); err == nil { + if patObj.Token != "" { + return patObj.Token, nil + } + if patObj.PAT != "" { + return patObj.PAT, nil + } + } + + return rawToken, nil +} + func TestAccIntakeRunnerMin(t *testing.T) { + cfg := testIntakeRunnerConfigVarsMin() + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckIntakeRunnerDestroy, Steps: []resource.TestStep{ // Create the minimum runner from the HCL file { - ConfigVariables: testIntakeRunnerConfigVarsMin, - Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + resourceIntakeRunnerMin, + ConfigVariables: cfg, + Config: testutil.NewConfigBuilder().BuildProviderConfig() + resourceIntakeRunnerMin, Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr(intakeRunnerResource, "name", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["name"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ConvertConfigVariable(cfg["project_id"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "name", testutil.ConvertConfigVariable(cfg["name"])), resource.TestCheckResourceAttrSet(intakeRunnerResource, "runner_id"), resource.TestCheckNoResourceAttr(intakeRunnerResource, "description"), resource.TestCheckNoResourceAttr(intakeRunnerResource, "labels"), - resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["max_message_size_kib"])), - resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["max_messages_per_hour"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", testutil.ConvertConfigVariable(cfg["max_message_size_kib"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", testutil.ConvertConfigVariable(cfg["max_messages_per_hour"])), resource.TestCheckResourceAttrSet(intakeRunnerResource, "id"), resource.TestCheckResourceAttrSet(intakeRunnerResource, "uri"), resource.TestCheckResourceAttrSet(intakeRunnerResource, "create_time"), resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.Region), ), }, - // Data source check: creates config that includes resource and data source + // Data source check { - ConfigVariables: testIntakeRunnerConfigVarsMin, + ConfigVariables: cfg, Config: fmt.Sprintf(` %s %s @@ -93,7 +348,6 @@ func TestAccIntakeRunnerMin(t *testing.T) { region = %s.region }`, testutil.NewConfigBuilder().BuildProviderConfig(), resourceIntakeRunnerMin, intakeRunnerResource, intakeRunnerResource, intakeRunnerResource), Check: resource.ComposeAggregateTestCheckFunc( - // Make sure it's correctly found resource by comparing runner_id attribute resource.TestCheckResourceAttrPair(intakeRunnerResource, "project_id", "data.stackit_intake_runner.example", "project_id"), resource.TestCheckResourceAttrPair(intakeRunnerResource, "runner_id", "data.stackit_intake_runner.example", "runner_id"), resource.TestCheckResourceAttrPair(intakeRunnerResource, "name", "data.stackit_intake_runner.example", "name"), @@ -107,30 +361,28 @@ func TestAccIntakeRunnerMin(t *testing.T) { }, // Simulate terraform import { - ConfigVariables: testIntakeRunnerConfigVarsMin, + ConfigVariables: cfg, Config: testutil.NewConfigBuilder().BuildProviderConfig() + "\n" + resourceIntakeRunnerMin, ResourceName: intakeRunnerResource, ImportState: true, ImportStateVerify: true, ImportStateIdFunc: func(s *terraform.State) (string, error) { - // Construct ID string r, ok := s.RootModule().Resources[intakeRunnerResource] if !ok { return "", fmt.Errorf("couldn't find resource %s", intakeRunnerResource) } - // ID structure: project_id, region, runner_id return fmt.Sprintf("%s,%s,%s", r.Primary.Attributes["project_id"], r.Primary.Attributes["region"], r.Primary.Attributes["runner_id"]), nil }, }, - // Update check: verifies API updated resource name without crashing + // Update check { ConfigVariables: testIntakeRunnerConfigVarsMinUpdated(), Config: testutil.NewConfigBuilder().BuildProviderConfig() + "\n" + resourceIntakeRunnerMin, Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMinUpdated()["project_id"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ConvertConfigVariable(cfg["project_id"])), resource.TestCheckResourceAttr(intakeRunnerResource, "name", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMinUpdated()["name"])), - resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["max_message_size_kib"])), - resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["max_messages_per_hour"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", testutil.ConvertConfigVariable(cfg["max_message_size_kib"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", testutil.ConvertConfigVariable(cfg["max_messages_per_hour"])), resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.Region), resource.TestCheckNoResourceAttr(intakeRunnerResource, "description"), resource.TestCheckNoResourceAttr(intakeRunnerResource, "labels"), @@ -145,20 +397,22 @@ func TestAccIntakeRunnerMin(t *testing.T) { } func TestAccIntakeRunnerMax(t *testing.T) { + cfg := testIntakeRunnerConfigVarsMax() + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckIntakeRunnerDestroy, Steps: []resource.TestStep{ // Create the max intake runner from HCL files and verify comparison { - ConfigVariables: testIntakeRunnerConfigVarsMax, + ConfigVariables: cfg, Config: testutil.NewConfigBuilder().BuildProviderConfig() + "\n" + resourceIntakeRunnerMax, Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["project_id"])), - resource.TestCheckResourceAttr(intakeRunnerResource, "name", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["name"])), - resource.TestCheckResourceAttr(intakeRunnerResource, "description", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["description"])), - resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["max_message_size_kib"])), - resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["max_messages_per_hour"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ConvertConfigVariable(cfg["project_id"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "name", testutil.ConvertConfigVariable(cfg["name"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "description", testutil.ConvertConfigVariable(cfg["description"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", testutil.ConvertConfigVariable(cfg["max_message_size_kib"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", testutil.ConvertConfigVariable(cfg["max_messages_per_hour"])), resource.TestCheckResourceAttr(intakeRunnerResource, "labels.%", "2"), resource.TestCheckResourceAttr(intakeRunnerResource, "labels.env", "development"), resource.TestCheckResourceAttr(intakeRunnerResource, "labels.created_by", "terraform-provider-stackit"), @@ -166,11 +420,12 @@ func TestAccIntakeRunnerMax(t *testing.T) { resource.TestCheckResourceAttrSet(intakeRunnerResource, "id"), resource.TestCheckResourceAttrSet(intakeRunnerResource, "uri"), resource.TestCheckResourceAttrSet(intakeRunnerResource, "create_time"), - resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["region"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.ConvertConfigVariable(cfg["region"])), ), }, + // Data source check { - ConfigVariables: testIntakeRunnerConfigVarsMax, + ConfigVariables: cfg, Config: fmt.Sprintf(` %s %s @@ -192,18 +447,16 @@ func TestAccIntakeRunnerMax(t *testing.T) { }, // Simulate terraform import { - ConfigVariables: testIntakeRunnerConfigVarsMax, + ConfigVariables: cfg, Config: testutil.NewConfigBuilder().BuildProviderConfig() + "\n" + resourceIntakeRunnerMax, ResourceName: intakeRunnerResource, ImportState: true, ImportStateVerify: true, ImportStateIdFunc: func(s *terraform.State) (string, error) { - // Construct ID string r, ok := s.RootModule().Resources[intakeRunnerResource] if !ok { return "", fmt.Errorf("couldn't find resource %s", intakeRunnerResource) } - // ID structure: project_id, region, runner_id return fmt.Sprintf("%s,%s,%s", r.Primary.Attributes["project_id"], r.Primary.Attributes["region"], r.Primary.Attributes["runner_id"]), nil }, }, @@ -212,11 +465,11 @@ func TestAccIntakeRunnerMax(t *testing.T) { ConfigVariables: testIntakeRunnerConfigVarsMaxUpdated(), Config: testutil.NewConfigBuilder().BuildProviderConfig() + "\n" + resourceIntakeRunnerMax, Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["project_id"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ConvertConfigVariable(cfg["project_id"])), resource.TestCheckResourceAttr(intakeRunnerResource, "name", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMaxUpdated()["name"])), - resource.TestCheckResourceAttr(intakeRunnerResource, "description", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["description"])), - resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["max_message_size_kib"])), - resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["max_messages_per_hour"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "description", testutil.ConvertConfigVariable(cfg["description"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", testutil.ConvertConfigVariable(cfg["max_message_size_kib"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", testutil.ConvertConfigVariable(cfg["max_messages_per_hour"])), resource.TestCheckResourceAttr(intakeRunnerResource, "labels.%", "2"), resource.TestCheckResourceAttr(intakeRunnerResource, "labels.env", "development"), resource.TestCheckResourceAttr(intakeRunnerResource, "labels.created_by", "terraform-provider-stackit"), @@ -224,56 +477,301 @@ func TestAccIntakeRunnerMax(t *testing.T) { resource.TestCheckResourceAttrSet(intakeRunnerResource, "id"), resource.TestCheckResourceAttrSet(intakeRunnerResource, "uri"), resource.TestCheckResourceAttrSet(intakeRunnerResource, "create_time"), - resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["region"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.ConvertConfigVariable(cfg["region"])), ), }, }, }) } -// testAccCheckIntakeRunnerDestroy act as independent auditor to verify destroy operation +func TestAccIntakesMin(t *testing.T) { + cfg := testIntakesConfigVarsMin() + cfgUpdated := testIntakesConfigVarsMinUpdated() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckIntakesDestroy, + Steps: []resource.TestStep{ + // Step 1: Provision prerequisites and dynamically acquire Dremio PAT + { + ConfigVariables: cfg, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentDremio).BuildProviderConfig() + "\n" + strings.Split(resourceIntakesMin, "resource \"stackit_intakes\"")[0], + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("stackit_dremio_instance.dremio", "endpoints.ui"), + resource.TestCheckResourceAttrSet("stackit_dremio_user.dremio_user", "user_id"), + func(s *terraform.State) error { + dremioRes, ok := s.RootModule().Resources["stackit_dremio_instance.dremio"] + if !ok { + return fmt.Errorf("could not find stackit_dremio_instance.dremio in state") + } + uiEndpoint := dremioRes.Primary.Attributes["endpoints.ui"] + if uiEndpoint == "" { + return fmt.Errorf("dremio instance endpoints.ui is empty") + } + + username := testutil.ConvertConfigVariable(cfg["dremio_user_name"]) + password := testutil.ConvertConfigVariable(cfg["dremio_user_password"]) + + dremioPAT, err := getDremioPAT(context.Background(), uiEndpoint, username, password) + if err != nil { + return fmt.Errorf("failed to obtain Dremio PAT: %w", err) + } + + cfg["dremio_personal_access_token"] = config.StringVariable(dremioPAT) + cfgUpdated["dremio_personal_access_token"] = config.StringVariable(dremioPAT) + return nil + }, + ), + }, + // Step 2: Create minimal intake using the generated PAT + { + ConfigVariables: cfg, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentDremio).BuildProviderConfig() + "\n" + resourceIntakesMin, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(intakesResource, "project_id", testutil.ConvertConfigVariable(cfg["project_id"])), + resource.TestCheckResourceAttr(intakesResource, "name", testutil.ConvertConfigVariable(cfg["intake_name"])), + resource.TestCheckResourceAttrSet(intakesResource, "intake_id"), + resource.TestCheckResourceAttrSet(intakesResource, "runner_id"), + resource.TestCheckResourceAttrSet(intakesResource, "id"), + resource.TestCheckResourceAttrSet(intakesResource, "uri"), + resource.TestCheckResourceAttrSet(intakesResource, "create_time"), + resource.TestCheckResourceAttr(intakesResource, "region", testutil.Region), + ), + }, + // Step 3: Data source check + { + ConfigVariables: cfg, + Config: fmt.Sprintf(` + %s + %s + data "stackit_intakes" "example" { + project_id = %s.project_id + intake_id = %s.intake_id + region = %s.region + }`, testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentDremio).BuildProviderConfig(), resourceIntakesMin, intakesResource, intakesResource, intakesResource), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(intakesResource, "project_id", "data.stackit_intakes.example", "project_id"), + resource.TestCheckResourceAttrPair(intakesResource, "intake_id", "data.stackit_intakes.example", "intake_id"), + resource.TestCheckResourceAttrPair(intakesResource, "name", "data.stackit_intakes.example", "name"), + resource.TestCheckResourceAttrPair(intakesResource, "runner_id", "data.stackit_intakes.example", "runner_id"), + resource.TestCheckResourceAttrPair(intakesResource, "region", "data.stackit_intakes.example", "region"), + resource.TestCheckResourceAttrPair(intakesResource, "uri", "data.stackit_intakes.example", "uri"), + resource.TestCheckResourceAttrPair(intakesResource, "create_time", "data.stackit_intakes.example", "create_time"), + ), + }, + // Step 4: Import state check + { + ConfigVariables: cfg, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentDremio).BuildProviderConfig() + "\n" + resourceIntakesMin, + ResourceName: intakesResource, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"dremio_personal_access_token"}, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources[intakesResource] + if !ok { + return "", fmt.Errorf("couldn't find resource %s", intakesResource) + } + return fmt.Sprintf("%s,%s,%s", r.Primary.Attributes["project_id"], r.Primary.Attributes["region"], r.Primary.Attributes["intake_id"]), nil + }, + }, + // Step 5: Update check + { + ConfigVariables: cfgUpdated, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentDremio).BuildProviderConfig() + "\n" + resourceIntakesMin, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(intakesResource, "project_id", testutil.ConvertConfigVariable(cfg["project_id"])), + resource.TestCheckResourceAttr(intakesResource, "name", testutil.ConvertConfigVariable(cfgUpdated["intake_name"])), + resource.TestCheckResourceAttrSet(intakesResource, "intake_id"), + ), + }, + }, + }) +} + +func TestAccIntakesMax(t *testing.T) { + cfg := testIntakesConfigVarsMax() + cfgUpdated := testIntakesConfigVarsMaxUpdated() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckIntakesDestroy, + Steps: []resource.TestStep{ + // Step 1: Provision prerequisites and dynamically acquire Dremio PAT + { + ConfigVariables: cfg, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentDremio).BuildProviderConfig() + "\n" + strings.Split(resourceIntakesMax, "resource \"stackit_intakes\"")[0], + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("stackit_dremio_instance.dremio", "endpoints.ui"), + resource.TestCheckResourceAttrSet("stackit_dremio_user.dremio_user", "user_id"), + func(s *terraform.State) error { + dremioRes, ok := s.RootModule().Resources["stackit_dremio_instance.dremio"] + if !ok { + return fmt.Errorf("could not find stackit_dremio_instance.dremio in state") + } + uiEndpoint := dremioRes.Primary.Attributes["endpoints.ui"] + if uiEndpoint == "" { + return fmt.Errorf("dremio instance endpoints.ui is empty") + } + + username := testutil.ConvertConfigVariable(cfg["dremio_user_name"]) + password := testutil.ConvertConfigVariable(cfg["dremio_user_password"]) + + dremioPAT, err := getDremioPAT(context.Background(), uiEndpoint, username, password) + if err != nil { + return fmt.Errorf("failed to obtain Dremio PAT: %w", err) + } + + cfg["dremio_personal_access_token"] = config.StringVariable(dremioPAT) + cfgUpdated["dremio_personal_access_token"] = config.StringVariable(dremioPAT) + return nil + }, + ), + }, + // Step 2: Create full intake with generated Dremio PAT + { + ConfigVariables: cfg, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentDremio).BuildProviderConfig() + "\n" + resourceIntakesMax, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(intakesResource, "project_id", testutil.ConvertConfigVariable(cfg["project_id"])), + resource.TestCheckResourceAttr(intakesResource, "name", testutil.ConvertConfigVariable(cfg["intake_name"])), + resource.TestCheckResourceAttr(intakesResource, "description", testutil.ConvertConfigVariable(cfg["description"])), + resource.TestCheckResourceAttr(intakesResource, "labels.env", "development"), + resource.TestCheckResourceAttr(intakesResource, "labels.created_by", "terraform-provider-stackit"), + resource.TestCheckResourceAttr(intakesResource, "catalog_auth_type", "dremio"), + resource.TestCheckResourceAttr(intakesResource, "catalog_namespace", "intake"), + resource.TestCheckResourceAttr(intakesResource, "catalog_warehouse", "default"), + resource.TestCheckResourceAttrSet(intakesResource, "intake_id"), + resource.TestCheckResourceAttrSet(intakesResource, "runner_id"), + resource.TestCheckResourceAttrSet(intakesResource, "catalog_uri"), + resource.TestCheckResourceAttrSet(intakesResource, "catalog_table_name"), + ), + }, + // Step 3: Data source check + { + ConfigVariables: cfg, + Config: fmt.Sprintf(` + %s + %s + data "stackit_intakes" "example" { + project_id = %s.project_id + intake_id = %s.intake_id + }`, testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentDremio).BuildProviderConfig(), resourceIntakesMax, intakesResource, intakesResource), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(intakesResource, "project_id", "data.stackit_intakes.example", "project_id"), + resource.TestCheckResourceAttrPair(intakesResource, "intake_id", "data.stackit_intakes.example", "intake_id"), + resource.TestCheckResourceAttrPair(intakesResource, "name", "data.stackit_intakes.example", "name"), + resource.TestCheckResourceAttrPair(intakesResource, "description", "data.stackit_intakes.example", "description"), + resource.TestCheckResourceAttrPair(intakesResource, "catalog_auth_type", "data.stackit_intakes.example", "catalog_auth_type"), + resource.TestCheckResourceAttrPair(intakesResource, "catalog_namespace", "data.stackit_intakes.example", "catalog_namespace"), + resource.TestCheckResourceAttrPair(intakesResource, "catalog_warehouse", "data.stackit_intakes.example", "catalog_warehouse"), + ), + }, + // Step 4: Import state check (ignore write-only PAT) + { + ConfigVariables: cfg, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentDremio).BuildProviderConfig() + "\n" + resourceIntakesMax, + ResourceName: intakesResource, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"dremio_personal_access_token"}, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources[intakesResource] + if !ok { + return "", fmt.Errorf("couldn't find resource %s", intakesResource) + } + return fmt.Sprintf("%s,%s,%s", r.Primary.Attributes["project_id"], r.Primary.Attributes["region"], r.Primary.Attributes["intake_id"]), nil + }, + }, + // Step 5: Update check + { + ConfigVariables: cfgUpdated, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).Experiments(testutil.ExperimentDremio).BuildProviderConfig() + "\n" + resourceIntakesMax, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(intakesResource, "project_id", testutil.ConvertConfigVariable(cfg["project_id"])), + resource.TestCheckResourceAttr(intakesResource, "name", testutil.ConvertConfigVariable(cfgUpdated["intake_name"])), + resource.TestCheckResourceAttr(intakesResource, "description", testutil.ConvertConfigVariable(cfgUpdated["description"])), + resource.TestCheckResourceAttrSet(intakesResource, "intake_id"), + ), + }, + }, + }) +} + +// testAccCheckIntakeRunnerDestroy verifies all runners are destroyed, actively deleting any leftovers func testAccCheckIntakeRunnerDestroy(s *terraform.State) error { ctx := context.Background() - client, err := intake.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.GitCustomEndpoint, false)...) + client, err := intake.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.IntakeCustomEndpoint, false)...) if err != nil { return fmt.Errorf("creating client: %w", err) } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - instancesToDestroy := []string{} + var instancesToDestroy []string for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_intake_runner" { continue } - // Intake internal ID: "[project_id],[region],[runner_id]" runnerId := strings.Split(rs.Primary.ID, core.Separator)[2] instancesToDestroy = append(instancesToDestroy, runnerId) } - // List all resources in the project/region to see what's left instancesResp, err := client.DefaultAPI.ListIntakeRunners(ctx, testutil.ProjectId, testutil.Region).Execute() if err != nil { - return fmt.Errorf("getting instancesResp: %w", err) + return fmt.Errorf("listing intake runners: %w", err) + } + + for i := range instancesResp.IntakeRunners { + if utils.Contains(instancesToDestroy, instancesResp.IntakeRunners[i].Id) { + err := client.DefaultAPI.DeleteIntakeRunner(ctx, testutil.ProjectId, testutil.Region, instancesResp.IntakeRunners[i].Id).Execute() + if err != nil { + return fmt.Errorf("destroying runner %s during CheckDestroy: %w", instancesResp.IntakeRunners[i].Id, err) + } + + _, err = wait.DeleteIntakeRunnerWaitHandler(ctx, client.DefaultAPI, testutil.ProjectId, testutil.Region, instancesResp.IntakeRunners[i].Id).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("destroying runner %s during CheckDestroy: waiting for deletion %w", instancesResp.IntakeRunners[i].Id, err) + } + } + } + return nil +} + +// testAccCheckIntakesDestroy verifies all intakes are destroyed, actively deleting any leftovers +func testAccCheckIntakesDestroy(s *terraform.State) error { + ctx := context.Background() + client, err := intake.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.IntakeCustomEndpoint, false)...) + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + var instancesToDestroy []string + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_intakes" { + continue + } + idParts := strings.Split(rs.Primary.ID, core.Separator) + if len(idParts) < 3 { + continue + } + intakeId := idParts[2] + instancesToDestroy = append(instancesToDestroy, intakeId) + } + + instancesResp, err := client.DefaultAPI.ListIntakes(ctx, testutil.ProjectId, testutil.Region).Execute() + if err != nil { + return fmt.Errorf("listing intakes: %w", err) } - // If the API returns a list of runners, check if our deleted ones are still there - items := instancesResp.IntakeRunners - 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.DeleteIntakeRunner(ctx, testutil.ProjectId, testutil.Region, items[i].Id).Execute() + for i := range instancesResp.Intakes { + if utils.Contains(instancesToDestroy, instancesResp.Intakes[i].Id) { + err := client.DefaultAPI.DeleteIntake(ctx, testutil.ProjectId, testutil.Region, instancesResp.Intakes[i].Id).Execute() if err != nil { - return fmt.Errorf("deleting runner %s during CheckDestroy: %w", items[i].Id, err) + return fmt.Errorf("destroying intake %s during CheckDestroy: %w", instancesResp.Intakes[i].Id, err) } - // Using the wait handler for destruction verification - _, err = wait.DeleteIntakeRunnerWaitHandler(ctx, client.DefaultAPI, testutil.ProjectId, testutil.Region, items[i].Id).WaitWithContext(ctx) + _, err = wait.DeleteIntakeWaitHandler(ctx, client.DefaultAPI, testutil.ProjectId, testutil.Region, instancesResp.Intakes[i].Id).WaitWithContext(ctx) if err != nil { - return fmt.Errorf("deleting runner %s during CheckDestroy: waiting for deletion %w", items[i].Id, err) + return fmt.Errorf("destroying intake %s during CheckDestroy: waiting for deletion %w", instancesResp.Intakes[i].Id, err) } } } diff --git a/stackit/internal/services/intake/intakes/datasource.go b/stackit/internal/services/intake/intakes/datasource.go new file mode 100644 index 000000000..0f05b9dae --- /dev/null +++ b/stackit/internal/services/intake/intakes/datasource.go @@ -0,0 +1,222 @@ +package intakes + +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" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + intakeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/intake/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" +) + +// Ensure the implementation satisfies the expected interfaces +var ( + _ datasource.DataSource = &intakesDataSource{} +) + +// NewIntakesDataSource is a helper function to simplify the provider implementation +func NewIntakesDataSource() datasource.DataSource { + return &intakesDataSource{} +} + +type intakesDataSource struct { + client *intake.APIClient + providerData core.ProviderData +} + +func (d *intakesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_intakes" +} + +// Configure adds the provider configured client to the data source +func (d *intakesDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := intakeUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.client = apiClient + tflog.Info(ctx, "Intakes client configured for data source") +} + +// Schema defines the schema for the data source +func (d *intakesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + descriptions := map[string]string{ //nolint:gosec // descriptions + "main": "Datasource for STACKIT Intake.", + "id": "Terraform's internal resource identifier. It is structured as `project_id`,`region`,`intake_id`.", + "project_id": "STACKIT Project ID to which the intake is associated.", + "intake_id": "The intake ID.", + "runner_id": "The runner ID.", + "name": "The name of the intake.", + "description": "The description of the intake.", + "labels": "User-defined labels.", + "uri": "The URI of the intake.", + "create_time": "The creation time of the intake.", + "region": "The resource region. If not defined, the provider region is used.", + "dremio_personal_access_token": "The Dremio personal access token.", + "dremio_token_endpoint": "The Dremio token endpoint.", + "catalog_auth_type": "The catalog authentication type.", + "catalog_namespace": "The catalog namespace.", + "catalog_partitioning": "The catalog partitioning.", + "catalog_partition_by": "The catalog partition by.", + "catalog_table_name": "The catalog table name.", + "catalog_uri": "The catalog URI.", + "catalog_warehouse": "The catalog warehouse.", + } + + 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(), + }, + }, + "intake_id": schema.StringAttribute{ + Description: descriptions["intake_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "runner_id": schema.StringAttribute{ + Description: descriptions["runner_id"], + Computed: true, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Computed: true, + }, + "description": schema.StringAttribute{ + Description: descriptions["description"], + Computed: true, + }, + "labels": schema.MapAttribute{ + Description: descriptions["labels"], + ElementType: types.StringType, + Computed: true, + }, + "uri": schema.StringAttribute{ + Description: descriptions["uri"], + Computed: true, + }, + "create_time": schema.StringAttribute{ + Description: descriptions["create_time"], + Computed: true, + }, + "region": schema.StringAttribute{ + Optional: true, + Description: descriptions["region"], + }, + "dremio_personal_access_token": schema.StringAttribute{ + Description: descriptions["dremio_personal_access_token"], + Computed: true, + Sensitive: true, + }, + "dremio_token_endpoint": schema.StringAttribute{ + Description: descriptions["dremio_token_endpoint"], + Computed: true, + }, + "catalog_auth_type": schema.StringAttribute{ + Description: descriptions["catalog_auth_type"], + Computed: true, + }, + "catalog_namespace": schema.StringAttribute{ + Description: descriptions["catalog_namespace"], + Computed: true, + }, + "catalog_partitioning": schema.StringAttribute{ + Description: descriptions["catalog_partitioning"], + Computed: true, + }, + "catalog_partition_by": schema.ListAttribute{ + Description: descriptions["catalog_partition_by"], + ElementType: types.StringType, + Computed: true, + }, + "catalog_table_name": schema.StringAttribute{ + Description: descriptions["catalog_table_name"], + Computed: true, + }, + "catalog_uri": schema.StringAttribute{ + Description: descriptions["catalog_uri"], + Computed: true, + }, + "catalog_warehouse": schema.StringAttribute{ + Description: descriptions["catalog_warehouse"], + Computed: true, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *intakesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) + intakeId := model.IntakeId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "intake_id", intakeId) + + intakeResp, err := d.client.DefaultAPI.GetIntake(ctx, projectId, region, intakeId).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 intake", fmt.Sprintf("Intake with ID %s not found in project %s and region %s", intakeId, projectId, region)) + return + } + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading intake", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(ctx, intakeResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading intake", 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, "Intake read") +} diff --git a/stackit/internal/services/intake/intakes/resource.go b/stackit/internal/services/intake/intakes/resource.go new file mode 100644 index 000000000..8e739920b --- /dev/null +++ b/stackit/internal/services/intake/intakes/resource.go @@ -0,0 +1,747 @@ +package intakes + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" + "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/stackit-sdk-go/core/oapierror" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + intakeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/intake/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi/wait" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &intakesResource{} + _ resource.ResourceWithConfigure = &intakesResource{} + _ resource.ResourceWithImportState = &intakesResource{} + _ resource.ResourceWithModifyPlan = &intakesResource{} +) + +// Model is the internal model of the terraform resource +type Model struct { + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + RunnerId types.String `tfsdk:"runner_id"` + IntakeId types.String `tfsdk:"intake_id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Labels types.Map `tfsdk:"labels"` + Region types.String `tfsdk:"region"` + Uri types.String `tfsdk:"uri"` + CreateTime types.String `tfsdk:"create_time"` + DremioPAT types.String `tfsdk:"dremio_personal_access_token"` + DremioTokenEndpoint types.String `tfsdk:"dremio_token_endpoint"` + CatalogAuthType types.String `tfsdk:"catalog_auth_type"` + CatalogNamespace types.String `tfsdk:"catalog_namespace"` + CatalogPartitioning types.String `tfsdk:"catalog_partitioning"` + CatalogPartitionBy types.List `tfsdk:"catalog_partition_by"` + CatalogTableName types.String `tfsdk:"catalog_table_name"` + CatalogUri types.String `tfsdk:"catalog_uri"` + CatalogWarehouse types.String `tfsdk:"catalog_warehouse"` +} + +// NewIntakesResource is a helper function to simplify the provider implementation. +func NewIntakesResource() resource.Resource { + return &intakesResource{} +} + +// intakesResource is the resource implementation. +type intakesResource struct { + client *intake.APIClient + providerData core.ProviderData +} + +// Metadata returns the resource type name. +func (r *intakesResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_intakes" +} + +// Configure adds the provider configured client to the resource. +func (r *intakesResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := intakeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + r.providerData = providerData + tflog.Info(ctx, "Intakes client configured") +} + +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *intakesResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // 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 + } +} + +// Schema defines the schema for the data source +func (r *intakesResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + descriptions := map[string]string{ //nolint:gosec // descriptions + "main": "Manages STACKIT Intake.", + "id": "Terraform's internal resource identifier. It is structured as `project_id`,`region`,`intake_id`.", + "project_id": "STACKIT Project ID to which the intake is associated.", + "runner_id": "The runner ID.", + "intake_id": "The intake ID.", + "name": "The name of the intake.", + "region": "The resource region. If not defined, the provider region is used.", + "description": "The description of the intake.", + "labels": "User-defined labels.", + "uri": "The URI of the intake.", + "create_time": "The creation time of the intake.", + "dremio_personal_access_token": "The Dremio personal access token.", + "dremio_token_endpoint": "The Dremio token endpoint.", + "catalog_auth_type": "The catalog authentication type.", + "catalog_namespace": "The catalog namespace.", + "catalog_partitioning": "The catalog partitioning.", + "catalog_partition_by": "The catalog partition by.", + "catalog_table_name": "The catalog table name.", + "catalog_uri": "The catalog URI.", + "catalog_warehouse": "The catalog warehouse.", + } + + 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(), + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "runner_id": schema.StringAttribute{ + Description: descriptions["runner_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "intake_id": schema.StringAttribute{ + Description: descriptions["intake_id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Required: true, + }, + "description": schema.StringAttribute{ + Description: descriptions["description"], + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "labels": schema.MapAttribute{ + Description: descriptions["labels"], + ElementType: types.StringType, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Map{ + mapplanmodifier.UseStateForUnknown(), + }, + }, + "uri": schema.StringAttribute{ + Description: descriptions["uri"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "create_time": schema.StringAttribute{ + Description: descriptions["create_time"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "region": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: descriptions["region"], + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "dremio_personal_access_token": schema.StringAttribute{ + Description: descriptions["dremio_personal_access_token"], + Optional: true, + Sensitive: true, + }, + "dremio_token_endpoint": schema.StringAttribute{ + Description: descriptions["dremio_token_endpoint"], + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "catalog_auth_type": schema.StringAttribute{ + Description: descriptions["catalog_auth_type"], + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "catalog_namespace": schema.StringAttribute{ + Description: descriptions["catalog_namespace"], + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "catalog_partitioning": schema.StringAttribute{ + Description: descriptions["catalog_partitioning"], + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "catalog_partition_by": schema.ListAttribute{ + Description: descriptions["catalog_partition_by"], + ElementType: types.StringType, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + }, + "catalog_table_name": schema.StringAttribute{ + Description: descriptions["catalog_table_name"], + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "catalog_uri": schema.StringAttribute{ + Description: descriptions["catalog_uri"], + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "catalog_warehouse": schema.StringAttribute{ + Description: descriptions["catalog_warehouse"], + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *intakesResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := model.Region.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + + payload, err := toCreatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating intake", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + intakeResp, err := r.client.DefaultAPI.CreateIntake(ctx, projectId, region).CreateIntakePayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating intake", 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, + "intake_id": intakeResp.Id, + }) + + if resp.Diagnostics.HasError() { + return + } + + _, err = wait.CreateIntakeWaitHandler(ctx, r.client.DefaultAPI, projectId, region, intakeResp.GetId()).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating intake", fmt.Sprintf("Intake creation waiting: %v", err)) + return + } + + err = mapFields(ctx, intakeResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating intake", fmt.Sprintf("Processing API payload: %v", err)) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Intake created") +} + +// Read refreshes the Terraform state with the latest data. +func (r *intakesResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + resp.Diagnostics.Append(req.State.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + intakeId := model.IntakeId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "intake_id", intakeId) + + intakeResp, err := r.client.DefaultAPI.GetIntake(ctx, projectId, region, intakeId).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) { + if oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading intake", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(ctx, intakeResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading intake", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Intake read") +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *intakesResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + var model, state Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + intakeId := model.IntakeId.ValueString() + region := model.Region.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "intake_id", intakeId) + ctx = tflog.SetField(ctx, "region", region) + + payload, err := toUpdatePayload(ctx, &model, &state) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating intake", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + intakeResp, err := r.client.DefaultAPI.UpdateIntake(ctx, projectId, region, intakeId).UpdateIntakePayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating intake", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + _, err = wait.UpdateIntakeWaitHandler(ctx, r.client.DefaultAPI, projectId, region, intakeId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating intake", fmt.Sprintf("Intake update waiting: %v", err)) + return + } + + err = mapFields(ctx, intakeResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating intake", fmt.Sprintf("Processing API response: %v", err)) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Intake updated") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *intakesResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := model.Region.ValueString() + intakeId := model.IntakeId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "intake_id", intakeId) + + err := r.client.DefaultAPI.DeleteIntake(ctx, projectId, region, intakeId).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { + tflog.Info(ctx, "Intake already deleted") + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting intake", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + _, err = wait.DeleteIntakeWaitHandler(ctx, r.client.DefaultAPI, projectId, region, intakeId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting intake", fmt.Sprintf("Intake deletion waiting: %v", err)) + return + } + + tflog.Info(ctx, "Intake deleted") +} + +// ImportState imports a resource into the Terraform state on success. +// The expected format of the Intake resource import identifier is: [project_id],[region],[intake_id] +func (r *intakesResource) 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 intake", + fmt.Sprintf("Expected import identifier with format [project_id],[region],[intake_id], got %q", req.ID), + ) + return + } + + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "intake_id": idParts[2], + }) + + tflog.Info(ctx, "Intake state imported") +} + +// Maps intake fields to the provider internal model +func mapFields(ctx context.Context, intakeResp *intake.IntakeResponse, model *Model, region string) error { + if intakeResp == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + model.Id = utils.BuildInternalTerraformId( + model.ProjectId.ValueString(), + region, + intakeResp.Id, + ) + + labels, err := utils.MapLabels(ctx, &intakeResp.Labels, model.Labels) + if err != nil { + return err + } + + model.IntakeId = types.StringValue(intakeResp.Id) + model.RunnerId = types.StringValue(intakeResp.IntakeRunnerId) + model.Name = types.StringValue(intakeResp.DisplayName) + model.Labels = labels + model.Description = types.StringPointerValue(intakeResp.Description) + model.Region = types.StringValue(region) + model.Uri = types.StringValue(intakeResp.Uri) + model.CreateTime = types.StringValue(intakeResp.CreateTime.String()) + + model.CatalogNamespace = types.StringPointerValue(intakeResp.Catalog.Namespace) + model.CatalogTableName = types.StringPointerValue(intakeResp.Catalog.TableName) + model.CatalogUri = types.StringValue(intakeResp.Catalog.Uri) + model.CatalogWarehouse = types.StringValue(intakeResp.Catalog.Warehouse) + + if intakeResp.Catalog.Partitioning != nil { + model.CatalogPartitioning = types.StringValue(string(*intakeResp.Catalog.Partitioning)) + } else { + model.CatalogPartitioning = types.StringNull() + } + + if intakeResp.Catalog.PartitionBy != nil { + partitionByList, diags := types.ListValueFrom(ctx, types.StringType, intakeResp.Catalog.PartitionBy) + if diags.HasError() { + return fmt.Errorf("converting partition_by list: %v", diags) + } + model.CatalogPartitionBy = partitionByList + } else { + model.CatalogPartitionBy = types.ListNull(types.StringType) + } + + if intakeResp.Catalog.Auth != nil { + model.CatalogAuthType = types.StringValue(string(intakeResp.Catalog.Auth.Type)) + if intakeResp.Catalog.Auth.Dremio != nil { + model.DremioTokenEndpoint = types.StringValue(intakeResp.Catalog.Auth.Dremio.TokenEndpoint) + } else { + model.DremioTokenEndpoint = types.StringNull() + } + } else { + model.CatalogAuthType = types.StringNull() + model.DremioTokenEndpoint = types.StringNull() + } + + if model.DremioPAT.IsUnknown() { + model.DremioPAT = types.StringNull() + } + + return nil +} + +func toCreatePayload(ctx context.Context, model *Model) (*intake.CreateIntakePayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + labels, err := utils.LabelsToPayload(ctx, model.Labels) + if err != nil { + return nil, err + } + + partitionBy, err := conversion.StringListToSlice(model.CatalogPartitionBy) + if err != nil { + return nil, err + } + + var partitioning *intake.PartitioningType + if !model.CatalogPartitioning.IsNull() && !model.CatalogPartitioning.IsUnknown() { + p, err := intake.NewPartitioningTypeFromValue(model.CatalogPartitioning.ValueString()) + if err != nil { + return nil, err + } + partitioning = p + } + + var auth *intake.CatalogAuth + if !model.CatalogAuthType.IsNull() && !model.CatalogAuthType.IsUnknown() { + authType := model.CatalogAuthType.ValueString() + auth = &intake.CatalogAuth{ + Type: intake.CatalogAuthType(authType), + } + if authType == "dremio" { + auth.Dremio = intake.NewDremioAuth(model.DremioPAT.ValueString(), model.DremioTokenEndpoint.ValueString()) + } + } + + return &intake.CreateIntakePayload{ + Description: conversion.StringValueToPointer(model.Description), + DisplayName: model.Name.ValueString(), + IntakeRunnerId: model.RunnerId.ValueString(), + Labels: labels, + Catalog: intake.IntakeCatalog{ + Auth: auth, + Namespace: conversion.StringValueToPointer(model.CatalogNamespace), + PartitionBy: partitionBy, + Partitioning: partitioning, + TableName: conversion.StringValueToPointer(model.CatalogTableName), + Uri: model.CatalogUri.ValueString(), + Warehouse: model.CatalogWarehouse.ValueString(), + }, + }, nil +} + +func hasCatalogChanged(model, state *Model) bool { + if state == nil { + return !model.CatalogUri.IsNull() || !model.CatalogWarehouse.IsNull() || !model.CatalogNamespace.IsNull() || !model.CatalogTableName.IsNull() || !model.CatalogAuthType.IsNull() || !model.CatalogPartitioning.IsNull() || !model.CatalogPartitionBy.IsNull() + } + if !model.CatalogUri.Equal(state.CatalogUri) { + return true + } + if !model.CatalogWarehouse.Equal(state.CatalogWarehouse) { + return true + } + if !model.CatalogNamespace.Equal(state.CatalogNamespace) { + return true + } + if !model.CatalogTableName.Equal(state.CatalogTableName) { + return true + } + if !model.CatalogAuthType.Equal(state.CatalogAuthType) { + return true + } + if !model.CatalogPartitioning.Equal(state.CatalogPartitioning) { + return true + } + if !model.CatalogPartitionBy.Equal(state.CatalogPartitionBy) { + return true + } + if !model.DremioPAT.Equal(state.DremioPAT) { + return true + } + if !model.DremioTokenEndpoint.Equal(state.DremioTokenEndpoint) { + return true + } + return false +} + +// Build UpdateIntakePayload from provider's model +func toUpdatePayload(ctx context.Context, model, state *Model) (*intake.UpdateIntakePayload, error) { + if model == nil { + return nil, fmt.Errorf("model is nil") + } + + payload := &intake.UpdateIntakePayload{} + if !model.RunnerId.IsNull() && !model.RunnerId.IsUnknown() { + payload.IntakeRunnerId = model.RunnerId.ValueString() + } + if !model.Name.IsNull() && !model.Name.IsUnknown() { + payload.DisplayName = conversion.StringValueToPointer(model.Name) + } + if !model.Description.IsNull() && !model.Description.IsUnknown() { + payload.Description = conversion.StringValueToPointer(model.Description) + } + + labels, err := utils.LabelsToPayload(ctx, model.Labels) + if err != nil { + return nil, err + } + payload.Labels = labels + + if hasCatalogChanged(model, state) { + catalog := &intake.IntakeCatalogPatch{} + if !model.CatalogUri.IsNull() && !model.CatalogUri.IsUnknown() { + catalog.Uri = conversion.StringValueToPointer(model.CatalogUri) + } + if !model.CatalogWarehouse.IsNull() && !model.CatalogWarehouse.IsUnknown() { + catalog.Warehouse = conversion.StringValueToPointer(model.CatalogWarehouse) + } + if !model.CatalogNamespace.IsNull() && !model.CatalogNamespace.IsUnknown() { + catalog.Namespace = conversion.StringValueToPointer(model.CatalogNamespace) + } + if !model.CatalogTableName.IsNull() && !model.CatalogTableName.IsUnknown() { + catalog.TableName = conversion.StringValueToPointer(model.CatalogTableName) + } + if !model.CatalogPartitionBy.IsNull() && !model.CatalogPartitionBy.IsUnknown() { + partitionBy, err := conversion.StringListToSlice(model.CatalogPartitionBy) + if err != nil { + return nil, err + } + catalog.PartitionBy = partitionBy + } + if !model.CatalogPartitioning.IsNull() && !model.CatalogPartitioning.IsUnknown() { + p, err := intake.NewPartitioningUpdateTypeFromValue(model.CatalogPartitioning.ValueString()) + if err != nil { + return nil, err + } + catalog.Partitioning = p + } + var auth *intake.CatalogAuthPatch + if !model.CatalogAuthType.IsNull() && !model.CatalogAuthType.IsUnknown() { + authType := model.CatalogAuthType.ValueString() + authTypeVal, err := intake.NewCatalogAuthTypeFromValue(authType) + if err != nil { + return nil, err + } + auth = &intake.CatalogAuthPatch{ + Type: authTypeVal, + } + if authType == "dremio" { + dremioAuth := &intake.DremioAuthPatch{} + if !model.DremioPAT.IsNull() && !model.DremioPAT.IsUnknown() { + dremioAuth.PersonalAccessToken = conversion.StringValueToPointer(model.DremioPAT) + } + if !model.DremioTokenEndpoint.IsNull() && !model.DremioTokenEndpoint.IsUnknown() { + dremioAuth.TokenEndpoint = conversion.StringValueToPointer(model.DremioTokenEndpoint) + } + auth.Dremio = dremioAuth + } + } + catalog.Auth = auth + payload.Catalog = catalog + } + + return payload, nil +} diff --git a/stackit/internal/services/intake/intakes/resource_test.go b/stackit/internal/services/intake/intakes/resource_test.go new file mode 100644 index 000000000..641d8b92f --- /dev/null +++ b/stackit/internal/services/intake/intakes/resource_test.go @@ -0,0 +1,304 @@ +package intakes + +import ( + "context" + "fmt" + "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" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" +) + +func TestMapFields(t *testing.T) { + intakeId := uuid.New().String() + runnerId := uuid.New().String() + now := time.Now() + + tests := []struct { + description string + input *intake.IntakeResponse + model *Model + region string + expected *Model + wantErr bool + }{ + { + "success", + &intake.IntakeResponse{ + Id: intakeId, + IntakeRunnerId: runnerId, + DisplayName: "name", + Description: utils.Ptr("description"), + Labels: map[string]string{"key": "value"}, + Uri: "https://intake.eu01.onstackit.cloud", + CreateTime: now, + Catalog: intake.IntakeCatalog{ + Namespace: utils.Ptr("intake_ns"), + TableName: utils.Ptr("intake_table"), + Uri: "https://catalog.dremio.eu01.onstackit.cloud", + Warehouse: "default", + Partitioning: utils.Ptr(intake.PartitioningType("DAY")), + PartitionBy: []string{"col1", "col2"}, + Auth: &intake.CatalogAuth{ + Type: intake.CatalogAuthType("dremio"), + Dremio: &intake.DremioAuth{ //nolint:gosec // mock test data + TokenEndpoint: "https://dremio.eu01.onstackit.cloud/oauth/endpoint", + }, + }, + }, + }, + &Model{ + ProjectId: types.StringValue("pid"), + DremioPAT: types.StringValue("secret_token"), + }, + "eu01", + &Model{ + Id: types.StringValue(fmt.Sprintf("pid,eu01,%s", intakeId)), + ProjectId: types.StringValue("pid"), + Region: types.StringValue("eu01"), + IntakeId: types.StringValue(intakeId), + RunnerId: types.StringValue(runnerId), + Name: types.StringValue("name"), + Description: types.StringValue("description"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{"key": types.StringValue("value")}), + Uri: types.StringValue("https://intake.eu01.onstackit.cloud"), + CreateTime: types.StringValue(now.String()), + DremioPAT: types.StringValue("secret_token"), + DremioTokenEndpoint: types.StringValue("https://dremio.eu01.onstackit.cloud/oauth/endpoint"), + CatalogAuthType: types.StringValue("dremio"), + CatalogNamespace: types.StringValue("intake_ns"), + CatalogPartitioning: types.StringValue("DAY"), + CatalogPartitionBy: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("col1"), types.StringValue("col2")}), + CatalogTableName: types.StringValue("intake_table"), + CatalogUri: types.StringValue("https://catalog.dremio.eu01.onstackit.cloud"), + CatalogWarehouse: types.StringValue("default"), + }, + false, + }, + { + "nil input", + nil, + &Model{}, + "eu01", + nil, + true, + }, + { + "nil model", + &intake.IntakeResponse{}, + nil, + "eu01", + nil, + true, + }, + { + "empty response", + &intake.IntakeResponse{ + Id: "", + Labels: map[string]string{}, + }, + &Model{ + ProjectId: types.StringValue("pid"), + }, + "eu01", + &Model{ + Id: types.StringValue("pid,eu01,"), + ProjectId: types.StringValue("pid"), + Region: types.StringValue("eu01"), + IntakeId: types.StringValue(""), + RunnerId: types.StringValue(""), + Name: types.StringValue(""), + Description: types.StringNull(), + Labels: types.MapNull(types.StringType), + Uri: types.StringValue(""), + CreateTime: types.StringValue(time.Time{}.String()), + DremioPAT: types.StringNull(), + DremioTokenEndpoint: types.StringNull(), + CatalogAuthType: types.StringNull(), + CatalogNamespace: types.StringNull(), + CatalogPartitioning: types.StringNull(), + CatalogPartitionBy: types.ListNull(types.StringType), + CatalogTableName: types.StringNull(), + CatalogUri: types.StringValue(""), + CatalogWarehouse: types.StringValue(""), + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapFields(context.Background(), tt.input, tt.model, tt.region) + 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.model); diff != "" { + t.Errorf("mapFields mismatch (-want +got):\n%s", diff) + } + } + }) + } +} + +func TestToCreatePayload(t *testing.T) { + runnerId := uuid.New().String() + + tests := []struct { + description string + model *Model + expected *intake.CreateIntakePayload + wantErr bool + }{ + { + "success", + &Model{ + RunnerId: types.StringValue(runnerId), + Name: types.StringValue("name"), + Description: types.StringValue("description"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{"key": types.StringValue("value")}), + DremioPAT: types.StringValue("token"), + DremioTokenEndpoint: types.StringValue("https://dremio.eu01.onstackit.cloud/oauth/endpoint"), + CatalogAuthType: types.StringValue("dremio"), + CatalogNamespace: types.StringValue("ns"), + CatalogPartitioning: types.StringValue("intake-time"), + CatalogPartitionBy: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("col1"), types.StringValue("col2")}), + CatalogTableName: types.StringValue("table"), + CatalogUri: types.StringValue("https://catalog.uri"), + CatalogWarehouse: types.StringValue("wh"), + }, + &intake.CreateIntakePayload{ + IntakeRunnerId: runnerId, + DisplayName: "name", + Description: utils.Ptr("description"), + Labels: map[string]string{"key": "value"}, + Catalog: intake.IntakeCatalog{ + Auth: &intake.CatalogAuth{ + Type: intake.CatalogAuthType("dremio"), + Dremio: intake.NewDremioAuth("token", "https://dremio.eu01.onstackit.cloud/oauth/endpoint"), + }, + Namespace: utils.Ptr("ns"), + PartitionBy: []string{"col1", "col2"}, + Partitioning: utils.Ptr(intake.PartitioningType("intake-time")), + TableName: utils.Ptr("table"), + Uri: "https://catalog.uri", + Warehouse: "wh", + }, + }, + false, + }, + { + "nil model", + nil, + nil, + true, + }, + { + "empty model", + &Model{}, + &intake.CreateIntakePayload{ + IntakeRunnerId: "", + DisplayName: "", + Description: nil, + Labels: map[string]string{}, + Catalog: intake.IntakeCatalog{ + Auth: nil, + Namespace: nil, + PartitionBy: nil, + Partitioning: nil, + TableName: nil, + Uri: "", + Warehouse: "", + }, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + payload, err := toCreatePayload(context.Background(), tt.model) + 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 + model *Model + expected *intake.UpdateIntakePayload + wantErr bool + }{ + { + "success", + &Model{ + RunnerId: types.StringValue("runner-id"), + Name: types.StringValue("name"), + Description: types.StringValue("description"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{"key": types.StringValue("value")}), + }, + &intake.UpdateIntakePayload{ + IntakeRunnerId: "runner-id", + DisplayName: conversion.StringValueToPointer(types.StringValue("name")), + Description: conversion.StringValueToPointer(types.StringValue("description")), + Labels: map[string]string{"key": "value"}, + }, + false, + }, + { + "nil model", + nil, + nil, + true, + }, + { + "empty model", + &Model{}, + &intake.UpdateIntakePayload{ + Labels: map[string]string{}, + }, + false, + }, + { + "unknown values", + &Model{ + Name: types.StringUnknown(), + Description: types.StringUnknown(), + Labels: types.MapUnknown(types.StringType), + }, + &intake.UpdateIntakePayload{ + Labels: map[string]string{}, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + payload, err := toUpdatePayload(context.Background(), tt.model, nil) + 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/internal/services/intake/runner/resource.go b/stackit/internal/services/intake/runner/resource.go index 53738c288..7dadd6078 100644 --- a/stackit/internal/services/intake/runner/resource.go +++ b/stackit/internal/services/intake/runner/resource.go @@ -252,7 +252,7 @@ func (r *runnerResource) Create(ctx context.Context, req resource.CreateRequest, } // Wait for creation of intake runner - _, err = wait.CreateIntakeWaitHandler(ctx, r.client.DefaultAPI, projectId, region, runnerResp.GetId()).WaitWithContext(ctx) + _, err = wait.CreateIntakeRunnerWaitHandler(ctx, r.client.DefaultAPI, projectId, region, runnerResp.GetId()).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating runner", fmt.Sprintf("Intake runner creation waiting: %v", err)) return @@ -351,7 +351,7 @@ func (r *runnerResource) Update(ctx context.Context, req resource.UpdateRequest, ctx = core.LogResponse(ctx) // Wait for update - _, err = wait.UpdateIntakeWaitHandler(ctx, r.client.DefaultAPI, projectId, region, runnerId).WaitWithContext(ctx) + _, err = wait.UpdateIntakeRunnerWaitHandler(ctx, r.client.DefaultAPI, projectId, region, runnerId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating runner", fmt.Sprintf("Runner update waiting: %v", err)) return diff --git a/stackit/internal/services/intake/testdata/resource-intake-max.tf b/stackit/internal/services/intake/testdata/resource-intake-max.tf new file mode 100644 index 000000000..164641b5b --- /dev/null +++ b/stackit/internal/services/intake/testdata/resource-intake-max.tf @@ -0,0 +1,64 @@ +variable "project_id" {} +variable "region" {} +variable "runner_name" {} +variable "intake_name" {} +variable "description" {} +variable "max_message_size_kib" {} +variable "max_messages_per_hour" {} + +variable "dremio_display_name" {} +variable "dremio_user_email" {} +variable "dremio_user_first_name" {} +variable "dremio_user_last_name" {} +variable "dremio_user_name" {} +variable "dremio_user_password" {} +variable "dremio_personal_access_token" {} + +resource "stackit_intake_runner" "example" { + project_id = var.project_id + region = var.region + name = var.runner_name + max_message_size_kib = var.max_message_size_kib + max_messages_per_hour = var.max_messages_per_hour +} + +resource "stackit_dremio_instance" "dremio" { + project_id = var.project_id + region = var.region + display_name = var.dremio_display_name + authentication = { + type = "local-only" + } +} + +resource "stackit_dremio_user" "dremio_user" { + project_id = var.project_id + region = var.region + instance_id = stackit_dremio_instance.dremio.instance_id + + email = var.dremio_user_email + first_name = var.dremio_user_first_name + last_name = var.dremio_user_last_name + name = var.dremio_user_name + password = var.dremio_user_password +} + +resource "stackit_intakes" "example" { + project_id = var.project_id + region = var.region + runner_id = stackit_intake_runner.example.runner_id + name = var.intake_name + description = var.description + + labels = { + "env" = "development" + "created_by" = "terraform-provider-stackit" + } + + catalog_auth_type = "dremio" + catalog_namespace = "intake" + catalog_warehouse = "default" + catalog_uri = startswith(stackit_dremio_instance.dremio.endpoints.catalog, "https://") ? stackit_dremio_instance.dremio.endpoints.catalog : "https://${stackit_dremio_instance.dremio.endpoints.catalog}" + dremio_token_endpoint = startswith(stackit_dremio_instance.dremio.endpoints.ui, "https://") ? "${stackit_dremio_instance.dremio.endpoints.ui}/oauth/token" : "https://${stackit_dremio_instance.dremio.endpoints.ui}/oauth/token" + dremio_personal_access_token = var.dremio_personal_access_token +} diff --git a/stackit/internal/services/intake/testdata/resource-intake-min.tf b/stackit/internal/services/intake/testdata/resource-intake-min.tf new file mode 100644 index 000000000..8ebaf42ca --- /dev/null +++ b/stackit/internal/services/intake/testdata/resource-intake-min.tf @@ -0,0 +1,50 @@ +variable "project_id" {} +variable "runner_name" {} +variable "intake_name" {} +variable "max_message_size_kib" {} +variable "max_messages_per_hour" {} + +variable "dremio_display_name" {} +variable "dremio_user_email" {} +variable "dremio_user_first_name" {} +variable "dremio_user_last_name" {} +variable "dremio_user_name" {} +variable "dremio_user_password" {} +variable "dremio_personal_access_token" {} + +resource "stackit_intake_runner" "example" { + project_id = var.project_id + name = var.runner_name + max_message_size_kib = var.max_message_size_kib + max_messages_per_hour = var.max_messages_per_hour +} + +resource "stackit_dremio_instance" "dremio" { + project_id = var.project_id + display_name = var.dremio_display_name + authentication = { + type = "local-only" + } +} + +resource "stackit_dremio_user" "dremio_user" { + project_id = var.project_id + instance_id = stackit_dremio_instance.dremio.instance_id + + email = var.dremio_user_email + first_name = var.dremio_user_first_name + last_name = var.dremio_user_last_name + name = var.dremio_user_name + password = var.dremio_user_password +} + +resource "stackit_intakes" "example" { + project_id = var.project_id + runner_id = stackit_intake_runner.example.runner_id + name = var.intake_name + catalog_auth_type = "dremio" + catalog_warehouse = "default" + catalog_uri = startswith(stackit_dremio_instance.dremio.endpoints.catalog, "https://") ? stackit_dremio_instance.dremio.endpoints.catalog : "https://${stackit_dremio_instance.dremio.endpoints.catalog}" + dremio_token_endpoint = startswith(stackit_dremio_instance.dremio.endpoints.ui, "https://") ? "${stackit_dremio_instance.dremio.endpoints.ui}/oauth/token" : "https://${stackit_dremio_instance.dremio.endpoints.ui}/oauth/token" + dremio_personal_access_token = var.dremio_personal_access_token +} diff --git a/stackit/internal/services/intake/testdata/resource-max.tf b/stackit/internal/services/intake/testdata/resource-runner-max.tf similarity index 100% rename from stackit/internal/services/intake/testdata/resource-max.tf rename to stackit/internal/services/intake/testdata/resource-runner-max.tf diff --git a/stackit/internal/services/intake/testdata/resource-min.tf b/stackit/internal/services/intake/testdata/resource-runner-min.tf similarity index 100% rename from stackit/internal/services/intake/testdata/resource-min.tf rename to stackit/internal/services/intake/testdata/resource-runner-min.tf diff --git a/stackit/provider.go b/stackit/provider.go index 49c1af158..82db41e4f 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -69,6 +69,7 @@ import ( iaasAlphaVpcRoutingTable "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/vpcroutingtable" iaasAlphaVpcStaticRoute "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/vpcroutingtable/staticroute" iamRoleBindingsV1 "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1" + intakes "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/intake/intakes" intakeRunner "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/intake/runner" kmsKey "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/key" kmsKeyRing "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/keyring" @@ -716,6 +717,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource iaasRoutingTableRoutes.NewRoutingTableRoutesDataSource, iaasSecurityGroupRule.NewSecurityGroupRuleDataSource, intakeRunner.NewRunnerDataSource, + intakes.NewIntakesDataSource, kmsKey.NewKeyDataSource, kmsKeyRing.NewKeyRingDataSource, kmsWrappingKey.NewWrappingKeyDataSource, @@ -836,6 +838,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { iaasRoutingTable.NewRoutingTableResource, iaasRoutingTableRoute.NewRoutingTableRouteResource, intakeRunner.NewRunnerResource, + intakes.NewIntakesResource, kmsKey.NewKeyResource, kmsKeyRing.NewKeyRingResource, kmsWrappingKey.NewWrappingKeyResource,