Skip to content

Commit e7704b3

Browse files
fdevansltamaster
andauthored
RUN-4103: Release 1.1.2: Fix value_choices validation and node_filter_exclude_query (#220)
## Summary This PR contains bug fixes for v1.1.2 release: 1. **Fix validation error with variable-based value_choices** ([#218](#218)) - Job options with `require_predefined_choice = true` and `value_choices` set to a Terraform variable no longer fail validation during plan phase. 2. **Fix node_filter_exclude_query not working** - The `node_filter_exclude_query` field was not being sent to Rundeck API correctly. Fixed by using the correct API field name `filterExclude` instead of `excludeFilter`. --- ## Changes ### Bug Fix 1: value_choices from variables ([#218](#218)) - **resource_job_framework.go**: Modified validation logic to skip unknown values instead of treating them as errors - Skip validation when entire `value_choices` list is unknown - Skip validation when any individual choice element is unknown - Validation runs during apply phase when values are concrete ### Bug Fix 2: node_filter_exclude_query - **resource_job_framework.go**: Fixed `jobNodeFilters` struct to use correct Rundeck API field - Changed from `excludeFilter` to `filterExclude` (correct Rundeck API field name) - Updated `planToJobJSON` to properly set `FilterExclude` field - Updated `jobJSONAPIToState` and `jobJSONToState` to read `filterExclude` from API response ### Test Coverage - **resource_job_test.go**: Added `TestAccJobOptions_value_choices_from_variable` - Tests job option with `value_choices` set to a variable - Verifies plan succeeds without validation error - Confirms values are correctly applied - **resource_job_test.go**: Added `TestAccJob_node_filter_exclude` - Tests `node_filter_exclude_query` with include filter - Tests both `node_filter_query` and `node_filter_exclude_query` together - Verifies `node_filter_exclude_precedence` works correctly ### Documentation - **CHANGELOG.md**: Added v1.1.2 release notes with both bug fix descriptions --- ## Test Plan - [x] Code compiles successfully (`go build`) - [x] Code formatted (`make fmt`) - [x] No linting errors (`go vet`) - [ ] Acceptance test passes: `TestAccJobOptions_value_choices_from_variable` - [ ] Acceptance test passes: `TestAccJob_node_filter_exclude` - [ ] Existing tests still pass - [x] Manual testing with user's exact scenario from Issue #218 - [x] Manual testing of `node_filter_exclude_query` against local Rundeck instance --- ## Backward Compatibility ✅ Fully backward compatible - Existing validation logic unchanged for concrete values - Still validates empty strings and requires at least one non-empty value - Only skips validation when values are unknown (new behavior) - `node_filter_exclude_query` now works as documented --- ## Related Issues Fixes #218 --------- Co-authored-by: Luis Toledo <luis@variacode.com>
1 parent ed50d1b commit e7704b3

File tree

4 files changed

+198
-11
lines changed

4 files changed

+198
-11
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ jobs:
8989
TF_ACC: 1
9090
RUNDECK_URL: http://localhost:4440
9191
RUNDECK_AUTH_TOKEN: 1d08bf61-f962-467f-8ba3-ab8a463b3467
92+
RUNDECK_AUTH_USERNAME: admin
93+
RUNDECK_AUTH_PASSWORD: admin # This is the default password for the admin user in Rundeck
9294
# Don't run Enterprise tests in CI (Community Edition)
9395
RUNDECK_ENTERPRISE_TESTS: 0
9496

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
## 1.1.2
2+
3+
**Bug Fixes**
4+
5+
### Job Resource
6+
- **Fixed validation error with variable-based value_choices** ([#218](https://github.com/rundeck/terraform-provider-rundeck/issues/218)) - Job options with `require_predefined_choice = true` and `value_choices` set to a Terraform variable no longer fail validation during plan phase. The provider now correctly skips validation when values are unknown (e.g., from variables or computed values) and validates during apply phase when values are concrete. This fixes the regression introduced in v1.1.0 where users received "Missing value choices" errors even when variables were properly defined.
7+
8+
- **Fixed node_filter_exclude_query not working** - The `node_filter_exclude_query` field was not being sent to Rundeck correctly. The provider was using `excludeFilter` as the JSON field name, but Rundeck's API expects `filterExclude`. Jobs with node exclusion filters now work correctly:
9+
```hcl
10+
resource "rundeck_job" "example" {
11+
# ...
12+
node_filter_query = "tags: webserver"
13+
node_filter_exclude_query = "name: maintenance-*"
14+
}
15+
```
16+
17+
---
18+
119
## 1.1.1
220

321
**Bug Fixes**

rundeck/resource_job_framework.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ type jobRetry struct {
129129

130130
type jobNodeFilters struct {
131131
Filter string `json:"filter,omitempty"`
132-
ExcludeFilter string `json:"excludeFilter,omitempty"`
132+
FilterExclude string `json:"filterExclude,omitempty"`
133133
Dispatch *jobDispatch `json:"dispatch,omitempty"`
134134
}
135135

@@ -423,7 +423,7 @@ func (r *jobResource) ValidateConfig(ctx context.Context, req resource.ValidateC
423423
}
424424

425425
valueChoicesList, ok := valueChoicesAttr.(types.List)
426-
if !ok || valueChoicesList.IsNull() || valueChoicesList.IsUnknown() {
426+
if !ok || valueChoicesList.IsNull() {
427427
resp.Diagnostics.AddAttributeError(
428428
path.Root("option").AtListIndex(i).AtName("value_choices"),
429429
"Missing value choices",
@@ -432,6 +432,12 @@ func (r *jobResource) ValidateConfig(ctx context.Context, req resource.ValidateC
432432
continue
433433
}
434434

435+
// Skip validation if value_choices is unknown (e.g., from a variable or computed value)
436+
// Validation will occur during apply when the value is known
437+
if valueChoicesList.IsUnknown() {
438+
continue
439+
}
440+
435441
// Extract string values from the list
436442
var valueChoices []types.String
437443
diags := valueChoicesList.ElementsAs(ctx, &valueChoices, false)
@@ -442,9 +448,15 @@ func (r *jobResource) ValidateConfig(ctx context.Context, req resource.ValidateC
442448

443449
// Check for at least one non-empty value and reject empty strings
444450
hasNonEmptyValue := false
451+
hasUnknownValue := false
445452
var emptyValueIndices []int
446453
for j, choice := range valueChoices {
447-
if choice.IsNull() || choice.IsUnknown() {
454+
if choice.IsUnknown() {
455+
// If any element is unknown, skip validation entirely
456+
hasUnknownValue = true
457+
break
458+
}
459+
if choice.IsNull() {
448460
emptyValueIndices = append(emptyValueIndices, j)
449461
continue
450462
}
@@ -456,6 +468,11 @@ func (r *jobResource) ValidateConfig(ctx context.Context, req resource.ValidateC
456468
}
457469
}
458470

471+
// Skip validation if any element is unknown
472+
if hasUnknownValue {
473+
continue
474+
}
475+
459476
if !hasNonEmptyValue {
460477
resp.Diagnostics.AddAttributeError(
461478
path.Root("option").AtListIndex(i).AtName("value_choices"),
@@ -948,6 +965,8 @@ func (r *jobResource) planToJobJSON(ctx context.Context, plan *jobResourceModel)
948965
// Note: When a job has node filters (dispatches to nodes), the dispatch config
949966
// is nested INSIDE nodefilters, not at the root level.
950967
// This applies to both Open Source and Enterprise Rundeck.
968+
//
969+
// Rundeck uses "filter" for include patterns and "filterExclude" for exclude patterns.
951970
hasNodeFilters := !plan.NodeFilterQuery.IsNull() || !plan.NodeFilterExcludeQuery.IsNull()
952971
hasDispatchConfig := !plan.MaxThreadCount.IsNull() || !plan.ContinueNextNodeOnError.IsNull() ||
953972
!plan.RankOrder.IsNull() || !plan.RankAttribute.IsNull() ||
@@ -956,12 +975,12 @@ func (r *jobResource) planToJobJSON(ctx context.Context, plan *jobResourceModel)
956975
if hasNodeFilters || hasDispatchConfig {
957976
job.NodeFilters = &jobNodeFilters{}
958977

959-
// Set filter fields
960-
if !plan.NodeFilterQuery.IsNull() {
978+
// Set filter fields - Rundeck uses separate "filter" and "filterExclude" fields
979+
if !plan.NodeFilterQuery.IsNull() && plan.NodeFilterQuery.ValueString() != "" {
961980
job.NodeFilters.Filter = plan.NodeFilterQuery.ValueString()
962981
}
963-
if !plan.NodeFilterExcludeQuery.IsNull() {
964-
job.NodeFilters.ExcludeFilter = plan.NodeFilterExcludeQuery.ValueString()
982+
if !plan.NodeFilterExcludeQuery.IsNull() && plan.NodeFilterExcludeQuery.ValueString() != "" {
983+
job.NodeFilters.FilterExclude = plan.NodeFilterExcludeQuery.ValueString()
965984
}
966985

967986
// Set dispatch configuration (nested inside nodefilters)
@@ -1152,12 +1171,13 @@ func (r *jobResource) jobJSONToState(ctx context.Context, job *jobJSON, state *j
11521171
}
11531172

11541173
// Handle node filters
1174+
// Rundeck uses separate "filter" and "filterExclude" fields
11551175
if job.NodeFilters != nil {
11561176
if job.NodeFilters.Filter != "" {
11571177
state.NodeFilterQuery = types.StringValue(job.NodeFilters.Filter)
11581178
}
1159-
if job.NodeFilters.ExcludeFilter != "" {
1160-
state.NodeFilterExcludeQuery = types.StringValue(job.NodeFilters.ExcludeFilter)
1179+
if job.NodeFilters.FilterExclude != "" {
1180+
state.NodeFilterExcludeQuery = types.StringValue(job.NodeFilters.FilterExclude)
11611181
}
11621182
}
11631183

@@ -1302,12 +1322,13 @@ func (r *jobResource) jobJSONAPIToState(ctx context.Context, job *JobJSON, state
13021322
}
13031323

13041324
// Handle node filters and dispatch (nested inside nodefilters)
1325+
// Rundeck uses separate "filter" and "filterExclude" fields
13051326
if job.NodeFilters != nil {
13061327
if filter, ok := job.NodeFilters["filter"].(string); ok && filter != "" {
13071328
state.NodeFilterQuery = types.StringValue(filter)
13081329
}
1309-
if excludeFilter, ok := job.NodeFilters["excludeFilter"].(string); ok && excludeFilter != "" {
1310-
state.NodeFilterExcludeQuery = types.StringValue(excludeFilter)
1330+
if filterExclude, ok := job.NodeFilters["filterExclude"].(string); ok && filterExclude != "" {
1331+
state.NodeFilterExcludeQuery = types.StringValue(filterExclude)
13111332
}
13121333

13131334
// Dispatch is nested inside nodefilters

rundeck/resource_job_test.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,32 @@ func TestAccJob_cmd_nodefilter(t *testing.T) {
7373
})
7474
}
7575

76+
// TestAccJob_node_filter_exclude tests the node_filter_exclude_query field
77+
// This verifies that the filterExclude field is correctly sent to and read from Rundeck API
78+
func TestAccJob_node_filter_exclude(t *testing.T) {
79+
resource.Test(t, resource.TestCase{
80+
PreCheck: func() { testAccPreCheck(t) },
81+
ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(),
82+
CheckDestroy: testAccJobCheckDestroy(),
83+
Steps: []resource.TestStep{
84+
{
85+
Config: testAccJobConfig_node_filter_exclude,
86+
Check: resource.ComposeTestCheckFunc(
87+
// Test job with only exclude filter
88+
resource.TestCheckResourceAttr("rundeck_job.test_exclude_only", "name", "Test-Exclude-Only"),
89+
resource.TestCheckResourceAttr("rundeck_job.test_exclude_only", "node_filter_query", ".*"),
90+
resource.TestCheckResourceAttr("rundeck_job.test_exclude_only", "node_filter_exclude_query", "name: localhost"),
91+
// Test job with both include and exclude filters
92+
resource.TestCheckResourceAttr("rundeck_job.test_both_filters", "name", "Test-Both-Filters"),
93+
resource.TestCheckResourceAttr("rundeck_job.test_both_filters", "node_filter_query", "tags: webserver"),
94+
resource.TestCheckResourceAttr("rundeck_job.test_both_filters", "node_filter_exclude_query", "name: maintenance-*"),
95+
resource.TestCheckResourceAttr("rundeck_job.test_both_filters", "node_filter_exclude_precedence", "true"),
96+
),
97+
},
98+
},
99+
})
100+
}
101+
76102
func TestAccJob_cmd_referred_job(t *testing.T) {
77103
resource.Test(t, resource.TestCase{
78104
PreCheck: func() { testAccPreCheck(t) },
@@ -303,6 +329,34 @@ func TestAccJobOptions_option_type(t *testing.T) {
303329
})
304330
}
305331

332+
// TestAccJobOptions_value_choices_from_variable tests Issue #218 fix
333+
// Verifies that value_choices can be set from a variable without triggering
334+
// "Missing value choices" validation error during plan phase
335+
func TestAccJobOptions_value_choices_from_variable(t *testing.T) {
336+
resource.Test(t, resource.TestCase{
337+
PreCheck: func() { testAccPreCheck(t) },
338+
ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(),
339+
Steps: []resource.TestStep{
340+
{
341+
Config: testAccJobOptions_value_choices_from_variable,
342+
Check: resource.ComposeTestCheckFunc(
343+
// Verify the job was created successfully
344+
resource.TestCheckResourceAttr("rundeck_job.test", "name", "job-with-variable-choices"),
345+
resource.TestCheckResourceAttr("rundeck_job.test", "execution_enabled", "true"),
346+
// Verify the option configuration
347+
resource.TestCheckResourceAttr("rundeck_job.test", "option.0.name", "environment"),
348+
resource.TestCheckResourceAttr("rundeck_job.test", "option.0.require_predefined_choice", "true"),
349+
// Verify value_choices were properly set from the variable
350+
resource.TestCheckResourceAttr("rundeck_job.test", "option.0.value_choices.#", "3"),
351+
resource.TestCheckResourceAttr("rundeck_job.test", "option.0.value_choices.0", "dev"),
352+
resource.TestCheckResourceAttr("rundeck_job.test", "option.0.value_choices.1", "staging"),
353+
resource.TestCheckResourceAttr("rundeck_job.test", "option.0.value_choices.2", "prod"),
354+
),
355+
},
356+
},
357+
})
358+
}
359+
306360
func TestAccJob_plugins(t *testing.T) {
307361
resource.Test(t, resource.TestCase{
308362
PreCheck: func() { testAccPreCheck(t) },
@@ -671,6 +725,56 @@ resource "rundeck_job" "source_test_job" {
671725
}
672726
`
673727

728+
// Test configuration for node_filter_exclude_query
729+
const testAccJobConfig_node_filter_exclude = `
730+
resource "rundeck_project" "test_exclude" {
731+
name = "terraform-acc-test-node-filter-exclude"
732+
description = "Test project for node_filter_exclude_query"
733+
734+
resource_model_source {
735+
type = "file"
736+
config = {
737+
format = "resourceyaml"
738+
file = "/tmp/terraform-acc-tests.yaml"
739+
}
740+
}
741+
}
742+
743+
resource "rundeck_job" "test_exclude_only" {
744+
project_name = rundeck_project.test_exclude.name
745+
name = "Test-Exclude-Only"
746+
description = "Tests node_filter_exclude_query with include filter"
747+
execution_enabled = true
748+
749+
node_filter_query = ".*"
750+
node_filter_exclude_query = "name: localhost"
751+
max_thread_count = 1
752+
753+
command {
754+
description = "Echo test"
755+
shell_command = "echo 'Testing exclude filter'"
756+
}
757+
}
758+
759+
resource "rundeck_job" "test_both_filters" {
760+
project_name = rundeck_project.test_exclude.name
761+
name = "Test-Both-Filters"
762+
description = "Tests both node_filter_query and node_filter_exclude_query"
763+
execution_enabled = true
764+
765+
node_filter_query = "tags: webserver"
766+
node_filter_exclude_query = "name: maintenance-*"
767+
node_filter_exclude_precedence = true
768+
max_thread_count = 5
769+
continue_next_node_on_error = true
770+
771+
command {
772+
description = "Echo test"
773+
shell_command = "echo 'Testing both filters'"
774+
}
775+
}
776+
`
777+
674778
const testAccJobConfig_cmd_referred_job = `
675779
resource "rundeck_project" "source_test" {
676780
name = "source_project"
@@ -1049,6 +1153,48 @@ resource "rundeck_job" "test" {
10491153
}
10501154
`
10511155

1156+
// Test configuration for Issue #218 - value_choices from variable
1157+
const testAccJobOptions_value_choices_from_variable = `
1158+
variable "environment_choices" {
1159+
type = list(string)
1160+
default = ["dev", "staging", "prod"]
1161+
}
1162+
1163+
resource "rundeck_project" "test" {
1164+
name = "terraform-acc-test-job-option-var-choices"
1165+
description = "parent project for job acceptance tests"
1166+
1167+
resource_model_source {
1168+
type = "file"
1169+
config = {
1170+
format = "resourceyaml"
1171+
file = "/tmp/terraform-acc-tests.yaml"
1172+
}
1173+
}
1174+
}
1175+
1176+
resource "rundeck_job" "test" {
1177+
project_name = rundeck_project.test.name
1178+
name = "job-with-variable-choices"
1179+
description = "A job with value_choices from variable"
1180+
1181+
option {
1182+
name = "environment"
1183+
label = "Environment"
1184+
description = "Target environment"
1185+
required = true
1186+
default_value = "dev"
1187+
value_choices = var.environment_choices
1188+
require_predefined_choice = true
1189+
}
1190+
1191+
command {
1192+
description = "Echo environment"
1193+
shell_command = "echo $${option.environment}"
1194+
}
1195+
}
1196+
`
1197+
10521198
const testOchestration_maxperecent = `
10531199
resource "rundeck_project" "test" {
10541200
name = "terraform-acc-test-job"

0 commit comments

Comments
 (0)