Skip to content

Commit 86b9b4e

Browse files
committed
Add support for the wait_for_completed flag to aap_job resource
Letting the user wait on aap_job resources to complete before continuing creating aap resources. Extra parameters allow the user to tweak the timeout and poll interval of this wait operation
1 parent 6468ff6 commit 86b9b4e

File tree

5 files changed

+130
-11
lines changed

5 files changed

+130
-11
lines changed

docs/resources/job.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ description: |-
44
Launches an AAP job.
55
A job is launched only when the resource is first created or when the resource is changed. The triggers argument can be used to launch a new job based on any arbitrary value.
66
This resource always creates a new job in AAP. A destroy will not delete a job created by this resource, it will only remove the resource from the state.
7+
Moreover, you can set wait_for_completion to true, then Terraform will wait until this job is created and reaches any final state before continuing. This parameter works in both create and update operations.
8+
You can also tweak wait_for_completion_timeout_seconds to control the timeout limit.
79
---
810

911
# aap_job (Resource)
@@ -14,7 +16,11 @@ A job is launched only when the resource is first created or when the resource i
1416

1517
This resource always creates a new job in AAP. A destroy will not delete a job created by this resource, it will only remove the resource from the state.
1618

17-
-> **Note** To pass an inventory to an aap_job resource, the underlying job template *must* have been conigured to prompt for the inventory on launch.
19+
Moreover, you can set `wait_for_completion` to true, then Terraform will wait until this job is created and reaches any final state before continuing. This parameter works in both create and update operations.
20+
21+
You can also tweak `wait_for_completion_timeout_seconds` to control the timeout limit.
22+
23+
-> **Note** To pass an inventory to an aap_job resource, the underlying job template *must* have been configured to prompt for the inventory on launch.
1824

1925
!> **Warning** If an AAP Job launched by this resource is deleted from AAP, the resource will be removed from the state and a new job will be created to replace it.
2026

