diff --git a/CHANGELOG.md b/CHANGELOG.md index bb57750e..38efd5af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,19 @@ All notable changes to this project will be documented in this file. -## [Unreleased](https://github.com/dbt-labs/terraform-provider-dbtcloud/compare/v0.3.19...HEAD) +## [Unreleased](https://github.com/dbt-labs/terraform-provider-dbtcloud/compare/v0.3.20...HEAD) + +# [0.3.20](https://github.com/dbt-labs/terraform-provider-dbtcloud/compare/v0.3.19...v0.3.20) + +### Changes + +- [#305](https://github.com/dbt-labs/terraform-provider-dbtcloud/issues/305) - Add the resource `dbtcloud_lineage_integration` to setup auto-exposures in Tableau +- Add ability to provide a project description in `dbtcloud_project` +- Add ability to enable model query history in `dbtcloud_environment` + +### Fixes + +- [#309](https://github.com/dbt-labs/terraform-provider-dbtcloud/issues/309) - Fix the datasource `dbtcloud_global_connections` when PL is used in some connection # [0.3.19](https://github.com/dbt-labs/terraform-provider-dbtcloud/compare/v0.3.18...v0.3.19) diff --git a/docs/data-sources/environment.md b/docs/data-sources/environment.md index 587b918f..fa53b38c 100644 --- a/docs/data-sources/environment.md +++ b/docs/data-sources/environment.md @@ -27,6 +27,7 @@ Retrieve data for a single environment - `custom_branch` (String) The custom branch name to use - `dbt_version` (String) Version number of dbt to use in this environment. - `deployment_type` (String) The type of deployment environment (currently 'production', 'staging' or empty) +- `enable_model_query_history` (Boolean) Whether model query history is on - `extended_attributes_id` (Number) The ID of the extended attributes applied - `name` (String) The name of the environment - `type` (String) The type of environment (must be either development or deployment) diff --git a/docs/data-sources/environments.md b/docs/data-sources/environments.md index b06d22f2..8f7767ad 100644 --- a/docs/data-sources/environments.md +++ b/docs/data-sources/environments.md @@ -33,6 +33,7 @@ Read-Only: - `custom_branch` (String) The custom branch name to use - `dbt_version` (String) Version number of dbt to use in this environment. - `deployment_type` (String) The type of deployment environment (currently 'production', 'staging' or empty) +- `enable_model_query_history` (Boolean) Whether model query history is on - `environment_id` (Number) The ID of the environment - `extended_attributes_id` (Number) The ID of the extended attributes applied - `name` (String) The name of the environment diff --git a/docs/data-sources/global_connections.md b/docs/data-sources/global_connections.md index ca188210..99caa3a4 100644 --- a/docs/data-sources/global_connections.md +++ b/docs/data-sources/global_connections.md @@ -36,5 +36,5 @@ Read-Only: - `is_ssh_tunnel_enabled` (Boolean) - `name` (String) Connection name - `oauth_configuration_id` (Number) -- `private_link_endpoint_id` (Number) Private Link Endpoint ID. +- `private_link_endpoint_id` (String) Private Link Endpoint ID. - `updated_at` (String) When the connection was updated diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md index dec875d5..029801aa 100644 --- a/docs/data-sources/project.md +++ b/docs/data-sources/project.md @@ -33,6 +33,7 @@ data "dbtcloud_project" "test_project" { ### Optional +- `description` (String) The description of the project - `name` (String) Given name for project - `project_id` (Number) ID of the project to represent diff --git a/docs/resources/environment.md b/docs/resources/environment.md index 23f2dd21..0a38abbb 100644 --- a/docs/resources/environment.md +++ b/docs/resources/environment.md @@ -64,14 +64,15 @@ resource "dbtcloud_environment" "dev_environment" { ### Optional - `connection_id` (Number) The ID of the connection to use (can be the `id` of a `dbtcloud_global_connection` or the `connection_id` of a legacy connection). - - At the moment, it is optional and the environment will use the connection set in `dbtcloud_project_connection` if `connection_id` is not set in this resource - - In future versions this field will become required, so it is recommended to set it from now on - - When configuring this field, it needs to be configured for all the environments of the project - - To avoid Terraform state issues, when using this field, the `dbtcloud_project_connection` resource should be removed from the project or you need to make sure that the `connection_id` is the same in `dbtcloud_project_connection` and in the `connection_id` of the Development environment of the project +- At the moment, it is optional and the environment will use the connection set in `dbtcloud_project_connection` if `connection_id` is not set in this resource +- In future versions this field will become required, so it is recommended to set it from now on +- When configuring this field, it needs to be configured for all the environments of the project +- To avoid Terraform state issues, when using this field, the `dbtcloud_project_connection` resource should be removed from the project or you need to make sure that the `connection_id` is the same in `dbtcloud_project_connection` and in the `connection_id` of the Development environment of the project - `credential_id` (Number) Credential ID to create the environment with. A credential is not required for development environments but is required for deployment environments - `custom_branch` (String) Which custom branch to use in this environment - `dbt_version` (String) Version number of dbt to use in this environment. It needs to be in the format `major.minor.0-latest` (e.g. `1.5.0-latest`), `major.minor.0-pre` or `versionless`. Defaults to`versionless` if no version is provided - `deployment_type` (String) The type of environment. Only valid for environments of type 'deployment' and for now can only be 'production', 'staging' or left empty for generic environments +- `enable_model_query_history` (Boolean) Whether to enable model query history in this environment. As of Oct 2024, works only for Snowflake and BigQuery. - `extended_attributes_id` (Number) ID of the extended attributes for the environment - `is_active` (Boolean) Whether the environment is active - `use_custom_branch` (Boolean) Whether to use a custom git branch in this environment diff --git a/docs/resources/lineage_integration.md b/docs/resources/lineage_integration.md new file mode 100644 index 00000000..a122fb59 --- /dev/null +++ b/docs/resources/lineage_integration.md @@ -0,0 +1,69 @@ +--- +page_title: "dbtcloud_lineage_integration Resource - dbtcloud" +subcategory: "" +description: |- + Setup lineage integration for dbt Cloud to automatically fetch lineage from external BI tools in dbt Explorer. Currently supports Tableau. + This resource requires having an environment tagged as production already created for you project. +--- + +# dbtcloud_lineage_integration (Resource) + + +Setup lineage integration for dbt Cloud to automatically fetch lineage from external BI tools in dbt Explorer. Currently supports Tableau. + +This resource requires having an environment tagged as production already created for you project. + +## Example Usage + +```terraform +// the resource can only be configured when a Prod environment has been set +// so, you might want to explicitly set the dependency on your Prod environment resource + +resource "dbtcloud_lineage_integration" "my_lineage" { + project_id = dbtcloud_project.my_project.id + host = "my.host.com" + site_id = "mysiteid" + token_name = "my-token-name" + token = "my-sensitive-token" + + depends_on = [dbtcloud_environment.my_prod_env] +} +``` + + +## Schema + +### Required + +- `host` (String) The URL of the BI server (see docs for more details) +- `project_id` (Number) The dbt Cloud project ID for the integration +- `site_id` (String) The sitename for the collections of dashboards (see docs for more details) +- `token` (String, Sensitive) The secret token value to use to authenticate to the BI server +- `token_name` (String) The token to use to authenticate to the BI server + +### Read-Only + +- `id` (String) Combination of `project_id` and `lineage_integration_id` +- `lineage_integration_id` (Number) The ID of the lineage integration +- `name` (String) The integration type. Today only 'tableau' is supported + +## Import + +Import is supported using the following syntax: + +```shell +# using import blocks (requires Terraform >= 1.5) +import { + to = dbtcloud_lineage_integration.my_lineage_integration + id = "projet_id:lineage_integration_id" +} + +import { + to = dbtcloud_lineage_integration.my_lineage_integration + id = "123:4567" +} + +# using the older import command +terraform import dbtcloud_lineage_integration.my_lineage_integration "projet_id:lineage_integration_id" +terraform import dbtcloud_lineage_integration.my_lineage_integration 123:4567 +``` diff --git a/docs/resources/project.md b/docs/resources/project.md index a13b8d7b..d8ee77ad 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -16,6 +16,10 @@ description: |- resource "dbtcloud_project" "dbt_project" { name = "Analytics" } +resource "dbtcloud_project" "dbt_project_with_description" { + name = "Analytics with description" + description = "My awesome analytics project" +} resource "dbtcloud_project" "dbt_project_with_subdir" { name = "Analytics in Subdir" @@ -33,6 +37,7 @@ resource "dbtcloud_project" "dbt_project_with_subdir" { ### Optional - `dbt_project_subdirectory` (String) dbt project subdirectory path +- `description` (String) Description for the project. Will show in dbt Explorer. ### Read-Only diff --git a/docs/resources/project_connection.md b/docs/resources/project_connection.md index fc149b60..22041822 100644 --- a/docs/resources/project_connection.md +++ b/docs/resources/project_connection.md @@ -2,13 +2,13 @@ page_title: "dbtcloud_project_connection Resource - dbtcloud" subcategory: "" description: |- - + ~> This resource is deprecated with the release of global connections and it will be removed in a future version of the provider. Going forward, please set the connection_id in the dbtcloud_environment resource instead. --- # dbtcloud_project_connection (Resource) - +~> This resource is deprecated with the release of global connections and it will be removed in a future version of the provider. Going forward, please set the `connection_id` in the `dbtcloud_environment` resource instead. ## Example Usage diff --git a/docs/resources/snowflake_credential.md b/docs/resources/snowflake_credential.md index ee5efe47..0bb7123d 100644 --- a/docs/resources/snowflake_credential.md +++ b/docs/resources/snowflake_credential.md @@ -14,7 +14,7 @@ description: |- ```terraform resource "dbtcloud_snowflake_credential" "prod_credential" { - project_id = data.dbtcloud_project.dbt_project.id + project_id = dbtcloud_project.dbt_project.id auth_type = "password" num_threads = 16 schema = "SCHEMA" diff --git a/examples/resources/dbtcloud_lineage_integration/import.sh b/examples/resources/dbtcloud_lineage_integration/import.sh new file mode 100644 index 00000000..3643c3fc --- /dev/null +++ b/examples/resources/dbtcloud_lineage_integration/import.sh @@ -0,0 +1,14 @@ +# using import blocks (requires Terraform >= 1.5) +import { + to = dbtcloud_lineage_integration.my_lineage_integration + id = "projet_id:lineage_integration_id" +} + +import { + to = dbtcloud_lineage_integration.my_lineage_integration + id = "123:4567" +} + +# using the older import command +terraform import dbtcloud_lineage_integration.my_lineage_integration "projet_id:lineage_integration_id" +terraform import dbtcloud_lineage_integration.my_lineage_integration 123:4567 diff --git a/examples/resources/dbtcloud_lineage_integration/resource.tf b/examples/resources/dbtcloud_lineage_integration/resource.tf new file mode 100644 index 00000000..d4b89b10 --- /dev/null +++ b/examples/resources/dbtcloud_lineage_integration/resource.tf @@ -0,0 +1,12 @@ +// the resource can only be configured when a Prod environment has been set +// so, you might want to explicitly set the dependency on your Prod environment resource + +resource "dbtcloud_lineage_integration" "my_lineage" { + project_id = dbtcloud_project.my_project.id + host = "my.host.com" + site_id = "mysiteid" + token_name = "my-token-name" + token = "my-sensitive-token" + + depends_on = [dbtcloud_environment.my_prod_env] +} diff --git a/examples/resources/dbtcloud_project/resource.tf b/examples/resources/dbtcloud_project/resource.tf index 822ce328..14bc9ad8 100644 --- a/examples/resources/dbtcloud_project/resource.tf +++ b/examples/resources/dbtcloud_project/resource.tf @@ -1,6 +1,10 @@ resource "dbtcloud_project" "dbt_project" { name = "Analytics" } +resource "dbtcloud_project" "dbt_project_with_description" { + name = "Analytics with description" + description = "My awesome analytics project" +} resource "dbtcloud_project" "dbt_project_with_subdir" { name = "Analytics in Subdir" diff --git a/examples/resources/dbtcloud_snowflake_credential/resource.tf b/examples/resources/dbtcloud_snowflake_credential/resource.tf index 0cf44291..2c24ab3e 100644 --- a/examples/resources/dbtcloud_snowflake_credential/resource.tf +++ b/examples/resources/dbtcloud_snowflake_credential/resource.tf @@ -1,5 +1,5 @@ resource "dbtcloud_snowflake_credential" "prod_credential" { - project_id = data.dbtcloud_project.dbt_project.id + project_id = dbtcloud_project.dbt_project.id auth_type = "password" num_threads = 16 schema = "SCHEMA" diff --git a/pkg/dbt_cloud/environment.go b/pkg/dbt_cloud/environment.go index 4d039392..95398dbb 100644 --- a/pkg/dbt_cloud/environment.go +++ b/pkg/dbt_cloud/environment.go @@ -34,6 +34,7 @@ type Environment struct { DeploymentType *string `json:"deployment_type,omitempty"` ExtendedAttributesID *int `json:"extended_attributes_id,omitempty"` ConnectionID *int `json:"connection_id,omitempty"` + EnableModelQueryHistory bool `json:"enable_model_query_history,omitempty"` } func (c *Client) GetEnvironment(projectId int, environmentId int) (*Environment, error) { @@ -79,6 +80,7 @@ func (c *Client) CreateEnvironment( deploymentType string, extendedAttributesID int, connectionID int, + enableModelQueryHistory bool, ) (*Environment, error) { state := STATE_ACTIVE if !isActive { @@ -86,13 +88,14 @@ func (c *Client) CreateEnvironment( } newEnvironment := Environment{ - State: state, - Account_Id: c.AccountID, - Project_Id: projectId, - Name: name, - Dbt_Version: dbtVersion, - Type: type_, - Use_Custom_Branch: useCustomBranch, + State: state, + Account_Id: c.AccountID, + Project_Id: projectId, + Name: name, + Dbt_Version: dbtVersion, + Type: type_, + Use_Custom_Branch: useCustomBranch, + EnableModelQueryHistory: enableModelQueryHistory, } if credentialId != 0 { newEnvironment.Credential_Id = &credentialId diff --git a/pkg/dbt_cloud/global_connections.go b/pkg/dbt_cloud/global_connections.go index 6ab982a7..7ba84bb4 100644 --- a/pkg/dbt_cloud/global_connections.go +++ b/pkg/dbt_cloud/global_connections.go @@ -6,16 +6,16 @@ import ( ) type GlobalConnectionSummary struct { - ID int64 `json:"id"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - AccountID int64 `json:"account_id"` - Name string `json:"name"` - AdapterVersion string `json:"adapter_version"` - PrivateLinkEndpointID *int64 `json:"private_link_endpoint_id"` - IsSSHTunnelEnabled bool `json:"is_ssh_tunnel_enabled"` - OauthConfigurationID *int64 `json:"oauth_configuration_id"` - EnvironmentCount int64 `json:"environment__count"` + ID int64 `json:"id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + AccountID int64 `json:"account_id"` + Name string `json:"name"` + AdapterVersion string `json:"adapter_version"` + PrivateLinkEndpointID *string `json:"private_link_endpoint_id"` + IsSSHTunnelEnabled bool `json:"is_ssh_tunnel_enabled"` + OauthConfigurationID *int64 `json:"oauth_configuration_id"` + EnvironmentCount int64 `json:"environment__count"` } func (c *Client) GetAllConnections() ([]GlobalConnectionSummary, error) { diff --git a/pkg/dbt_cloud/lineage.go b/pkg/dbt_cloud/lineage.go new file mode 100644 index 00000000..55ff02dc --- /dev/null +++ b/pkg/dbt_cloud/lineage.go @@ -0,0 +1,176 @@ +package dbt_cloud + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" +) + +type LineageIntegrationConfig struct { + Host string `json:"host,omitempty"` + SiteID string `json:"site_id,omitempty"` + TokenName string `json:"token_name,omitempty"` + Token string `json:"token,omitempty"` +} + +type LineageIntegration struct { + ID *int64 `json:"id,omitempty"` + AccountID int64 `json:"account_id,omitempty"` + ProjectID int64 `json:"project_id,omitempty"` + Name string `json:"name,omitempty"` + Config LineageIntegrationConfig `json:"config"` +} + +type LineageIntegrationResponse struct { + Data LineageIntegration `json:"data"` + Status ResponseStatus `json:"status"` +} + +func (c *Client) GetLineageIntegration( + projectID int64, + lineageIntegrationID int64, +) (*LineageIntegration, error) { + req, err := http.NewRequest( + "GET", + fmt.Sprintf( + "%s/v3/accounts/%d/projects/%d/integrations/lineage/%d/", + c.HostURL, + c.AccountID, + projectID, + lineageIntegrationID, + ), + nil, + ) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + lineageIntegrationResponse := LineageIntegrationResponse{} + err = json.Unmarshal(body, &lineageIntegrationResponse) + if err != nil { + return nil, err + } + + return &lineageIntegrationResponse.Data, nil +} + +func (c *Client) CreateLineageIntegration( + projectID int64, + name string, + host string, + siteID string, + tokenName string, + token string, +) (*LineageIntegration, error) { + newLineageIntegration := LineageIntegration{ + AccountID: int64(c.AccountID), + ProjectID: projectID, + Name: name, + Config: LineageIntegrationConfig{ + Host: host, + SiteID: siteID, + TokenName: tokenName, + Token: token, + }, + } + newLineageIntegrationData, err := json.Marshal(newLineageIntegration) + if err != nil { + return nil, err + } + + req, err := http.NewRequest( + "POST", + fmt.Sprintf( + "%s/v3/accounts/%d/projects/%d/integrations/lineage/", + c.HostURL, + c.AccountID, + projectID, + ), + strings.NewReader(string(newLineageIntegrationData)), + ) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + lineageIntegrationResponse := LineageIntegrationResponse{} + err = json.Unmarshal(body, &lineageIntegrationResponse) + if err != nil { + return nil, err + } + + return &lineageIntegrationResponse.Data, nil +} + +func (c *Client) UpdateLineageIntegration( + projectID int64, + lineageIntegrationID int64, + lineageIntegration LineageIntegration, +) (*LineageIntegration, error) { + lineageIntegrationData, err := json.Marshal(lineageIntegration) + if err != nil { + return nil, err + } + + req, err := http.NewRequest( + "PATCH", + fmt.Sprintf( + "%s/v3/accounts/%d/projects/%d/integrations/lineage/%d/", + c.HostURL, + c.AccountID, + projectID, + lineageIntegrationID, + ), + strings.NewReader(string(lineageIntegrationData)), + ) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + lineageIntegrationResponse := LineageIntegrationResponse{} + err = json.Unmarshal(body, &lineageIntegrationResponse) + if err != nil { + return nil, err + } + + return &lineageIntegrationResponse.Data, nil +} + +func (c *Client) DeleteLineageIntegration(projectID int64, lineageIntegrationID int64) error { + req, err := http.NewRequest( + "DELETE", + fmt.Sprintf( + "%s/v3/accounts/%d/projects/%d/integrations/lineage/%d/", + c.HostURL, + c.AccountID, + projectID, + lineageIntegrationID, + ), + nil, + ) + if err != nil { + return err + } + + _, err = c.doRequest(req) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/dbt_cloud/project.go b/pkg/dbt_cloud/project.go index 4d47e946..da6b7a0c 100644 --- a/pkg/dbt_cloud/project.go +++ b/pkg/dbt_cloud/project.go @@ -11,9 +11,10 @@ import ( type Project struct { ID *int `json:"id,omitempty"` Name string `json:"name"` + Description string `json:"description"` DbtProjectSubdirectory *string `json:"dbt_project_subdirectory,omitempty"` - ConnectionID *int `json:"connection_id,integer,omitempty"` - RepositoryID *int `json:"repository_id,integer,omitempty"` + ConnectionID *int `json:"connection_id,omitempty"` + RepositoryID *int `json:"repository_id,omitempty"` State int `json:"state"` AccountID int `json:"account_id"` FreshnessJobId *int `json:"freshness_job_id"` @@ -147,11 +148,16 @@ func (c *Client) GetProject(projectID string) (*Project, error) { return &projectResponse.Data, nil } -func (c *Client) CreateProject(name string, dbtProjectSubdirectory string) (*Project, error) { +func (c *Client) CreateProject( + name string, + description string, + dbtProjectSubdirectory string, +) (*Project, error) { newProject := Project{ - Name: name, - State: STATE_ACTIVE, - AccountID: c.AccountID, + Name: name, + Description: description, + State: STATE_ACTIVE, + AccountID: c.AccountID, } if dbtProjectSubdirectory != "" { newProject.DbtProjectSubdirectory = &dbtProjectSubdirectory diff --git a/pkg/framework/objects/environment/data_source.go b/pkg/framework/objects/environment/data_source.go index 3320bb09..233b8d42 100644 --- a/pkg/framework/objects/environment/data_source.go +++ b/pkg/framework/objects/environment/data_source.go @@ -66,6 +66,7 @@ func (d *environmentDataSource) Read( state.ExtendedAttributesID = types.Int64PointerValue( helper.IntPointerToInt64Pointer(environment.ExtendedAttributesID), ) + state.EnableModelQueryHistory = types.BoolValue(environment.EnableModelQueryHistory) diags := resp.State.Set(ctx, &state) resp.Diagnostics.Append(diags...) diff --git a/pkg/framework/objects/environment/data_source_acceptance_test.go b/pkg/framework/objects/environment/data_source_acceptance_test.go index f47dd71d..aa1a78f4 100644 --- a/pkg/framework/objects/environment/data_source_acceptance_test.go +++ b/pkg/framework/objects/environment/data_source_acceptance_test.go @@ -32,6 +32,11 @@ func TestAccDbtCloudEnvironmentDataSource(t *testing.T) { "custom_branch", "customBranchName", ), + resource.TestCheckResourceAttr( + "data.dbtcloud_environment.test", + "enable_model_query_history", + "true", + ), ) resource.ParallelTest(t, resource.TestCase{ @@ -58,6 +63,7 @@ func environment(projectName, environmentName string) string { type = "development" use_custom_branch = true custom_branch = "customBranchName" + enable_model_query_history = true } data "dbtcloud_environment" "test" { diff --git a/pkg/framework/objects/environment/data_source_all.go b/pkg/framework/objects/environment/data_source_all.go index 76f44c53..dbaae81e 100644 --- a/pkg/framework/objects/environment/data_source_all.go +++ b/pkg/framework/objects/environment/data_source_all.go @@ -79,6 +79,7 @@ func (d *environmentsDataSources) Read( currentEnv.ExtendedAttributesID = types.Int64PointerValue( helper.IntPointerToInt64Pointer(environment.ExtendedAttributesID), ) + currentEnv.EnableModelQueryHistory = types.BoolValue(environment.EnableModelQueryHistory) allEnvs = append(allEnvs, currentEnv) } state.Environments = allEnvs diff --git a/pkg/framework/objects/environment/data_source_all_acceptance_test.go b/pkg/framework/objects/environment/data_source_all_acceptance_test.go index 40405541..e9f9f6cc 100644 --- a/pkg/framework/objects/environment/data_source_all_acceptance_test.go +++ b/pkg/framework/objects/environment/data_source_all_acceptance_test.go @@ -76,6 +76,7 @@ func environments( type = "deployment" use_custom_branch = true custom_branch = "customBranchName" + enable_model_query_history = true } resource "dbtcloud_environment" "test_environment2" { diff --git a/pkg/framework/objects/environment/model.go b/pkg/framework/objects/environment/model.go index 3af1e2bd..069eb782 100644 --- a/pkg/framework/objects/environment/model.go +++ b/pkg/framework/objects/environment/model.go @@ -3,17 +3,18 @@ package environment import "github.com/hashicorp/terraform-plugin-framework/types" type EnvironmentDataSourceModel struct { - EnvironmentID types.Int64 `tfsdk:"environment_id"` - ProjectID types.Int64 `tfsdk:"project_id"` - CredentialsID types.Int64 `tfsdk:"credentials_id"` - Name types.String `tfsdk:"name"` - DbtVersion types.String `tfsdk:"dbt_version"` - Type types.String `tfsdk:"type"` - UseCustomBranch types.Bool `tfsdk:"use_custom_branch"` - CustomBranch types.String `tfsdk:"custom_branch"` - DeploymentType types.String `tfsdk:"deployment_type"` - ExtendedAttributesID types.Int64 `tfsdk:"extended_attributes_id"` - ConnectionID types.Int64 `tfsdk:"connection_id"` + EnvironmentID types.Int64 `tfsdk:"environment_id"` + ProjectID types.Int64 `tfsdk:"project_id"` + CredentialsID types.Int64 `tfsdk:"credentials_id"` + Name types.String `tfsdk:"name"` + DbtVersion types.String `tfsdk:"dbt_version"` + Type types.String `tfsdk:"type"` + UseCustomBranch types.Bool `tfsdk:"use_custom_branch"` + CustomBranch types.String `tfsdk:"custom_branch"` + DeploymentType types.String `tfsdk:"deployment_type"` + ExtendedAttributesID types.Int64 `tfsdk:"extended_attributes_id"` + ConnectionID types.Int64 `tfsdk:"connection_id"` + EnableModelQueryHistory types.Bool `tfsdk:"enable_model_query_history"` } type EnvironmentsDataSourceModel struct { diff --git a/pkg/framework/objects/environment/schema.go b/pkg/framework/objects/environment/schema.go index ca65fdaa..e5f450d9 100644 --- a/pkg/framework/objects/environment/schema.go +++ b/pkg/framework/objects/environment/schema.go @@ -59,6 +59,10 @@ func (r *environmentDataSource) Schema( Computed: true, Description: "A connection ID (used with Global Connections)", }, + "enable_model_query_history": schema.BoolAttribute{ + Computed: true, + Description: "Whether model query history is on", + }, }, } } @@ -124,6 +128,10 @@ func (r *environmentsDataSources) Schema( Computed: true, Description: "A connection ID (used with Global Connections)", }, + "enable_model_query_history": schema.BoolAttribute{ + Computed: true, + Description: "Whether model query history is on", + }, }, }, }, diff --git a/pkg/framework/objects/global_connection/data_source_all.go b/pkg/framework/objects/global_connection/data_source_all.go index 36c9497f..f261d369 100644 --- a/pkg/framework/objects/global_connection/data_source_all.go +++ b/pkg/framework/objects/global_connection/data_source_all.go @@ -56,7 +56,7 @@ func (d *globalConnectionsDataSource) Read( currentConnection.CreatedAt = types.StringValue(connection.CreatedAt) currentConnection.UpdatedAt = types.StringValue(connection.UpdatedAt) currentConnection.AdapterVersion = types.StringValue(connection.AdapterVersion) - currentConnection.PrivateLinkEndpointID = types.Int64PointerValue( + currentConnection.PrivateLinkEndpointID = types.StringPointerValue( connection.PrivateLinkEndpointID, ) currentConnection.IsSSHTunnelEnabled = types.BoolValue(connection.IsSSHTunnelEnabled) diff --git a/pkg/framework/objects/global_connection/model.go b/pkg/framework/objects/global_connection/model.go index a3295ef4..c4e310f0 100644 --- a/pkg/framework/objects/global_connection/model.go +++ b/pkg/framework/objects/global_connection/model.go @@ -270,7 +270,7 @@ type GlobalConnectionSummary struct { CreatedAt types.String `tfsdk:"created_at"` UpdatedAt types.String `tfsdk:"updated_at"` AdapterVersion types.String `tfsdk:"adapter_version"` - PrivateLinkEndpointID types.Int64 `tfsdk:"private_link_endpoint_id"` + PrivateLinkEndpointID types.String `tfsdk:"private_link_endpoint_id"` IsSSHTunnelEnabled types.Bool `tfsdk:"is_ssh_tunnel_enabled"` OauthConfigurationID types.Int64 `tfsdk:"oauth_configuration_id"` EnvironmentCount types.Int64 `tfsdk:"environment__count"` diff --git a/pkg/framework/objects/global_connection/schema.go b/pkg/framework/objects/global_connection/schema.go index 5941f3ce..f5c2139a 100644 --- a/pkg/framework/objects/global_connection/schema.go +++ b/pkg/framework/objects/global_connection/schema.go @@ -1068,7 +1068,7 @@ func (r *globalConnectionsDataSource) Schema( Computed: true, Description: "Type of adapter used for the connection", }, - "private_link_endpoint_id": datasource_schema.Int64Attribute{ + "private_link_endpoint_id": datasource_schema.StringAttribute{ Computed: true, Description: "Private Link Endpoint ID.", }, diff --git a/pkg/framework/objects/lineage_integration/model.go b/pkg/framework/objects/lineage_integration/model.go new file mode 100644 index 00000000..afee429b --- /dev/null +++ b/pkg/framework/objects/lineage_integration/model.go @@ -0,0 +1,14 @@ +package lineage_integration + +import "github.com/hashicorp/terraform-plugin-framework/types" + +type LineageIntegrationResourceModel struct { + ID types.String `tfsdk:"id"` + LineageIntegrationID types.Int64 `tfsdk:"lineage_integration_id"` + ProjectID types.Int64 `tfsdk:"project_id"` + Name types.String `tfsdk:"name"` + Host types.String `tfsdk:"host"` + SiteID types.String `tfsdk:"site_id"` + TokenName types.String `tfsdk:"token_name"` + Token types.String `tfsdk:"token"` +} diff --git a/pkg/framework/objects/lineage_integration/resource.go b/pkg/framework/objects/lineage_integration/resource.go new file mode 100644 index 00000000..4bb451da --- /dev/null +++ b/pkg/framework/objects/lineage_integration/resource.go @@ -0,0 +1,226 @@ +package lineage_integration + +import ( + "context" + "fmt" + "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 = &lineageIntegrationResource{} + _ resource.ResourceWithConfigure = &lineageIntegrationResource{} + _ resource.ResourceWithImportState = &lineageIntegrationResource{} +) + +func LineageIntegrationResource() resource.Resource { + return &lineageIntegrationResource{} +} + +type lineageIntegrationResource struct { + client *dbt_cloud.Client +} + +func (r *lineageIntegrationResource) Metadata( + _ context.Context, + req resource.MetadataRequest, + resp *resource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_lineage_integration" +} + +func (r *lineageIntegrationResource) Read( + ctx context.Context, + req resource.ReadRequest, + resp *resource.ReadResponse, +) { + var data LineageIntegrationResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + projectID := data.ProjectID.ValueInt64() + lineageIntegrationID := data.LineageIntegrationID.ValueInt64() + lineageIntegration, err := r.client.GetLineageIntegration(projectID, lineageIntegrationID) + if err != nil { + if strings.HasPrefix(err.Error(), "resource-not-found") { + resp.Diagnostics.AddWarning( + "Resource not found", + "The lineage_integration resource was not found and has been removed from the state.", + ) + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Error getting the lineage", err.Error()) + return + } + + data.ID = types.StringValue( + fmt.Sprintf( + "%d%s%d", + lineageIntegration.ProjectID, + dbt_cloud.ID_DELIMITER, + *lineageIntegration.ID, + ), + ) + + data.LineageIntegrationID = types.Int64PointerValue(lineageIntegration.ID) + data.ProjectID = types.Int64Value(int64(lineageIntegration.ProjectID)) + data.Name = types.StringValue(lineageIntegration.Name) + data.Host = types.StringValue(lineageIntegration.Config.Host) + data.SiteID = types.StringValue(lineageIntegration.Config.SiteID) + data.TokenName = types.StringValue(lineageIntegration.Config.TokenName) + + // we only set the token if it is null as it is sensitive + // this means that we are importing the data + if data.Token.IsNull() { + data.Token = types.StringValue("********") + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + +} + +func (r *lineageIntegrationResource) Create( + ctx context.Context, + req resource.CreateRequest, + resp *resource.CreateResponse, +) { + var data LineageIntegrationResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + lineageIntegration, err := r.client.CreateLineageIntegration( + data.ProjectID.ValueInt64(), + data.Name.ValueString(), + data.Host.ValueString(), + data.SiteID.ValueString(), + data.TokenName.ValueString(), + data.Token.ValueString(), + ) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to create lineage integration", + "Error: "+err.Error(), + ) + return + } + + data.ID = types.StringValue( + fmt.Sprintf( + "%d%s%d", + lineageIntegration.ProjectID, + dbt_cloud.ID_DELIMITER, + *lineageIntegration.ID, + ), + ) + data.LineageIntegrationID = types.Int64PointerValue(lineageIntegration.ID) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *lineageIntegrationResource) Delete( + ctx context.Context, + req resource.DeleteRequest, + resp *resource.DeleteResponse, +) { + var data LineageIntegrationResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + lineageID := data.LineageIntegrationID.ValueInt64() + projectID := data.ProjectID.ValueInt64() + + err := r.client.DeleteLineageIntegration(projectID, lineageID) + if err != nil { + resp.Diagnostics.AddError("Error deleting lineage", err.Error()) + return + } +} + +func (r *lineageIntegrationResource) Update( + ctx context.Context, + req resource.UpdateRequest, + resp *resource.UpdateResponse, +) { + var plan, state LineageIntegrationResourceModel + + // Read plan and state values into the models + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + patchPayload := dbt_cloud.LineageIntegration{} + + if plan.Host != state.Host { + patchPayload.Config.Host = plan.Host.ValueString() + } + if plan.SiteID != state.SiteID { + patchPayload.Config.SiteID = plan.SiteID.ValueString() + } + if plan.TokenName != state.TokenName { + patchPayload.Config.TokenName = plan.TokenName.ValueString() + } + if plan.Token != state.Token { + patchPayload.Config.Token = plan.Token.ValueString() + } + + projectID := state.ProjectID.ValueInt64() + lineageID := state.LineageIntegrationID.ValueInt64() + + // Update the lineage + _, err := r.client.UpdateLineageIntegration(projectID, lineageID, patchPayload) + if err != nil { + resp.Diagnostics.AddError("Error updating lineage", err.Error()) + return + } + + // Set the updated state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *lineageIntegrationResource) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { + + projectID, lineageID, err := helper.SplitIDToInts(req.ID, "lineage") + if err != nil { + resp.Diagnostics.AddError("Error splitting the ID", err.Error()) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute( + ctx, path.Root("lineage_integration_id"), lineageID, + )...) + resp.Diagnostics.Append(resp.State.SetAttribute( + ctx, path.Root("project_id"), projectID, + )...) +} + +func (r *lineageIntegrationResource) Configure( + _ context.Context, + req resource.ConfigureRequest, + _ *resource.ConfigureResponse, +) { + if req.ProviderData == nil { + return + } + + r.client = req.ProviderData.(*dbt_cloud.Client) +} diff --git a/pkg/framework/objects/lineage_integration/resource_acceptance_test.go b/pkg/framework/objects/lineage_integration/resource_acceptance_test.go new file mode 100644 index 00000000..28b3482b --- /dev/null +++ b/pkg/framework/objects/lineage_integration/resource_acceptance_test.go @@ -0,0 +1,150 @@ +package lineage_integration_test + +import ( + "fmt" + "os" + "regexp" + "strings" + "testing" + + "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/framework/acctest_helper" + "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/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 TestAccDbtCloudLineageIntegrationResource(t *testing.T) { + + envVarLineageIntegration, exists := os.LookupEnv("DBT_ACCEPTANCE_TEST_LINEAGE_INTEGRATION") + + if !exists { + t.Skip( + "Skipping lineage configuration acceptance tests as the env var DBT_ACCEPTANCE_TEST_LINEAGE_INTEGRATION is not set", + ) + } + + lineageIntegrationConfigs := strings.Split(envVarLineageIntegration, "~") + if len(lineageIntegrationConfigs) != 4 { + t.Fatalf( + "DBT_ACCEPTANCE_TEST_LINEAGE_INTEGRATION env var should be in the format: host~side_id~token_name~token", + ) + } + + lineageIntegrationHost := lineageIntegrationConfigs[0] + lineageIntegrationSiteID := lineageIntegrationConfigs[1] + lineageIntegrationTokenName := lineageIntegrationConfigs[2] + lineageIntegrationToken := lineageIntegrationConfigs[3] + + projectName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest_helper.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest_helper.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDbtCloudLineageIntegrationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDbtCloudLineageIntegrationResourceBasicConfig( + projectName, + lineageIntegrationHost, + lineageIntegrationSiteID, + lineageIntegrationTokenName, + lineageIntegrationToken, + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "dbtcloud_lineage_integration.my_lineage", + "id", + ), + ), + }, + // MODIFY + // IMPORT + { + ResourceName: "dbtcloud_lineage_integration.my_lineage", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"token"}, + }, + }, + }) +} + +func testAccDbtCloudLineageIntegrationResourceBasicConfig( + projectName, host, siteID, tokenName, token string, +) string { + return fmt.Sprintf(` +resource "dbtcloud_project" "test_lineage_integration" { + name = "%s" +} + +resource "dbtcloud_snowflake_credential" "my_cred" { + project_id = dbtcloud_project.test_lineage_integration.id + auth_type = "password" + num_threads = 16 + schema = "SCHEMA" + user = "user" + password = "password" +} + +resource "dbtcloud_global_connection" "my_connection" { +name = "terraform_snowflake_testing_proj_qa" + snowflake = { + account = "NA" + database = "DB" + warehouse = "WH" + } +} + +resource dbtcloud_environment my_env { + dbt_version = "versionless" + name = "Prod" + project_id = dbtcloud_project.test_lineage_integration.id + type = "deployment" + credential_id = dbtcloud_snowflake_credential.my_cred.credential_id + deployment_type = "production" + connection_id = dbtcloud_global_connection.my_connection.id +} + +resource dbtcloud_lineage_integration my_lineage { + project_id = dbtcloud_project.test_lineage_integration.id + host = "%s" + site_id = "%s" + token_name = "%s" + token = "%s" + + depends_on = [dbtcloud_environment.my_env] +} +`, projectName, host, siteID, tokenName, token) +} + +func testAccCheckDbtCloudLineageIntegrationDestroy(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_lineage_integration" { + continue + } + + projectID, lineageID, err := helper.SplitIDToInts(rs.Primary.ID, "lineage_integration") + if err != nil { + return fmt.Errorf("Error splitting ID: %s", err) + } + + _, err = apiClient.GetLineageIntegration(int64(projectID), int64(lineageID)) + if err == nil { + return fmt.Errorf("Lineage integration 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/lineage_integration/schema.go b/pkg/framework/objects/lineage_integration/schema.go new file mode 100644 index 00000000..a0816a6e --- /dev/null +++ b/pkg/framework/objects/lineage_integration/schema.go @@ -0,0 +1,75 @@ +package lineage_integration + +import ( + "context" + + "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/helper" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "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/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" +) + +func (r *lineageIntegrationResource) Schema( + _ context.Context, + _ resource.SchemaRequest, + resp *resource.SchemaResponse, +) { + resp.Schema = schema.Schema{ + Description: helper.DocString(` + Setup lineage integration for dbt Cloud to automatically fetch lineage from external BI tools in dbt Explorer. Currently supports Tableau. + + This resource requires having an environment tagged as production already created for you project. + `), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "Combination of `project_id` and `lineage_integration_id`", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "lineage_integration_id": schema.Int64Attribute{ + Computed: true, + Description: "The ID of the lineage integration", + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + }, + "project_id": schema.Int64Attribute{ + Required: true, + Description: "The dbt Cloud project ID for the integration", + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + Computed: true, + Description: "The integration type. Today only 'tableau' is supported", + Default: stringdefault.StaticString("tableau"), + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "host": schema.StringAttribute{ + Required: true, + Description: "The URL of the BI server (see docs for more details)", + }, + "site_id": schema.StringAttribute{ + Required: true, + Description: "The sitename for the collections of dashboards (see docs for more details)", + }, + "token_name": schema.StringAttribute{ + Required: true, + Description: "The token to use to authenticate to the BI server", + }, + "token": schema.StringAttribute{ + Required: true, + Sensitive: true, + Description: "The secret token value to use to authenticate to the BI server", + }, + }, + } +} diff --git a/pkg/provider/framework_provider.go b/pkg/provider/framework_provider.go index 98e04b22..81eb6718 100644 --- a/pkg/provider/framework_provider.go +++ b/pkg/provider/framework_provider.go @@ -11,6 +11,7 @@ import ( "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/framework/objects/group" "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/framework/objects/group_partial_permissions" "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/framework/objects/job" + "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/framework/objects/lineage_integration" "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/framework/objects/notification" "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/framework/objects/partial_license_map" "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/framework/objects/partial_notification" @@ -200,5 +201,6 @@ func (p *dbtCloudProvider) Resources(_ context.Context) []func() resource.Resour group.GroupResource, service_token.ServiceTokenResource, global_connection.GlobalConnectionResource, + lineage_integration.LineageIntegrationResource, } } diff --git a/pkg/sdkv2/data_sources/project.go b/pkg/sdkv2/data_sources/project.go index b31bc59d..580bcddb 100644 --- a/pkg/sdkv2/data_sources/project.go +++ b/pkg/sdkv2/data_sources/project.go @@ -22,6 +22,12 @@ var projectSchema = map[string]*schema.Schema{ Optional: true, Description: "Given name for project", }, + "description": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: "The description of the project", + }, "connection_id": { Type: schema.TypeInt, Computed: true, @@ -57,7 +63,11 @@ func DatasourceProject() *schema.Resource { } } -func datasourceProjectRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +func datasourceProjectRead( + ctx context.Context, + d *schema.ResourceData, + m interface{}, +) diag.Diagnostics { c := m.(*dbt_cloud.Client) var diags diag.Diagnostics @@ -67,7 +77,9 @@ func datasourceProjectRead(ctx context.Context, d *schema.ResourceData, m interf projectId := strconv.Itoa(d.Get("project_id").(int)) if _, ok := d.GetOk("name"); ok { - return diag.FromErr(fmt.Errorf("Both project_id and name were provided, only one is allowed")) + return diag.FromErr( + fmt.Errorf("both project_id and name were provided, only one is allowed"), + ) } var err error @@ -86,7 +98,7 @@ func datasourceProjectRead(ctx context.Context, d *schema.ResourceData, m interf } } else { - return diag.FromErr(fmt.Errorf("Either project_id or name must be provided")) + return diag.FromErr(fmt.Errorf("either project_id or name must be provided")) } if err := d.Set("project_id", project.ID); err != nil { @@ -95,6 +107,9 @@ func datasourceProjectRead(ctx context.Context, d *schema.ResourceData, m interf if err := d.Set("name", project.Name); err != nil { return diag.FromErr(err) } + if err := d.Set("description", project.Description); err != nil { + return diag.FromErr(err) + } if err := d.Set("connection_id", project.ConnectionID); err != nil { return diag.FromErr(err) } diff --git a/pkg/sdkv2/resources/environment.go b/pkg/sdkv2/resources/environment.go index 007174ae..5fb688e8 100644 --- a/pkg/sdkv2/resources/environment.go +++ b/pkg/sdkv2/resources/environment.go @@ -119,6 +119,12 @@ func ResourceEnvironment() *schema.Resource { - To avoid Terraform state issues, when using this field, the ~~~dbtcloud_project_connection~~~ resource should be removed from the project or you need to make sure that the ~~~connection_id~~~ is the same in ~~~dbtcloud_project_connection~~~ and in the ~~~connection_id~~~ of the Development environment of the project`, ), }, + "enable_model_query_history": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether to enable model query history in this environment. As of Oct 2024, works only for Snowflake and BigQuery.", + }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, @@ -146,6 +152,7 @@ func resourceEnvironmentCreate( deploymentType := d.Get("deployment_type").(string) extendedAttributesID := d.Get("extended_attributes_id").(int) connectionID := d.Get("connection_id").(int) + enableModelQueryHistory := d.Get("enable_model_query_history").(bool) environment, err := c.CreateEnvironment( isActive, @@ -159,6 +166,7 @@ func resourceEnvironmentCreate( deploymentType, extendedAttributesID, connectionID, + enableModelQueryHistory, ) if err != nil { return diag.FromErr(err) @@ -236,6 +244,9 @@ func resourceEnvironmentRead( return diag.FromErr(err) } } + if err := d.Set("enable_model_query_history", environment.EnableModelQueryHistory); err != nil { + return diag.FromErr(err) + } return diags } @@ -264,7 +275,8 @@ func resourceEnvironmentUpdate( d.HasChange("use_custom_branch") || d.HasChange("deployment_type") || d.HasChange("extended_attributes_id") || - d.HasChange("connection_id") { + d.HasChange("connection_id") || + d.HasChange("enable_model_query_history") { environment, err := c.GetEnvironment(projectId, environmentId) if err != nil { @@ -323,6 +335,9 @@ func resourceEnvironmentUpdate( environment.ConnectionID = nil } } + if d.HasChange("enable_model_query_history") { + environment.EnableModelQueryHistory = d.Get("enable_model_query_history").(bool) + } _, err = c.UpdateEnvironment(projectId, environmentId, *environment) if err != nil { return diag.FromErr(err) diff --git a/pkg/sdkv2/resources/environment_acceptance_test.go b/pkg/sdkv2/resources/environment_acceptance_test.go index 0e8d4c09..dec33b6b 100644 --- a/pkg/sdkv2/resources/environment_acceptance_test.go +++ b/pkg/sdkv2/resources/environment_acceptance_test.go @@ -410,6 +410,7 @@ resource "dbtcloud_environment" "test_env" { credential_id = dbtcloud_bigquery_credential.test_credential.credential_id deployment_type = "production" connection_id = dbtcloud_global_connection.test2.id + enable_model_query_history = true } resource "dbtcloud_bigquery_credential" "test_credential" { diff --git a/pkg/sdkv2/resources/project.go b/pkg/sdkv2/resources/project.go index b0b8c99f..aa8769a2 100644 --- a/pkg/sdkv2/resources/project.go +++ b/pkg/sdkv2/resources/project.go @@ -16,6 +16,12 @@ var projectSchema = map[string]*schema.Schema{ Required: true, Description: "Project name", }, + "description": { + Type: schema.TypeString, + Optional: true, + Default: "", + Description: "Description for the project. Will show in dbt Explorer.", + }, "dbt_project_subdirectory": { Type: schema.TypeString, Optional: true, @@ -37,7 +43,11 @@ func ResourceProject() *schema.Resource { } } -func resourceProjectRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +func resourceProjectRead( + ctx context.Context, + d *schema.ResourceData, + m interface{}, +) diag.Diagnostics { c := m.(*dbt_cloud.Client) var diags diag.Diagnostics @@ -56,6 +66,9 @@ func resourceProjectRead(ctx context.Context, d *schema.ResourceData, m interfac if err := d.Set("name", project.Name); err != nil { return diag.FromErr(err) } + if err := d.Set("description", project.Description); err != nil { + return diag.FromErr(err) + } if err := d.Set("dbt_project_subdirectory", project.DbtProjectSubdirectory); err != nil { return diag.FromErr(err) } @@ -63,15 +76,20 @@ func resourceProjectRead(ctx context.Context, d *schema.ResourceData, m interfac return diags } -func resourceProjectCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +func resourceProjectCreate( + ctx context.Context, + d *schema.ResourceData, + m interface{}, +) diag.Diagnostics { c := m.(*dbt_cloud.Client) var diags diag.Diagnostics name := d.Get("name").(string) + description := d.Get("description").(string) dbtProjectSubdirectory := d.Get("dbt_project_subdirectory").(string) - p, err := c.CreateProject(name, dbtProjectSubdirectory) + p, err := c.CreateProject(name, description, dbtProjectSubdirectory) if err != nil { return diag.FromErr(err) } @@ -83,11 +101,16 @@ func resourceProjectCreate(ctx context.Context, d *schema.ResourceData, m interf return diags } -func resourceProjectUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +func resourceProjectUpdate( + ctx context.Context, + d *schema.ResourceData, + m interface{}, +) diag.Diagnostics { c := m.(*dbt_cloud.Client) projectID := d.Id() - if d.HasChange("name") || d.HasChange("dbt_project_subdirectory") { + if d.HasChange("name") || d.HasChange("description") || + d.HasChange("dbt_project_subdirectory") { project, err := c.GetProject(projectID) if err != nil { return diag.FromErr(err) @@ -97,6 +120,10 @@ func resourceProjectUpdate(ctx context.Context, d *schema.ResourceData, m interf name := d.Get("name").(string) project.Name = name } + if d.HasChange("description") { + description := d.Get("description").(string) + project.Description = description + } if d.HasChange("dbt_project_subdirectory") { dbtProjectSubdirectory := d.Get("dbt_project_subdirectory").(string) project.DbtProjectSubdirectory = &dbtProjectSubdirectory @@ -111,7 +138,11 @@ func resourceProjectUpdate(ctx context.Context, d *schema.ResourceData, m interf return resourceProjectRead(ctx, d, m) } -func resourceProjectDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +func resourceProjectDelete( + ctx context.Context, + d *schema.ResourceData, + m interface{}, +) diag.Diagnostics { c := m.(*dbt_cloud.Client) projectID := d.Id() diff --git a/pkg/sdkv2/resources/project_acceptance_test.go b/pkg/sdkv2/resources/project_acceptance_test.go index b698ca32..17cbb158 100644 --- a/pkg/sdkv2/resources/project_acceptance_test.go +++ b/pkg/sdkv2/resources/project_acceptance_test.go @@ -15,6 +15,7 @@ import ( func TestAccDbtCloudProjectResource(t *testing.T) { projectName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + projectDescription := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) projectName2 := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) resource.Test(t, resource.TestCase{ @@ -47,7 +48,7 @@ func TestAccDbtCloudProjectResource(t *testing.T) { }, // MODIFY { - Config: testAccDbtCloudProjectResourceFullConfig(projectName2), + Config: testAccDbtCloudProjectResourceFullConfig(projectName2, projectDescription), Check: resource.ComposeTestCheckFunc( testAccCheckDbtCloudProjectExists("dbtcloud_project.test_project"), resource.TestCheckResourceAttr( @@ -81,13 +82,17 @@ resource "dbtcloud_project" "test_project" { `, projectName) } -func testAccDbtCloudProjectResourceFullConfig(projectName string) string { +func testAccDbtCloudProjectResourceFullConfig( + projectName string, + projectDescription string, +) string { return fmt.Sprintf(` resource "dbtcloud_project" "test_project" { name = "%s" + description = "%s" dbt_project_subdirectory = "/project/subdirectory_where/dbt-is" } -`, projectName) +`, projectName, projectDescription) } func testAccCheckDbtCloudProjectExists(resource string) resource.TestCheckFunc { diff --git a/pkg/sdkv2/resources/project_connection.go b/pkg/sdkv2/resources/project_connection.go index edd3a420..0590672f 100644 --- a/pkg/sdkv2/resources/project_connection.go +++ b/pkg/sdkv2/resources/project_connection.go @@ -37,6 +37,7 @@ func ResourceProjectConnection() *schema.Resource { Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, + Description: "~> This resource is deprecated with the release of global connections and it will be removed in a future version of the provider. Going forward, please set the `connection_id` in the `dbtcloud_environment` resource instead.", } }