Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changes/unreleased/Deprecations-20250723-131135.yaml
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion docs/data-sources/job.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion docs/data-sources/jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 9 additions & 1 deletion docs/resources/job.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand All @@ -247,6 +247,14 @@ Optional:
- `schedule` (Boolean) Whether the job runs on a schedule


<a id="nestedatt--execution"></a>
### Nested Schema for `execution`

Optional:

- `timeout_seconds` (Number) The number of seconds before the job times out


<a id="nestedblock--job_completion_trigger_condition"></a>
### Nested Schema for `job_completion_trigger_condition`

Expand Down
31 changes: 15 additions & 16 deletions pkg/framework/objects/job/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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"`
}
114 changes: 97 additions & 17 deletions pkg/framework/objects/job/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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)...)
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -190,7 +193,6 @@ func (j *jobResource) Create(ctx context.Context, req resource.CreateRequest, re
}
}


createDbtVersion := ""
if dbtVersion != nil {
createDbtVersion = *dbtVersion
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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()
Expand All @@ -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
}
62 changes: 62 additions & 0 deletions pkg/framework/objects/job/resource_acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading