Skip to content

Add support for the wait_for_completed flag to aap_job resource #65

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 22, 2025
Merged
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
17 changes: 16 additions & 1 deletion docs/resources/job.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ description: |-
Launches an AAP job.
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.
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.
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.
You can also tweak wait_for_completion_timeout_seconds to control the timeout limit.
---

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

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.

-> **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.
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.

You can also tweak `wait_for_completion_timeout_seconds` to control the timeout limit.

-> **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.

!> **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.

Expand Down Expand Up @@ -86,6 +92,13 @@ resource "aap_job" "sample_xyz" {
extra_vars = "os: Linux\nautomation: ansible-devel"
}

resource "aap_job" "sample_wait_for_completion" {
job_template_id = 9
inventory_id = aap_inventory.my_inventory.id
wait_for_completion = true
wait_for_completion_timeout_seconds = 120
}

output "job_foo" {
value = aap_job.sample_foo
}
Expand Down Expand Up @@ -120,6 +133,8 @@ output "job_xyz" {
- `extra_vars` (String) Extra Variables. Must be provided as either a JSON or YAML string.
- `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.
- `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.
- `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
- `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`

### Read-Only

Expand Down
7 changes: 7 additions & 0 deletions examples/resources/aap_job/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ resource "aap_job" "sample_xyz" {
extra_vars = "os: Linux\nautomation: ansible-devel"
}

resource "aap_job" "sample_wait_for_completion" {
job_template_id = 9
inventory_id = aap_inventory.my_inventory.id
wait_for_completion = true
wait_for_completion_timeout_seconds = 120
}

output "job_foo" {
value = aap_job.sample_foo
}
Expand Down
105 changes: 96 additions & 9 deletions internal/provider/job_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,24 @@ import (
"fmt"
"net/http"
"path"
"time"

"github.com/ansible/terraform-provider-aap/internal/provider/customtypes"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
)

// Default value for the wait_for_completion timeout, so the linter doesn't complain.
const waitForCompletionTimeoutDefault int64 = 120

// Job AAP API model
type JobAPIModel struct {
TemplateID int64 `json:"job_template,omitempty"`
Expand All @@ -31,14 +38,16 @@ type JobAPIModel struct {

// JobResourceModel maps the resource schema data.
type JobResourceModel struct {
TemplateID types.Int64 `tfsdk:"job_template_id"`
Type types.String `tfsdk:"job_type"`
URL types.String `tfsdk:"url"`
Status types.String `tfsdk:"status"`
InventoryID types.Int64 `tfsdk:"inventory_id"`
ExtraVars customtypes.AAPCustomStringValue `tfsdk:"extra_vars"`
IgnoredFields types.List `tfsdk:"ignored_fields"`
Triggers types.Map `tfsdk:"triggers"`
TemplateID types.Int64 `tfsdk:"job_template_id"`
Type types.String `tfsdk:"job_type"`
URL types.String `tfsdk:"url"`
Status types.String `tfsdk:"status"`
InventoryID types.Int64 `tfsdk:"inventory_id"`
ExtraVars customtypes.AAPCustomStringValue `tfsdk:"extra_vars"`
IgnoredFields types.List `tfsdk:"ignored_fields"`
Triggers types.Map `tfsdk:"triggers"`
WaitForCompletion types.Bool `tfsdk:"wait_for_completion"`
WaitForCompletionTimeout types.Int64 `tfsdk:"wait_for_completion_timeout_seconds"`
}

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

// Given a string with the name of an AAP Job state, this function returns `true`
// if such state is final and cannot transition further; a.k.a, the job is completed.
func IsFinalStateAAPJob(state string) bool {
finalStates := map[string]bool{
"new": false,
"pending": false,
"waiting": false,
"running": false,
"successful": true,
"failed": true,
"error": true,
"canceled": true,
}
result, isPresent := finalStates[state]
return isPresent && result
}

func retryUntilAAPJobReachesAnyFinalState(client ProviderHTTPClient, model JobResourceModel, diagnostics diag.Diagnostics) retry.RetryFunc {
return func() *retry.RetryError {
responseBody, err := client.Get(model.URL.ValueString())
diagnostics.Append(model.ParseHttpResponse(responseBody)...)
if err != nil {
return retry.RetryableError(fmt.Errorf("error fetching job status: %s", err))
}
fmt.Printf("Job ID: %s, Current Status: %s\n", model.TemplateID, model.Status.ValueString())

if !IsFinalStateAAPJob(model.Status.ValueString()) {
return retry.RetryableError(fmt.Errorf("job at: %s hasn't yet reached a final state. Current state: %s", model.URL, model.Status.ValueString()))
} else {
return nil
}
}
}

// Metadata returns the resource type name.
func (r *JobResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_job"
Expand Down Expand Up @@ -132,14 +175,32 @@ func (r *JobResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *
Computed: true,
Description: "The list of properties set by the user but ignored on server side.",
},
"wait_for_completion": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
Description: "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",
},
"wait_for_completion_timeout_seconds": schema.Int64Attribute{
Optional: true,
Computed: true,
Default: int64default.StaticInt64(waitForCompletionTimeoutDefault),
Description: "Sets the maximum amount of seconds Terraform will wait before timing out the updates, " +
"and the job creation will fail. Default value of `120`",
},
},
MarkdownDescription: "Launches an AAP job.\n\n" +
"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.\n\n" +
"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.",
"from the state.\n\n" +
"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.\n\n" +
"You can also tweak `wait_for_completion_timeout_seconds` to control the timeout limit.",
}
}

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

// If the job was configured to wait for completion, start polling the job status
// and wait for it to complete before marking the resource as created
if data.WaitForCompletion.ValueBool() {
timeout := time.Duration(data.WaitForCompletionTimeout.ValueInt64()) * time.Second
err := retry.RetryContext(ctx, timeout, retryUntilAAPJobReachesAnyFinalState(r.client, data, resp.Diagnostics))
if err != nil {
resp.Diagnostics.Append(diag.NewErrorDiagnostic("error when waiting for AAP job to complete", err.Error()))
}
if resp.Diagnostics.HasError() {
return
}
}

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
if resp.Diagnostics.HasError() {
Expand Down Expand Up @@ -220,6 +294,19 @@ func (r *JobResource) Update(ctx context.Context, req resource.UpdateRequest, re
return
}

// If the job was configured to wait for completion, start polling the job status
// and wait for it to complete before marking the resource as created
if data.WaitForCompletion.ValueBool() {
timeout := time.Duration(data.WaitForCompletionTimeout.ValueInt64()) * time.Second
err := retry.RetryContext(ctx, timeout, retryUntilAAPJobReachesAnyFinalState(r.client, data, resp.Diagnostics))
if err != nil {
resp.Diagnostics.Append(diag.NewErrorDiagnostic("error when waiting for AAP job to complete", err.Error()))
}
if resp.Diagnostics.HasError() {
return
}
}

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
if resp.Diagnostics.HasError() {
Expand Down
10 changes: 10 additions & 0 deletions internal/provider/job_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ func TestJobResourceCreateRequestBody(t *testing.T) {
},
expected: []byte(`{"inventory": 3}`),
},
{
name: "wait_for_completed parameters",
input: JobResourceModel{
InventoryID: basetypes.NewInt64Value(3),
TemplateID: types.Int64Value(1),
WaitForCompletion: basetypes.NewBoolValue(true),
WaitForCompletionTimeout: basetypes.NewInt64Value(60),
},
expected: []byte(`{"inventory":3}`),
},
}

for _, tc := range testTable {
Expand Down
2 changes: 1 addition & 1 deletion templates/resources/job.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ description: |-

{{ .Description | trimspace }}

-> **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.
-> **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.

!> **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.

Expand Down