@@ -86,6 +92,13 @@ resource "aap_job" "sample_xyz" {
8692
extra_vars = "os: Linux\nautomation: ansible-devel"
8793
}
8894
95+
resource "aap_job" "sample_wait_for_completion" {
96+
job_template_id = 9
97+
inventory_id = aap_inventory.my_inventory.id
98+
wait_for_completion = true
99+
wait_for_completion_timeout_seconds = 120
100+
}
101+
89102
output "job_foo" {
90103
value = aap_job.sample_foo
91104
}
@@ -120,6 +133,8 @@ output "job_xyz" {
120133
- `extra_vars` (String) Extra Variables. Must be provided as either a JSON or YAML string.
121134
- `inventory_id` (Number) Identifier for the inventory where job should be created in. If not provided, the job will be created in the default inventory.
122135
- `triggers` (Map of String) Map of arbitrary keys and values that, when changed, will trigger a creation of a new Job on AAP. Use 'terraform taint' if you want to force the creation of a new job without changing this value.
136+
- `wait_for_completion` (Boolean) When this is set to `true`, Terraform will wait until this aap_job resource is created, reaches any final status and then, proceeds with the following resource operation
137+
- `wait_for_completion_timeout_seconds` (Number) Sets the maximum amount of seconds Terraform will wait before timing out the updates, and the job creation will fail. Default value of `120`
123138

124139
### Read-Only
125140

examples/resources/aap_job/resource.tf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ resource "aap_job" "sample_xyz" {
6262
extra_vars = "os: Linux\nautomation: ansible-devel"
6363
}
6464

65+
resource "aap_job" "sample_wait_for_completion" {
66+
job_template_id = 9
67+
inventory_id = aap_inventory.my_inventory.id
68+
wait_for_completion = true
69+
wait_for_completion_timeout_seconds = 120
70+
}
71+
6572
output "job_foo" {
6673
value = aap_job.sample_foo
6774
}

internal/provider/job_resource.go

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,24 @@ import (
77
"fmt"
88
"net/http"
99
"path"
10+
"time"
1011

1112
"github.com/ansible/terraform-provider-aap/internal/provider/customtypes"
1213
"github.com/hashicorp/terraform-plugin-framework/attr"
1314
"github.com/hashicorp/terraform-plugin-framework/diag"
1415
"github.com/hashicorp/terraform-plugin-framework/resource"
1516
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
17+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
18+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
1619
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
1720
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
1821
"github.com/hashicorp/terraform-plugin-framework/types"
22+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
1923
)
2024

25+
// Default value for the wait_for_completion timeout, so the linter doesn't complain.
26+
const waitForCompletionTimeoutDefault int64 = 120
27+
2128
// Job AAP API model
2229
type JobAPIModel struct {
2330
TemplateID int64 `json:"job_template,omitempty"`
@@ -31,14 +38,16 @@ type JobAPIModel struct {
3138

3239
// JobResourceModel maps the resource schema data.
3340
type JobResourceModel struct {
34-
TemplateID types.Int64 `tfsdk:"job_template_id"`
35-
Type types.String `tfsdk:"job_type"`
36-
URL types.String `tfsdk:"url"`
37-
Status types.String `tfsdk:"status"`
38-
InventoryID types.Int64 `tfsdk:"inventory_id"`
39-
ExtraVars customtypes.AAPCustomStringValue `tfsdk:"extra_vars"`
40-
IgnoredFields types.List `tfsdk:"ignored_fields"`
41-
Triggers types.Map `tfsdk:"triggers"`
41+
TemplateID types.Int64 `tfsdk:"job_template_id"`
42+
Type types.String `tfsdk:"job_type"`
43+
URL types.String `tfsdk:"url"`
44+
Status types.String `tfsdk:"status"`
45+
InventoryID types.Int64 `tfsdk:"inventory_id"`
46+
ExtraVars customtypes.AAPCustomStringValue `tfsdk:"extra_vars"`
47+
IgnoredFields types.List `tfsdk:"ignored_fields"`
48+
Triggers types.Map `tfsdk:"triggers"`
49+
WaitForCompletion types.Bool `tfsdk:"wait_for_completion"`
50+
WaitForCompletionTimeout types.Int64 `tfsdk:"wait_for_completion_timeout_seconds"`
4251
}
4352

4453
// JobResource is the resource implementation.
@@ -61,6 +70,40 @@ func NewJobResource() resource.Resource {
6170
return &JobResource{}
6271
}
6372

73+
// Given a string with the name of an AAP Job state, this function returns `true`
74+
// if such state is final and cannot transition further; a.k.a, the job is completed.
75+
func IsFinalStateAAPJob(state string) bool {
76+
finalStates := map[string]bool{
77+
"new": false,
78+
"pending": false,
79+
"waiting": false,
80+
"running": false,
81+
"successful": true,
82+
"failed": true,
83+
"error": true,
84+
"canceled": true,
85+
}
86+
result, isPresent := finalStates[state]
87+
return isPresent && result
88+
}
89+
90+
func retryUntilAAPJobReachesAnyFinalState(client ProviderHTTPClient, model JobResourceModel, diagnostics diag.Diagnostics) retry.RetryFunc {
91+
return func() *retry.RetryError {
92+
responseBody, err := client.Get(model.URL.ValueString())
93+
diagnostics.Append(model.ParseHttpResponse(responseBody)...)
94+
if err != nil {
95+
return retry.RetryableError(fmt.Errorf("error fetching job status: %s", err))
96+
}
97+
fmt.Printf("Job ID: %s, Current Status: %s\n", model.TemplateID, model.Status.ValueString())
98+
99+
if !IsFinalStateAAPJob(model.Status.ValueString()) {
100+
return retry.RetryableError(fmt.Errorf("job at: %s hasn't yet reached a final state. Current state: %s", model.URL, model.Status.ValueString()))
101+
} else {
102+
return nil
103+
}
104+
}
105+
}
106+
64107
// Metadata returns the resource type name.
65108
func (r *JobResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
66109
resp.TypeName = req.ProviderTypeName + "_job"
@@ -132,14 +175,32 @@ func (r *JobResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *
132175
Computed: true,
133176
Description: "The list of properties set by the user but ignored on server side.",
134177
},
178+
"wait_for_completion": schema.BoolAttribute{
179+
Optional: true,
180+
Computed: true,
181+
Default: booldefault.StaticBool(false),
182+
Description: "When this is set to `true`, Terraform will wait until this aap_job resource is created, reaches " +
183+
"any final status and then, proceeds with the following resource operation",
184+
},
185+
"wait_for_completion_timeout_seconds": schema.Int64Attribute{
186+
Optional: true,
187+
Computed: true,
188+
Default: int64default.StaticInt64(waitForCompletionTimeoutDefault),
189+
Description: "Sets the maximum amount of seconds Terraform will wait before timing out the updates, " +
190+
"and the job creation will fail. Default value of `120`",
191+
},
135192
},
136193
MarkdownDescription: "Launches an AAP job.\n\n" +
137194
"A job is launched only when the resource is first created or when the " +
138195
"resource is changed. The " + "`triggers`" + " argument can be used to " +
139196
"launch a new job based on any arbitrary value.\n\n" +
140197
"This resource always creates a new job in AAP. A destroy will not " +
141198
"delete a job created by this resource, it will only remove the resource " +
142-
"from the state.",
199+
"from the state.\n\n" +
200+
"Moreover, you can set `wait_for_completion` to true, then Terraform will " +
201+
"wait until this job is created and reaches any final state before continuing. " +
202+
"This parameter works in both create and update operations.\n\n" +
203+
"You can also tweak `wait_for_completion_timeout_seconds` to control the timeout limit.",
143204
}
144205
}
145206

@@ -157,6 +218,19 @@ func (r *JobResource) Create(ctx context.Context, req resource.CreateRequest, re
157218
return
158219
}
159220

221+
// If the job was configured to wait for completion, start polling the job status
222+
// and wait for it to complete before marking the resource as created
223+
if data.WaitForCompletion.ValueBool() {
224+
timeout := time.Duration(data.WaitForCompletionTimeout.ValueInt64()) * time.Second
225+
err := retry.RetryContext(ctx, timeout, retryUntilAAPJobReachesAnyFinalState(r.client, data, resp.Diagnostics))
226+
if err != nil {
227+
resp.Diagnostics.Append(diag.NewErrorDiagnostic("error when waiting for AAP job to complete", err.Error()))
228+
}
229+
if resp.Diagnostics.HasError() {
230+
return
231+
}
232+
}
233+
160234
// Save updated data into Terraform state
161235
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
162236
if resp.Diagnostics.HasError() {
@@ -220,6 +294,19 @@ func (r *JobResource) Update(ctx context.Context, req resource.UpdateRequest, re
220294
return
221295
}
222296

297+
// If the job was configured to wait for completion, start polling the job status
298+
// and wait for it to complete before marking the resource as created
299+
if data.WaitForCompletion.ValueBool() {
300+
timeout := time.Duration(data.WaitForCompletionTimeout.ValueInt64()) * time.Second
301+
err := retry.RetryContext(ctx, timeout, retryUntilAAPJobReachesAnyFinalState(r.client, data, resp.Diagnostics))
302+
if err != nil {
303+
resp.Diagnostics.Append(diag.NewErrorDiagnostic("error when waiting for AAP job to complete", err.Error()))
304+
}
305+
if resp.Diagnostics.HasError() {
306+
return
307+
}
308+
}
309+
223310
// Save updated data into Terraform state
224311
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
225312
if resp.Diagnostics.HasError() {

internal/provider/job_resource_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ func TestJobResourceCreateRequestBody(t *testing.T) {
100100
},
101101
expected: []byte(`{"inventory": 3}`),
102102
},
103+
{
104+
name: "wait_for_completed parameters",
105+
input: JobResourceModel{
106+
InventoryID: basetypes.NewInt64Value(3),
107+
TemplateID: types.Int64Value(1),
108+
WaitForCompletion: basetypes.NewBoolValue(true),
109+
WaitForCompletionTimeout: basetypes.NewInt64Value(60),
110+
},
111+
expected: []byte(`{"inventory":3}`),
112+
},
103113
}
104114

105115
for _, tc := range testTable {

templates/resources/job.md.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ description: |-
88

99
{{ .Description | trimspace }}
1010

11-
-> **Note** To pass an inventory to an aap_job resource, the underlying job template *must* have been conigured to prompt for the inventory on launch.
11+
-> **Note** To pass an inventory to an aap_job resource, the underlying job template *must* have been configured to prompt for the inventory on launch.
1212

1313
!> **Warning** If an AAP Job launched by this resource is deleted from AAP, the resource will be removed from the state and a new job will be created to replace it.
1414

0 commit comments

Comments
 (0)