Skip to content

Commit 6062778

Browse files
trouzeclaude
andcommitted
feat: add dbtcloud_job_completion_trigger resource (closes #663)
New standalone resource for managing job-chaining triggers independently from dbtcloud_job. Solves circular dependency problems when chaining jobs. Attributes: job_id, trigger_job_id, project_id, statuses (success/error/canceled), resource_metadata. Reuses existing JobCompletionTriggerConditionsMappings and helper.StringSetToStringSlice from pkg/utils and pkg/helper. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 17814e7 commit 6062778

5 files changed

Lines changed: 423 additions & 0 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package job_completion_trigger
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework/types"
5+
)
6+
7+
type JobCompletionTriggerResourceModel struct {
8+
ID types.Int64 `tfsdk:"id"`
9+
JobID types.Int64 `tfsdk:"job_id"`
10+
TriggerJobID types.Int64 `tfsdk:"trigger_job_id"`
11+
ProjectID types.Int64 `tfsdk:"project_id"`
12+
Statuses types.Set `tfsdk:"statuses"`
13+
ResourceMetadata types.Dynamic `tfsdk:"resource_metadata"`
14+
}
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
package job_completion_trigger
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
8+
"github.com/dbt-labs/terraform-provider-dbtcloud/pkg/dbt_cloud"
9+
"github.com/dbt-labs/terraform-provider-dbtcloud/pkg/helper"
10+
"github.com/dbt-labs/terraform-provider-dbtcloud/pkg/utils"
11+
"github.com/hashicorp/terraform-plugin-framework/diag"
12+
"github.com/hashicorp/terraform-plugin-framework/path"
13+
"github.com/hashicorp/terraform-plugin-framework/resource"
14+
"github.com/hashicorp/terraform-plugin-framework/types"
15+
)
16+
17+
var (
18+
_ resource.Resource = &jobCompletionTriggerResource{}
19+
_ resource.ResourceWithConfigure = &jobCompletionTriggerResource{}
20+
_ resource.ResourceWithImportState = &jobCompletionTriggerResource{}
21+
)
22+
23+
func JobCompletionTriggerResource() resource.Resource {
24+
return &jobCompletionTriggerResource{}
25+
}
26+
27+
type jobCompletionTriggerResource struct {
28+
client *dbt_cloud.Client
29+
}
30+
31+
func (r *jobCompletionTriggerResource) Metadata(
32+
_ context.Context,
33+
req resource.MetadataRequest,
34+
resp *resource.MetadataResponse,
35+
) {
36+
resp.TypeName = req.ProviderTypeName + "_job_completion_trigger"
37+
}
38+
39+
func (r *jobCompletionTriggerResource) Schema(
40+
_ context.Context,
41+
_ resource.SchemaRequest,
42+
resp *resource.SchemaResponse,
43+
) {
44+
resp.Schema = resourceSchema
45+
}
46+
47+
func (r *jobCompletionTriggerResource) Configure(
48+
_ context.Context,
49+
req resource.ConfigureRequest,
50+
_ *resource.ConfigureResponse,
51+
) {
52+
if req.ProviderData == nil {
53+
return
54+
}
55+
r.client = req.ProviderData.(*dbt_cloud.Client)
56+
}
57+
58+
func (r *jobCompletionTriggerResource) Create(
59+
ctx context.Context,
60+
req resource.CreateRequest,
61+
resp *resource.CreateResponse,
62+
) {
63+
var plan JobCompletionTriggerResourceModel
64+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
65+
if resp.Diagnostics.HasError() {
66+
return
67+
}
68+
69+
jobID := plan.JobID.ValueInt64()
70+
job, err := r.client.GetJob(strconv.FormatInt(jobID, 10))
71+
if err != nil {
72+
resp.Diagnostics.AddError("Error fetching job", err.Error())
73+
return
74+
}
75+
76+
statuses, diags := buildStatusInts(plan.Statuses)
77+
resp.Diagnostics.Append(diags...)
78+
if resp.Diagnostics.HasError() {
79+
return
80+
}
81+
82+
job.JobCompletionTrigger = &dbt_cloud.JobCompletionTrigger{
83+
Condition: dbt_cloud.JobCompletionTriggerCondition{
84+
JobID: int(plan.TriggerJobID.ValueInt64()),
85+
ProjectID: int(plan.ProjectID.ValueInt64()),
86+
Statuses: statuses,
87+
},
88+
}
89+
90+
updatedJob, err := r.client.UpdateJob(strconv.FormatInt(jobID, 10), *job)
91+
if err != nil {
92+
resp.Diagnostics.AddError("Error setting job completion trigger", err.Error())
93+
return
94+
}
95+
96+
plan.ID = types.Int64Value(int64(*updatedJob.ID))
97+
plan.JobID = types.Int64Value(int64(*updatedJob.ID))
98+
99+
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
100+
}
101+
102+
func (r *jobCompletionTriggerResource) Read(
103+
ctx context.Context,
104+
req resource.ReadRequest,
105+
resp *resource.ReadResponse,
106+
) {
107+
var state JobCompletionTriggerResourceModel
108+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
109+
if resp.Diagnostics.HasError() {
110+
return
111+
}
112+
113+
jobID := state.JobID.ValueInt64()
114+
job, err := r.client.GetJob(strconv.FormatInt(jobID, 10))
115+
if err != nil {
116+
if helper.HandleResourceNotFound(ctx, err, &resp.Diagnostics, &resp.State, "job_completion_trigger") {
117+
return
118+
}
119+
resp.Diagnostics.AddError("Error fetching job", err.Error())
120+
return
121+
}
122+
123+
if job.JobCompletionTrigger == nil {
124+
resp.State.RemoveResource(ctx)
125+
return
126+
}
127+
128+
cond := job.JobCompletionTrigger.Condition
129+
state.TriggerJobID = types.Int64Value(int64(cond.JobID))
130+
state.ProjectID = types.Int64Value(int64(cond.ProjectID))
131+
132+
statusStrings := make([]string, 0, len(cond.Statuses))
133+
for _, s := range cond.Statuses {
134+
name, ok := utils.JobCompletionTriggerConditionsMappingCodeHuman[s]
135+
if !ok {
136+
resp.Diagnostics.AddError("Unexpected status value", fmt.Sprintf("Unknown status int %d returned from API", s))
137+
return
138+
}
139+
statusStrings = append(statusStrings, name.(string))
140+
}
141+
statusSet, setDiags := types.SetValueFrom(ctx, types.StringType, statusStrings)
142+
resp.Diagnostics.Append(setDiags...)
143+
if resp.Diagnostics.HasError() {
144+
return
145+
}
146+
state.Statuses = statusSet
147+
148+
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
149+
}
150+
151+
func (r *jobCompletionTriggerResource) Update(
152+
ctx context.Context,
153+
req resource.UpdateRequest,
154+
resp *resource.UpdateResponse,
155+
) {
156+
var plan JobCompletionTriggerResourceModel
157+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
158+
if resp.Diagnostics.HasError() {
159+
return
160+
}
161+
162+
jobID := plan.JobID.ValueInt64()
163+
job, err := r.client.GetJob(strconv.FormatInt(jobID, 10))
164+
if err != nil {
165+
resp.Diagnostics.AddError("Error fetching job", err.Error())
166+
return
167+
}
168+
169+
statuses, diags := buildStatusInts(plan.Statuses)
170+
resp.Diagnostics.Append(diags...)
171+
if resp.Diagnostics.HasError() {
172+
return
173+
}
174+
175+
job.JobCompletionTrigger = &dbt_cloud.JobCompletionTrigger{
176+
Condition: dbt_cloud.JobCompletionTriggerCondition{
177+
JobID: int(plan.TriggerJobID.ValueInt64()),
178+
ProjectID: int(plan.ProjectID.ValueInt64()),
179+
Statuses: statuses,
180+
},
181+
}
182+
183+
_, err = r.client.UpdateJob(strconv.FormatInt(jobID, 10), *job)
184+
if err != nil {
185+
resp.Diagnostics.AddError("Error updating job completion trigger", err.Error())
186+
return
187+
}
188+
189+
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
190+
}
191+
192+
func (r *jobCompletionTriggerResource) Delete(
193+
ctx context.Context,
194+
req resource.DeleteRequest,
195+
resp *resource.DeleteResponse,
196+
) {
197+
var state JobCompletionTriggerResourceModel
198+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
199+
if resp.Diagnostics.HasError() {
200+
return
201+
}
202+
203+
jobID := state.JobID.ValueInt64()
204+
job, err := r.client.GetJob(strconv.FormatInt(jobID, 10))
205+
if err != nil {
206+
if helper.HandleResourceNotFound(ctx, err, &resp.Diagnostics, &resp.State, "job_completion_trigger") {
207+
return
208+
}
209+
resp.Diagnostics.AddError("Error fetching job", err.Error())
210+
return
211+
}
212+
213+
job.JobCompletionTrigger = nil
214+
215+
_, err = r.client.UpdateJob(strconv.FormatInt(jobID, 10), *job)
216+
if err != nil {
217+
resp.Diagnostics.AddError("Error removing job completion trigger", err.Error())
218+
return
219+
}
220+
}
221+
222+
func (r *jobCompletionTriggerResource) ImportState(
223+
ctx context.Context,
224+
req resource.ImportStateRequest,
225+
resp *resource.ImportStateResponse,
226+
) {
227+
jobID, err := strconv.ParseInt(req.ID, 10, 64)
228+
if err != nil {
229+
resp.Diagnostics.AddError("Error parsing job ID for import", err.Error())
230+
return
231+
}
232+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), jobID)...)
233+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("job_id"), jobID)...)
234+
}
235+
236+
// buildStatusInts converts the plan's statuses set to a slice of API integer codes
237+
// using the shared utils.JobCompletionTriggerConditionsMappingHumanCode mapping.
238+
func buildStatusInts(statusSet types.Set) ([]int, diag.Diagnostics) {
239+
statusStrings := helper.StringSetToStringSlice(statusSet)
240+
statuses := make([]int, 0, len(statusStrings))
241+
var diags diag.Diagnostics
242+
for _, s := range statusStrings {
243+
v, ok := utils.JobCompletionTriggerConditionsMappingHumanCode[s]
244+
if !ok {
245+
diags.AddError(
246+
"Invalid status value",
247+
fmt.Sprintf("Invalid status %q: must be one of success, error, canceled", s),
248+
)
249+
return nil, diags
250+
}
251+
statuses = append(statuses, v)
252+
}
253+
return statuses, diags
254+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package job_completion_trigger_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/dbt-labs/terraform-provider-dbtcloud/pkg/framework/acctest_config"
8+
"github.com/dbt-labs/terraform-provider-dbtcloud/pkg/framework/acctest_helper"
9+
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
10+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
11+
)
12+
13+
func TestAccDbtCloudJobCompletionTriggerResource(t *testing.T) {
14+
projectName := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)
15+
jobNameA := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)
16+
jobNameB := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)
17+
18+
resource.ParallelTest(t, resource.TestCase{
19+
PreCheck: func() { acctest_helper.TestAccPreCheck(t) },
20+
ProtoV6ProviderFactories: acctest_helper.TestAccProtoV6ProviderFactories,
21+
Steps: []resource.TestStep{
22+
{
23+
Config: testAccDbtCloudJobCompletionTriggerResourceConfig(projectName, jobNameA, jobNameB),
24+
Check: resource.ComposeTestCheckFunc(
25+
resource.TestCheckResourceAttrSet(
26+
"dbtcloud_job_completion_trigger.test",
27+
"id",
28+
),
29+
resource.TestCheckResourceAttrPair(
30+
"dbtcloud_job_completion_trigger.test",
31+
"job_id",
32+
"dbtcloud_job.job_b",
33+
"id",
34+
),
35+
resource.TestCheckResourceAttrPair(
36+
"dbtcloud_job_completion_trigger.test",
37+
"trigger_job_id",
38+
"dbtcloud_job.job_a",
39+
"id",
40+
),
41+
resource.TestCheckResourceAttrPair(
42+
"dbtcloud_job_completion_trigger.test",
43+
"project_id",
44+
"dbtcloud_project.test_project",
45+
"id",
46+
),
47+
resource.TestCheckTypeSetElemAttr(
48+
"dbtcloud_job_completion_trigger.test",
49+
"statuses.*",
50+
"success",
51+
),
52+
),
53+
},
54+
{
55+
ResourceName: "dbtcloud_job_completion_trigger.test",
56+
ImportState: true,
57+
ImportStateVerify: true,
58+
ImportStateVerifyIgnore: []string{"resource_metadata"},
59+
},
60+
},
61+
})
62+
}
63+
64+
func testAccDbtCloudJobCompletionTriggerResourceConfig(projectName, jobNameA, jobNameB string) string {
65+
return fmt.Sprintf(`
66+
resource "dbtcloud_project" "test_project" {
67+
name = "%s"
68+
}
69+
70+
resource "dbtcloud_environment" "test_env" {
71+
dbt_version = "%s"
72+
name = "test"
73+
project_id = dbtcloud_project.test_project.id
74+
type = "deployment"
75+
}
76+
77+
resource "dbtcloud_job" "job_a" {
78+
name = "%s"
79+
project_id = dbtcloud_project.test_project.id
80+
environment_id = dbtcloud_environment.test_env.environment_id
81+
execute_steps = ["dbt run"]
82+
triggers = {
83+
github_webhook = false
84+
git_provider_webhook = false
85+
schedule = false
86+
}
87+
}
88+
89+
resource "dbtcloud_job" "job_b" {
90+
name = "%s"
91+
project_id = dbtcloud_project.test_project.id
92+
environment_id = dbtcloud_environment.test_env.environment_id
93+
execute_steps = ["dbt run"]
94+
triggers = {
95+
github_webhook = false
96+
git_provider_webhook = false
97+
schedule = false
98+
}
99+
}
100+
101+
resource "dbtcloud_job_completion_trigger" "test" {
102+
job_id = dbtcloud_job.job_b.id
103+
trigger_job_id = dbtcloud_job.job_a.id
104+
project_id = dbtcloud_project.test_project.id
105+
statuses = ["success"]
106+
}
107+
`, projectName, acctest_config.DBT_CLOUD_VERSION, jobNameA, jobNameB)
108+
}

0 commit comments

Comments
 (0)