From 118274c3f1e3f86a92b38970b91ba2880417669f Mon Sep 17 00:00:00 2001 From: Ashton Walden Date: Thu, 19 Dec 2024 10:14:46 -0800 Subject: [PATCH] Migrate project artefacts to framework from sdkv2 --- .gitignore | 3 +- .../objects/project_artefacts/model.go | 12 + .../objects/project_artefacts/resource.go | 238 ++++++++++++++++++ .../resource_acceptance_test.go | 187 ++++++++++++++ .../objects/project_artefacts/schema.go | 47 ++++ pkg/provider/framework_provider.go | 32 +-- pkg/provider/sdk_provider.go | 1 - 7 files changed, 504 insertions(+), 16 deletions(-) create mode 100644 pkg/framework/objects/project_artefacts/model.go create mode 100644 pkg/framework/objects/project_artefacts/resource.go create mode 100644 pkg/framework/objects/project_artefacts/resource_acceptance_test.go create mode 100644 pkg/framework/objects/project_artefacts/schema.go diff --git a/.gitignore b/.gitignore index 611e48c..4c48cab 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ terraform-provider* autogen TODO.md NOTES.md -.DS_Store \ No newline at end of file +.DS_Store +.env \ No newline at end of file diff --git a/pkg/framework/objects/project_artefacts/model.go b/pkg/framework/objects/project_artefacts/model.go new file mode 100644 index 0000000..c950ee3 --- /dev/null +++ b/pkg/framework/objects/project_artefacts/model.go @@ -0,0 +1,12 @@ +package project_artefacts + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type ProjectArtefactsResourceModel struct { + ID types.String `tfsdk:"id"` + ProjectID types.Int64 `tfsdk:"project_id"` + DocsJobID types.Int64 `tfsdk:"docs_job_id"` + FreshnessJobID types.Int64 `tfsdk:"freshness_job_id"` +} diff --git a/pkg/framework/objects/project_artefacts/resource.go b/pkg/framework/objects/project_artefacts/resource.go new file mode 100644 index 0000000..36adbb6 --- /dev/null +++ b/pkg/framework/objects/project_artefacts/resource.go @@ -0,0 +1,238 @@ +package project_artefacts + +import ( + "context" + "strconv" + "strings" + + "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/dbt_cloud" + "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/helper" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &projectArtefactsResource{} + _ resource.ResourceWithConfigure = &projectArtefactsResource{} + _ resource.ResourceWithImportState = &projectArtefactsResource{} +) + +type projectArtefactsResource struct { + client *dbt_cloud.Client +} + +// ImportState implements resource.ResourceWithImportState. +func (p *projectArtefactsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + id_as_int, err := strconv.Atoi(req.ID) + if err != nil { + resp.Diagnostics.AddError("Invalid ID", "The ID must be an integer") + return + } + resp.State.SetAttribute(ctx, path.Root("id"), req.ID) + resp.State.SetAttribute(ctx, path.Root("project_id"), id_as_int) + +} + +// Configure implements resource.ResourceWithConfigure. +func (p *projectArtefactsResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + switch c := req.ProviderData.(type) { + case nil: // do nothing + case *dbt_cloud.Client: + p.client = c + default: + resp.Diagnostics.AddError("Missing client", "A client is required to configure the project artefacts resource") + } +} + +// Create implements resource.Resource. +func (p *projectArtefactsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ProjectArtefactsResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + projectIDString := strconv.FormatInt(plan.ProjectID.ValueInt64(), 10) + + project, err := p.client.GetProject(projectIDString) + if err != nil { + resp.Diagnostics.AddError( + "Unable to get project", + "Error: "+err.Error(), + ) + + return + } + + if plan.DocsJobID.ValueInt64() != 0 { + conv := int(plan.DocsJobID.ValueInt64()) + project.DocsJobId = &conv + } else { + project.DocsJobId = nil + } + + if plan.FreshnessJobID.ValueInt64() != 0 { + conv := int(plan.FreshnessJobID.ValueInt64()) + project.FreshnessJobId = &conv + } else { + project.FreshnessJobId = nil + } + + if _, err := p.client.UpdateProject(projectIDString, *project); err != nil { + resp.Diagnostics.AddError( + "Unable to update project", + "Error: "+err.Error(), + ) + + return + } + + plan.ID = types.StringValue(strconv.Itoa(*project.ID)) + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +// Delete implements resource.Resource. +func (p *projectArtefactsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ProjectArtefactsResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + projectIDString := strconv.FormatInt(state.ProjectID.ValueInt64(), 10) + + project, err := p.client.GetProject(projectIDString) + if err != nil { + resp.Diagnostics.AddError( + "Unable to get project", + "Error: "+err.Error(), + ) + + return + } + + project.FreshnessJobId = nil + project.DocsJobId = nil + + _, err = p.client.UpdateProject(projectIDString, *project) + if err != nil { + resp.Diagnostics.AddError( + "Unable to update project", + "Error: "+err.Error(), + ) + + return + } +} + +// Metadata implements resource.Resource. +func (p *projectArtefactsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project_artefacts" +} + +// Read implements resource.Resource. +func (p *projectArtefactsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ProjectArtefactsResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + projectIDString := strconv.FormatInt(state.ProjectID.ValueInt64(), 10) + + project, err := p.client.GetProject(projectIDString) + if err != nil { + if strings.HasPrefix(err.Error(), "resource-not-found") { + resp.Diagnostics.AddError( + "Project not found", + "The project artefacts resource was not found and has been removed from the state.", + ) + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + "Unable to get project", + "Error: "+err.Error(), + ) + + return + } + + state.ID = types.StringValue(strconv.Itoa(*project.ID)) + if project.DocsJobId != nil { + state.DocsJobID = types.Int64PointerValue(helper.IntPointerToInt64Pointer(project.DocsJobId)) + } + + if project.FreshnessJobId != nil { + state.FreshnessJobID = types.Int64PointerValue(helper.IntPointerToInt64Pointer(project.FreshnessJobId)) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) + +} + +// Update implements resource.Resource. +func (p *projectArtefactsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state ProjectArtefactsResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + projectIDString := strconv.FormatInt(plan.ProjectID.ValueInt64(), 10) + + project, err := p.client.GetProject(projectIDString) + if err != nil { + resp.Diagnostics.AddError( + "Unable to get project", + "Error: "+err.Error(), + ) + + return + } + + if !state.DocsJobID.Equal(plan.DocsJobID) { + if plan.DocsJobID.ValueInt64() != 0 { + conv := int(plan.DocsJobID.ValueInt64()) + project.DocsJobId = &conv + } else { + project.DocsJobId = nil + } + } + + if !state.FreshnessJobID.Equal(plan.FreshnessJobID) { + if plan.FreshnessJobID.ValueInt64() != 0 { + conv := int(plan.FreshnessJobID.ValueInt64()) + project.FreshnessJobId = &conv + } else { + project.FreshnessJobId = nil + } + } + + project, err = p.client.UpdateProject(projectIDString, *project) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to update project", + "Error: "+err.Error(), + ) + + return + } + + plan.DocsJobID = types.Int64PointerValue(helper.IntPointerToInt64Pointer(project.DocsJobId)) + plan.FreshnessJobID = types.Int64PointerValue(helper.IntPointerToInt64Pointer(project.FreshnessJobId)) + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func ProjectArtefactsResource() resource.Resource { + return &projectArtefactsResource{} +} diff --git a/pkg/framework/objects/project_artefacts/resource_acceptance_test.go b/pkg/framework/objects/project_artefacts/resource_acceptance_test.go new file mode 100644 index 0000000..99fcab6 --- /dev/null +++ b/pkg/framework/objects/project_artefacts/resource_acceptance_test.go @@ -0,0 +1,187 @@ +package project_artefacts_test + +import ( + "fmt" + "regexp" + "strings" + "testing" + + "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/framework/acctest_helper" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccDbtCloudProjectArtefactsResource(t *testing.T) { + + projectName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + jobName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + environmentName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest_helper.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest_helper.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDbtCloudProjectArtefactsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDbtCloudProjectArtefactsResourceBasicConfig( + projectName, + environmentName, + jobName, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckDbtCloudProjectArtefactsExists( + "dbtcloud_project_artefacts.test_project_artefacts", + ), + ), + }, + // IMPORT + { + ResourceName: "dbtcloud_project_artefacts.test_project_artefacts", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{}, + }, + // EMPTY + { + Config: testAccDbtCloudProjectArtefactsResourceEmptyConfig(projectName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDbtCloudProjectArtefactsEmpty("dbtcloud_project.test_project"), + ), + }, + }, + }) +} + +func testAccDbtCloudProjectArtefactsResourceBasicConfig( + projectName, environmentName, jobName string, +) string { + return fmt.Sprintf(` +resource "dbtcloud_project" "test_artefacts_project" { + name = "%s" +} + +resource "dbtcloud_environment" "test_job_environment" { + project_id = dbtcloud_project.test_artefacts_project.id + name = "%s" + dbt_version = "%s" + type = "development" +} + +resource "dbtcloud_job" "test_job" { + name = "%s" + project_id = dbtcloud_project.test_artefacts_project.id + environment_id = dbtcloud_environment.test_job_environment.environment_id + execute_steps = [ + "dbt test" + ] + triggers = { + "github_webhook": false, + "git_provider_webhook": false, + "schedule": false, + } + run_generate_sources = true + generate_docs = true +} + +resource "dbtcloud_project_artefacts" "test_project_artefacts" { + project_id = dbtcloud_project.test_artefacts_project.id + docs_job_id = dbtcloud_job.test_job.id + freshness_job_id = dbtcloud_job.test_job.id +} +`, projectName, environmentName, acctest_helper.DBT_CLOUD_VERSION, jobName) +} + +func testAccDbtCloudProjectArtefactsResourceEmptyConfig(projectName string) string { + return fmt.Sprintf(` +resource "dbtcloud_project" "test_project" { + name = "%s" +} + +resource "dbtcloud_project_artefacts" "test_project_artefacts" { + project_id = dbtcloud_project.test_project.id + docs_job_id = 0 + freshness_job_id = 0 + } +`, projectName) +} + +func testAccCheckDbtCloudProjectArtefactsExists(resource string) resource.TestCheckFunc { + return func(state *terraform.State) error { + rs, ok := state.RootModule().Resources[resource] + if !ok { + return fmt.Errorf("Not found: %s", resource) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No Record ID is set") + } + apiClient, err := acctest_helper.SharedClient() + if err != nil { + return fmt.Errorf("Issue getting the client") + } + projectId := rs.Primary.ID + project, err := apiClient.GetProject(projectId) + if err != nil { + return fmt.Errorf("Can't get project") + } + if project.DocsJobId == nil { + return fmt.Errorf("error fetching item with resource %s. %s", resource, err) + } + if project.FreshnessJobId == nil { + return fmt.Errorf("error fetching item with resource %s. %s", resource, err) + } + return nil + } +} + +func testAccCheckDbtCloudProjectArtefactsEmpty(resource string) resource.TestCheckFunc { + return func(state *terraform.State) error { + rs, ok := state.RootModule().Resources[resource] + if !ok { + return fmt.Errorf("Not found: %s", resource) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No Record ID is set") + } + apiClient, err := acctest_helper.SharedClient() + if err != nil { + return fmt.Errorf("Issue getting the client") + } + project, err := apiClient.GetProject(rs.Primary.ID) + if err != nil { + return fmt.Errorf("Can't get project") + } + if project.DocsJobId != nil { + return fmt.Errorf("error fetching item with resource %s. %s", resource, err) + } + if project.FreshnessJobId != nil { + return fmt.Errorf("error fetching item with resource %s. %s", resource, err) + } + return nil + } +} + +func testAccCheckDbtCloudProjectArtefactsDestroy(s *terraform.State) error { + apiClient, err := acctest_helper.SharedClient() + if err != nil { + return fmt.Errorf("Issue getting the client") + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "dbtcloud_project_artefacts" { + continue + } + projectId := rs.Primary.ID + project, err := apiClient.GetProject(projectId) + if project != nil { + return fmt.Errorf("Project still exists") + } + notFoundErr := "resource-not-found" + expectedErr := regexp.MustCompile(notFoundErr) + if !expectedErr.Match([]byte(err.Error())) { + return fmt.Errorf("expected %s, got %s", notFoundErr, err) + } + } + + return nil +} diff --git a/pkg/framework/objects/project_artefacts/schema.go b/pkg/framework/objects/project_artefacts/schema.go new file mode 100644 index 0000000..8f6cf63 --- /dev/null +++ b/pkg/framework/objects/project_artefacts/schema.go @@ -0,0 +1,47 @@ +package project_artefacts + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" +) + +// Schema implements resource.Resource. +func (p *projectArtefactsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "[Deprecated] Resource for mentioning what jobs are the source of truth for the legacy dbt Docs and dbt Source Freshness pages. dbt Explorer doesn't require this config anymore.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "The ID of the project artefacts resource.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project_id": schema.Int64Attribute{ + Description: "Project ID", + Required: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + }, + "docs_job_id": schema.Int64Attribute{ + Description: "Docs Job ID", + Optional: true, + Computed: true, + Default: int64default.StaticInt64(0), + }, + "freshness_job_id": schema.Int64Attribute{ + Description: "Freshness Job ID", + Optional: true, + Computed: true, + Default: int64default.StaticInt64(0), + }, + }, + } +} diff --git a/pkg/provider/framework_provider.go b/pkg/provider/framework_provider.go index f39ec80..c75befe 100644 --- a/pkg/provider/framework_provider.go +++ b/pkg/provider/framework_provider.go @@ -5,6 +5,9 @@ import ( "os" "strconv" + "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/framework/objects/azure_dev_ops_project" + "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/framework/objects/project_artefacts" + "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/dbt_cloud" "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/framework/objects/account_features" "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/framework/objects/azure_dev_ops_project" @@ -186,33 +189,34 @@ func (p *dbtCloudProvider) DataSources(_ context.Context) []func() datasource.Da return []func() datasource.DataSource{ azure_dev_ops_project.AzureDevOpsProjectDataSource, azure_dev_ops_repository.AzureDevOpsRepositoryDataSource, - user.UserDataSource, - user.UsersDataSource, - notification.NotificationDataSource, environment.EnvironmentDataSource, environment.EnvironmentsDataSource, + global_connection.GlobalConnectionDataSource, + global_connection.GlobalConnectionsDataSource, group.GroupDataSource, job.JobsDataSource, - service_token.ServiceTokenDataSource, + notification.NotificationDataSource, project.ProjectsDataSource, - global_connection.GlobalConnectionDataSource, - global_connection.GlobalConnectionsDataSource, + service_token.ServiceTokenDataSource, + user.UserDataSource, + user.UsersDataSource, } } func (p *dbtCloudProvider) Resources(_ context.Context) []func() resource.Resource { return []func() resource.Resource{ - notification.NotificationResource, + account_features.AccountFeaturesResource, + global_connection.GlobalConnectionResource, group_partial_permissions.GroupPartialPermissionsResource, - partial_notification.PartialNotificationResource, - partial_license_map.PartialLicenseMapResource, group.GroupResource, - service_token.ServiceTokenResource, - global_connection.GlobalConnectionResource, - lineage_integration.LineageIntegrationResource, - oauth_configuration.OAuthConfigurationResource, - account_features.AccountFeaturesResource, ip_restrictions_rule.IPRestrictionsRuleResource, license_map.LicenseMapResource, + lineage_integration.LineageIntegrationResource, + notification.NotificationResource, + oauth_configuration.OAuthConfigurationResource, + partial_license_map.PartialLicenseMapResource, + partial_notification.PartialNotificationResource, + project_artefacts.ProjectArtefactsResource, + service_token.ServiceTokenResource, } } diff --git a/pkg/provider/sdk_provider.go b/pkg/provider/sdk_provider.go index 3d2fbfe..b7be42f 100644 --- a/pkg/provider/sdk_provider.go +++ b/pkg/provider/sdk_provider.go @@ -57,7 +57,6 @@ func SDKProvider(version string) func() *schema.Provider { "dbtcloud_project": resources.ResourceProject(), "dbtcloud_project_connection": resources.ResourceProjectConnection(), "dbtcloud_project_repository": resources.ResourceProjectRepository(), - "dbtcloud_project_artefacts": resources.ResourceProjectArtefacts(), "dbtcloud_environment": resources.ResourceEnvironment(), "dbtcloud_environment_variable": resources.ResourceEnvironmentVariable(), "dbtcloud_databricks_credential": resources.ResourceDatabricksCredential(),