diff --git a/.changes/unreleased/Deprecations-20250723-131135.yaml b/.changes/unreleased/Deprecations-20250723-131135.yaml new file mode 100644 index 00000000..0b5dfd37 --- /dev/null +++ b/.changes/unreleased/Deprecations-20250723-131135.yaml @@ -0,0 +1,3 @@ +kind: Deprecations +body: timeout_seconds is deprecated, new property in execution block +time: 2025-07-23T13:11:35.329628+03:00 diff --git a/docs/data-sources/job.md b/docs/data-sources/job.md index fe9036d1..2e3e19ab 100644 --- a/docs/data-sources/job.md +++ b/docs/data-sources/job.md @@ -43,7 +43,6 @@ Get detailed information for a specific dbt Cloud job. - `schedule` (Attributes) (see [below for nested schema](#nestedatt--schedule)) - `self_deferring` (Boolean) Whether this job defers on a previous run of itself (overrides value in deferring_job_id) - `settings` (Attributes) (see [below for nested schema](#nestedatt--settings)) -- `timeout_seconds` (Number, Deprecated) [Deprectated - Moved to execution.timeout_seconds] Number of seconds before the job times out - `triggers` (Attributes) (see [below for nested schema](#nestedatt--triggers)) - `triggers_on_draft_pr` (Boolean) Whether the CI job should be automatically triggered on draft PRs diff --git a/docs/data-sources/jobs.md b/docs/data-sources/jobs.md index 23913550..d7feb9c6 100644 --- a/docs/data-sources/jobs.md +++ b/docs/data-sources/jobs.md @@ -69,7 +69,6 @@ Read-Only: - `run_generate_sources` (Boolean) Whether the job test source freshness - `schedule` (Attributes) (see [below for nested schema](#nestedatt--jobs--schedule)) - `settings` (Attributes) (see [below for nested schema](#nestedatt--jobs--settings)) -- `timeout_seconds` (Number, Deprecated) [Deprectated - Moved to execution.timeout_seconds] Number of seconds before the job times out - `triggers` (Attributes) (see [below for nested schema](#nestedatt--jobs--triggers)) - `triggers_on_draft_pr` (Boolean) Whether the CI job should be automatically triggered on draft PRs diff --git a/docs/resources/job.md b/docs/resources/job.md index 3d23d477..7e2e2262 100644 --- a/docs/resources/job.md +++ b/docs/resources/job.md @@ -213,6 +213,7 @@ An example can be found [in this GitHub issue](https://github.com/dbt-labs/terra - `deferring_job_id` (Number) Job identifier that this job defers to (legacy deferring approach) - `description` (String) Description for the job - `errors_on_lint_failure` (Boolean) Whether the CI job should fail when a lint error is found. Only used when `run_lint` is set to `true`. Defaults to `true`. +- `execution` (Attributes) (see [below for nested schema](#nestedatt--execution)) - `generate_docs` (Boolean) Flag for whether the job should generate documentation - `is_active` (Boolean) Should always be set to true as setting it to false is the same as creating a job in a deleted state. To create/keep a job in a 'deactivated' state, check the `triggers` config. - `job_completion_trigger_condition` (Block List) Which other job should trigger this job when it finishes, and on which conditions (sometimes referred as 'job chaining'). (see [below for nested schema](#nestedblock--job_completion_trigger_condition)) @@ -228,7 +229,6 @@ An example can be found [in this GitHub issue](https://github.com/dbt-labs/terra - `schedule_type` (String) Type of schedule to use, one of every_day/ days_of_week/ custom_cron/ interval_cron - `self_deferring` (Boolean) Whether this job defers on a previous run of itself - `target_name` (String) Target name for the dbt profile -- `timeout_seconds` (Number, Deprecated) [Deprectated - Moved to execution.timeout_seconds] Number of seconds to allow the job to run before timing out - `triggers_on_draft_pr` (Boolean) Whether the CI job should be automatically triggered on draft PRs ### Read-Only @@ -247,6 +247,14 @@ Optional: - `schedule` (Boolean) Whether the job runs on a schedule + +### Nested Schema for `execution` + +Optional: + +- `timeout_seconds` (Number) The number of seconds before the job times out + + ### Nested Schema for `job_completion_trigger_condition` diff --git a/pkg/framework/objects/job/model.go b/pkg/framework/objects/job/model.go index 60deeb4a..ca79a9f4 100644 --- a/pkg/framework/objects/job/model.go +++ b/pkg/framework/objects/job/model.go @@ -99,8 +99,7 @@ type SingleJobDataSourceModel struct { } type JobResourceModel struct { - // Execution *JobExecution `tfsdk:"execution"` // has timeout-seconds - TimeoutSeconds types.Int64 `tfsdk:"timeout_seconds"` // moved under execution , add deprecation message + Execution types.Object `tfsdk:"execution"` // has timeout-seconds GenerateDocs types.Bool `tfsdk:"generate_docs"` // exists RunGenerateSources types.Bool `tfsdk:"run_generate_sources"` // exists ID types.Int64 `tfsdk:"id"` // will hold job id? @@ -119,18 +118,18 @@ type JobResourceModel struct { TriggersOnDraftPr types.Bool `tfsdk:"triggers_on_draft_pr"` // exists // Environment *JobEnvironment `tfsdk:"environment"` JobCompletionTriggerCondition []*JobCompletionTriggerCondition `tfsdk:"job_completion_trigger_condition"` // exists - RunCompareChanges types.Bool `tfsdk:"run_compare_changes"` // exists - IsActive types.Bool `tfsdk:"is_active"` - TargetName types.String `tfsdk:"target_name"` // add deprecated - NumThreads types.Int64 `tfsdk:"num_threads"` // add deprecated moved to settings - RunLint types.Bool `tfsdk:"run_lint"` - ErrorsOnLintFailure types.Bool `tfsdk:"errors_on_lint_failure"` - ScheduleType types.String `tfsdk:"schedule_type"` - ScheduleInterval types.Int64 `tfsdk:"schedule_interval"` - ScheduleHours []types.Int64 `tfsdk:"schedule_hours"` - ScheduleDays []types.Int64 `tfsdk:"schedule_days"` - ScheduleCron types.String `tfsdk:"schedule_cron"` // add deprecated move to schedule - DeferringJobId types.Int64 `tfsdk:"deferring_job_id"` // add deprecated move to deferring_job_definition_id - SelfDeferring types.Bool `tfsdk:"self_deferring"` - CompareChangesFlags types.String `tfsdk:"compare_changes_flags"` + RunCompareChanges types.Bool `tfsdk:"run_compare_changes"` // exists + IsActive types.Bool `tfsdk:"is_active"` + TargetName types.String `tfsdk:"target_name"` // add deprecated + NumThreads types.Int64 `tfsdk:"num_threads"` // add deprecated moved to settings + RunLint types.Bool `tfsdk:"run_lint"` + ErrorsOnLintFailure types.Bool `tfsdk:"errors_on_lint_failure"` + ScheduleType types.String `tfsdk:"schedule_type"` + ScheduleInterval types.Int64 `tfsdk:"schedule_interval"` + ScheduleHours []types.Int64 `tfsdk:"schedule_hours"` + ScheduleDays []types.Int64 `tfsdk:"schedule_days"` + ScheduleCron types.String `tfsdk:"schedule_cron"` // add deprecated move to schedule + DeferringJobId types.Int64 `tfsdk:"deferring_job_id"` // add deprecated move to deferring_job_definition_id + SelfDeferring types.Bool `tfsdk:"self_deferring"` + CompareChangesFlags types.String `tfsdk:"compare_changes_flags"` } diff --git a/pkg/framework/objects/job/resource.go b/pkg/framework/objects/job/resource.go index 64422c94..6abed253 100644 --- a/pkg/framework/objects/job/resource.go +++ b/pkg/framework/objects/job/resource.go @@ -10,9 +10,12 @@ import ( "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/dbt_cloud" "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/helper" "github.com/dbt-labs/terraform-provider-dbtcloud/pkg/utils" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) var ( @@ -87,7 +90,7 @@ func (j *jobResource) ImportState(ctx context.Context, req resource.ImportStateR ) return } - + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), jobID)...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("job_id"), jobID)...) } @@ -162,7 +165,7 @@ func (j *jobResource) Create(ctx context.Context, req resource.CreateRequest, re } selfDeferring := plan.SelfDeferring.ValueBool() - timeoutSeconds := int(plan.TimeoutSeconds.ValueInt64()) + timeoutSeconds, _ := getTimeoutFromPlan(ctx, plan) triggersOnDraftPR := plan.TriggersOnDraftPr.ValueBool() runCompareChanges := plan.RunCompareChanges.ValueBool() runLint := plan.RunLint.ValueBool() @@ -190,7 +193,6 @@ func (j *jobResource) Create(ctx context.Context, req resource.CreateRequest, re } } - createDbtVersion := "" if dbtVersion != nil { createDbtVersion = *dbtVersion @@ -241,19 +243,19 @@ func (j *jobResource) Create(ctx context.Context, req resource.CreateRequest, re return } - plan.ID = types.Int64Value(int64(*createdJob.ID)) - plan.JobId = types.Int64Value(int64(*createdJob.ID)) + plan.ID = types.Int64Value(int64(*createdJob.ID)) + plan.JobId = types.Int64Value(int64(*createdJob.ID)) if createdJob.JobType != "" { plan.JobType = types.StringValue(createdJob.JobType) } else { plan.JobType = types.StringNull() } - + jobIDStr := strconv.FormatInt(int64(*createdJob.ID), 10) createdSelfDeferring := createdJob.DeferringJobId != nil && strconv.Itoa(*createdJob.DeferringJobId) == jobIDStr plan.SelfDeferring = types.BoolValue(createdSelfDeferring) - + diags := resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -362,8 +364,8 @@ func (j *jobResource) Read(ctx context.Context, req resource.ReadRequest, resp * state.ScheduleDays = scheduleDaysNull } - if retrievedJob.Schedule.Date.Cron != nil && - retrievedJob.Schedule.Date.Type != "interval_cron" { // for interval_cron, the cron expression is auto generated in the code + if retrievedJob.Schedule.Date.Cron != nil && + retrievedJob.Schedule.Date.Type != "interval_cron" { // for interval_cron, the cron expression is auto generated in the code state.ScheduleCron = types.StringValue(*retrievedJob.Schedule.Date.Cron) } else { state.ScheduleCron = types.StringNull() @@ -384,12 +386,12 @@ func (j *jobResource) Read(ctx context.Context, req resource.ReadRequest, resp * } state.SelfDeferring = types.BoolValue(selfDeferring) - state.TimeoutSeconds = types.Int64Value(int64(retrievedJob.Execution.TimeoutSeconds)) + state.Execution, _ = executionStateFromResponse(ctx, retrievedJob) var triggers map[string]interface{} triggersInput, _ := json.Marshal(retrievedJob.Triggers) json.Unmarshal(triggersInput, &triggers) - + // for now, we allow people to keep the triggers.custom_branch_only config even if the parameter was deprecated in the API // we set the state to the current config value, so it doesn't do anything var customBranchValue types.Bool @@ -443,7 +445,7 @@ func (j *jobResource) Read(ctx context.Context, req resource.ReadRequest, resp * state.CompareChangesFlags = types.StringValue(retrievedJob.CompareChangesFlags) state.RunLint = types.BoolValue(retrievedJob.RunLint) state.ErrorsOnLintFailure = types.BoolValue(retrievedJob.ErrorsOnLintFailure) - + if retrievedJob.JobType != "" { state.JobType = types.StringValue(retrievedJob.JobType) } else { @@ -578,9 +580,9 @@ func (j *jobResource) Update(ctx context.Context, req resource.UpdateRequest, re } } - job.Execution.TimeoutSeconds = int(plan.TimeoutSeconds.ValueInt64()) + job.Execution = getJobExecutionPtrFromPlan(ctx, plan) job.TriggersOnDraftPR = plan.TriggersOnDraftPr.ValueBool() - + if len(plan.JobCompletionTriggerCondition) == 0 { job.JobCompletionTrigger = nil } else { @@ -598,7 +600,7 @@ func (j *jobResource) Update(ctx context.Context, req resource.UpdateRequest, re } job.JobCompletionTrigger = &jobCondTrigger } - + job.RunCompareChanges = plan.RunCompareChanges.ValueBool() job.RunLint = plan.RunLint.ValueBool() job.ErrorsOnLintFailure = plan.ErrorsOnLintFailure.ValueBool() @@ -618,14 +620,92 @@ func (j *jobResource) Update(ctx context.Context, req resource.UpdateRequest, re } else { plan.JobType = types.StringNull() } - + updatedJobIDStr := strconv.FormatInt(jobID, 10) updatedSelfDeferring := updatedJob.DeferringJobId != nil && strconv.Itoa(*updatedJob.DeferringJobId) == updatedJobIDStr plan.SelfDeferring = types.BoolValue(updatedSelfDeferring) - + diags := resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } } + +func getTimeoutFromPlan(ctx context.Context, plan JobResourceModel) (int, diag.Diagnostics) { + var diags diag.Diagnostics + + // 1. Declare a variable for your target model. + var executionSettings JobExecution + var timeoutSeconds int // Use a pointer to handle the case where the inner field is null. + + // 2. Check if the execution object is null or unknown. This is true if the user + // did not include the `execution` block in their configuration. + if plan.Execution.IsNull() || plan.Execution.IsUnknown() { + return 0, diags + } + + // 3. Use the .As() method to convert the types.Object into your model. + diags.Append(plan.Execution.As(ctx, &executionSettings, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return 0, diags + } + + // 4. Now that you have the populated model, you can access its fields. + // It's still important to check if the inner field is null. + if !executionSettings.TimeoutSeconds.IsNull() && !executionSettings.TimeoutSeconds.IsUnknown() { + timeoutSeconds = int(executionSettings.TimeoutSeconds.ValueInt64()) + } + + return timeoutSeconds, diags +} + +func getJobExecutionPtrFromPlan(ctx context.Context, plan JobResourceModel) dbt_cloud.JobExecution { + var diags diag.Diagnostics + + // 1. Declare a variable for your target model. + var executionSettings = dbt_cloud.JobExecution{ + TimeoutSeconds: 0, // Default to null + } + + // 2. Check if the execution object is null or unknown. This is true if the user + // did not include the `execution` block in their configuration. + if plan.Execution.IsNull() || plan.Execution.IsUnknown() { + return executionSettings + } + + // 3. Use the .As() method to convert the types.Object into your model. + diags.Append(plan.Execution.As(ctx, &executionSettings, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return executionSettings + } + + return executionSettings +} + +// executionStateFromResponse converts the execution settings from a dbt Cloud API response +// into a types.Object suitable for the Terraform state. +func executionStateFromResponse(ctx context.Context, retrievedJob *dbt_cloud.Job) (types.Object, diag.Diagnostics) { + var diags diag.Diagnostics + + // Define the schema for the execution object. This is needed for creating a null object. + attributeTypes := map[string]attr.Type{ + "timeout_seconds": types.Int64Type, + } + + // If the API response doesn't contain execution settings, return a null object. + if retrievedJob.Execution.TimeoutSeconds == 0 { + return types.ObjectNull(attributeTypes), diags + } + + // If settings were returned, populate our model from the API response. + executionModel := JobExecution{ + TimeoutSeconds: types.Int64Value(int64(retrievedJob.Execution.TimeoutSeconds)), + } + + // Convert the populated model into a types.Object. + executionObject, d := types.ObjectValueFrom(ctx, attributeTypes, executionModel) + diags.Append(d...) + + return executionObject, diags +} diff --git a/pkg/framework/objects/job/resource_acceptance_test.go b/pkg/framework/objects/job/resource_acceptance_test.go index dc687e6f..67c838b5 100644 --- a/pkg/framework/objects/job/resource_acceptance_test.go +++ b/pkg/framework/objects/job/resource_acceptance_test.go @@ -686,6 +686,68 @@ resource "dbtcloud_job" "test_job" { `, projectName, environmentName, acctest_config.DBT_CLOUD_VERSION, jobName, scheduleConfig) } +func TestAccDbtCloudJobResourceExecution(t *testing.T) { + + jobName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + projectName := 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: testAccCheckDbtCloudJobDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDbtCloudJobResourceExecutionConfig( + jobName, + projectName, + environmentName, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckDbtCloudJobExists("dbtcloud_job.test_job"), + resource.TestCheckResourceAttr("dbtcloud_job.test_job", "name", jobName), + ), + }, + }, + }) +} + +func testAccDbtCloudJobResourceExecutionConfig( + jobName, projectName, environmentName string, +) string { + return fmt.Sprintf(` +resource "dbtcloud_project" "test_job_project" { + name = "%s" +} + +resource "dbtcloud_environment" "test_job_environment" { + project_id = dbtcloud_project.test_job_project.id + name = "%s" + dbt_version = "%s" + type = "deployment" +} + +resource "dbtcloud_job" "test_job" { + name = "%s" + project_id = dbtcloud_project.test_job_project.id + environment_id = dbtcloud_environment.test_job_environment.environment_id + + execution = { + "timeout_seconds"= 180 + } + + execute_steps = [ + "dbt test" + ] + triggers = { + "github_webhook": false, + "git_provider_webhook": false, + "schedule": false, + } +} +`, projectName, environmentName, acctest_config.DBT_CLOUD_VERSION, jobName) +} + func testAccDbtCloudJobResourceBasicConfigTriggers( jobName, projectName, environmentName, trigger string, ) string { diff --git a/pkg/framework/objects/job/schema.go b/pkg/framework/objects/job/schema.go index b39a70e6..0bd9a688 100644 --- a/pkg/framework/objects/job/schema.go +++ b/pkg/framework/objects/job/schema.go @@ -33,11 +33,6 @@ func getJobAttributes() map[string]schema.Attribute { }, }, }, - "timeout_seconds": schema.Int64Attribute{ - Computed: true, - DeprecationMessage: "Moved to execution.timeout_seconds", - Description: "[Deprectated - Moved to execution.timeout_seconds] Number of seconds before the job times out", - }, "generate_docs": schema.BoolAttribute{ Computed: true, Description: "Whether the job generate docs", @@ -329,24 +324,16 @@ func (j *jobResource) Schema( int64planmodifier.UseStateForUnknown(), }, }, - // "execution": resource_schema.SingleNestedAttribute{ - // Optional: true, - // Computed: true, - // Attributes: map[string]resource_schema.Attribute{ - // "timeout_seconds": resource_schema.Int64Attribute{ - // Optional: true, - // Computed: true, - // Default: int64default.StaticInt64(0), - // Description: "The number of seconds before the job times out", - // }, - // }, - // }, - "timeout_seconds": resource_schema.Int64Attribute{ - Optional: true, - Computed: true, - Default: int64default.StaticInt64(0), - DeprecationMessage: "Moved to execution.timeout_seconds", - Description: "[Deprectated - Moved to execution.timeout_seconds] Number of seconds to allow the job to run before timing out", + "execution": resource_schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]resource_schema.Attribute{ + "timeout_seconds": resource_schema.Int64Attribute{ + Optional: true, + Computed: true, + Default: int64default.StaticInt64(0), + Description: "The number of seconds before the job times out", + }, + }, }, "generate_docs": resource_schema.BoolAttribute{ Optional: true,