diff --git a/rundeck/job.go b/rundeck/job.go index 08ae5507b..fbd74a913 100644 --- a/rundeck/job.go +++ b/rundeck/job.go @@ -320,8 +320,10 @@ type JobNodeFilter struct { // JobOrchestratorConfig Contains the options for the Job Orchestrators type JobOrchestratorConfig struct { - Count int `xml:"count,omitempty"` - Percent int `xml:"percent,omitempty"` + Count int `xml:"count,omitempty"` + Percent int `xml:"percent,omitempty"` + Attribute string `xml:"attribute,omitempty"` + Sort string `xml:"sort,omitempty"` } // JobOrchestrator describes how to schedule the jobs, in what order, and on how many nodes diff --git a/rundeck/resource_job.go b/rundeck/resource_job.go index 882acfbb4..0302b5a37 100644 --- a/rundeck/resource_job.go +++ b/rundeck/resource_job.go @@ -125,7 +125,10 @@ func resourceRundeckJob() *schema.Resource { Type: schema.TypeBool, Optional: true, }, - + "timeout": { + Type: schema.TypeString, + Optional: true, + }, "orchestrator": { Type: schema.TypeList, Optional: true, @@ -134,7 +137,7 @@ func resourceRundeckJob() *schema.Resource { "type": { Type: schema.TypeString, Required: true, - Description: "Option of `subset`, `tiered`, `percentage`", + Description: "Option of `subset`, `rankTiered`, `maxPercentage`, `orchestrator-highest-lowest-attribute`", }, "count": { Type: schema.TypeInt, @@ -146,6 +149,16 @@ func resourceRundeckJob() *schema.Resource { Optional: true, Description: "Value for the maxPercentage orchestrator", }, + "attribute": { + Type: schema.TypeString, + Optional: true, + Description: "The Node Attribute that shoud be used to rank nodes in High/Low Orchestrator.", + }, + "sort": { + Type: schema.TypeString, + Optional: true, + Description: "Option of `highest` or `lowest` for High/Low Orchestrator", + }, }, }, }, @@ -656,13 +669,36 @@ func jobFromResourceData(d *schema.ResourceData) (*JobDetail, error) { Type: orchMap["type"].(string), Config: JobOrchestratorConfig{}, } - orchCount := orchMap["count"] - if orchCount != nil { - job.Orchestrator.Config.Count = orchCount.(int) + orchType := orchMap["type"].(string) + if orchType == "orchestrator-highest-lowest-attribute" { + orchAttr := orchMap["attribute"] + if orchAttr != nil { + job.Orchestrator.Config.Attribute = orchAttr.(string) + } else { + return nil, fmt.Errorf("high Low Orchestrator must include an attribute to sort against") + } + orchSort := orchMap["sort"] + if orchSort != nil { + job.Orchestrator.Config.Sort = orchSort.(string) + } else { + return nil, fmt.Errorf("high low orchestrator must include sort direction of `high` or `low`") + } } - orchPct := orchMap["percent"] - if orchPct != nil { - job.Orchestrator.Config.Percent = orchPct.(int) + if orchType == "subset" { + orchCount := orchMap["count"] + if orchCount != nil { + job.Orchestrator.Config.Count = orchCount.(int) + } else { + return nil, fmt.Errorf("subset Orchestrator requires count setting") + } + } + if orchType == "maxPercentage" { + orchPct := orchMap["percent"] + if orchPct != nil { + job.Orchestrator.Config.Percent = orchPct.(int) + } else { + return nil, fmt.Errorf("max Percentage Orchestrator requires a percent integer configuration") + } } } @@ -726,11 +762,11 @@ func jobFromResourceData(d *schema.ResourceData) (*JobDetail, error) { ValueIsExposedToScripts: optionMap["exposed_to_scripts"].(bool), StoragePath: optionMap["storage_path"].(string), } - if option.StoragePath != "" && option.ObscureInput == false { - return nil, fmt.Errorf("Argument \"obscure_input\" must be set to `true` when \"storage_path\" is not empty.") + if option.StoragePath != "" && !option.ObscureInput { + return nil, fmt.Errorf("argument \"obscure_input\" must be set to `true` when \"storage_path\" is not empty") } - if option.ValueIsExposedToScripts && option.ObscureInput == false { - return nil, fmt.Errorf("Argument \"obscure_input\" must be set to `true` when \"exposed_to_scripts\" is set to true.") + if option.ValueIsExposedToScripts && !option.ObscureInput { + return nil, fmt.Errorf("argument \"obscure_input\" must be set to `true` when \"exposed_to_scripts\" is set to true") } for _, iv := range optionMap["value_choices"].([]interface{}) { diff --git a/rundeck/resource_job_test.go b/rundeck/resource_job_test.go index 984d0e0ee..20fec0458 100644 --- a/rundeck/resource_job_test.go +++ b/rundeck/resource_job_test.go @@ -69,7 +69,8 @@ func TestAccJob_cmd_nodefilter(t *testing.T) { }, }) } -func TestAccJob_cmd_orchestrator(t *testing.T) { + +func TestOchestrator_high_low(t *testing.T) { var job JobDetail resource.Test(t, resource.TestCase{ @@ -78,18 +79,70 @@ func TestAccJob_cmd_orchestrator(t *testing.T) { CheckDestroy: testAccJobCheckDestroy(&job), Steps: []resource.TestStep{ { - Config: testAccJobConfig_cmd_nodefilter, + Config: testOchestration_high_low, Check: resource.ComposeTestCheckFunc( testAccJobCheckExists("rundeck_job.test", &job), func(s *terraform.State) error { - if expected := "subset"; job.Orchestrator.Type != expected { - return fmt.Errorf("wrong subset; expected %v, got %v", expected, job.Orchestrator.Type) + if expected := "basic-job-with-node-filter"; job.Name != expected { + return fmt.Errorf("wrong name; expected %v, got %v", expected, job.Name) + } + if expected := "name: tacobell"; job.CommandSequence.Commands[0].Job.NodeFilter.Query != expected { + return fmt.Errorf("failed to set job node filter; expected %v, got %v", expected, job.CommandSequence.Commands[0].Job.NodeFilter.Query) } + return nil + }, + ), + }, + }, + }) +} + +func TestOchestrator_max_percent(t *testing.T) { + var job JobDetail - if expected := 1; job.Orchestrator.Config.Count != expected { - return fmt.Errorf("wrong subset count; expected %v, got %v", expected, job.Orchestrator.Config.Count) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccJobCheckDestroy(&job), + Steps: []resource.TestStep{ + { + Config: testOchestration_maxperecent, + Check: resource.ComposeTestCheckFunc( + testAccJobCheckExists("rundeck_job.test", &job), + func(s *terraform.State) error { + if expected := "basic-job-with-node-filter"; job.Name != expected { + return fmt.Errorf("wrong name; expected %v, got %v", expected, job.Name) } + if expected := "name: tacobell"; job.CommandSequence.Commands[0].Job.NodeFilter.Query != expected { + return fmt.Errorf("failed to set job node filter; expected %v, got %v", expected, job.CommandSequence.Commands[0].Job.NodeFilter.Query) + } + return nil + }, + ), + }, + }, + }) +} +func TestOchestrator_rankTiered(t *testing.T) { + var job JobDetail + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccJobCheckDestroy(&job), + Steps: []resource.TestStep{ + { + Config: testOchestration_rank_tiered, + Check: resource.ComposeTestCheckFunc( + testAccJobCheckExists("rundeck_job.test", &job), + func(s *terraform.State) error { + if expected := "basic-job-with-node-filter"; job.Name != expected { + return fmt.Errorf("wrong name; expected %v, got %v", expected, job.Name) + } + if expected := "name: tacobell"; job.CommandSequence.Commands[0].Job.NodeFilter.Query != expected { + return fmt.Errorf("failed to set job node filter; expected %v, got %v", expected, job.CommandSequence.Commands[0].Job.NodeFilter.Query) + } return nil }, ), @@ -499,3 +552,159 @@ resource "rundeck_job" "test" { } } ` + +const testOchestration_maxperecent = ` +resource "rundeck_project" "test" { + name = "terraform-acc-test-job" + description = "parent project for job acceptance tests" + + resource_model_source { + type = "file" + config = { + format = "resourcexml" + file = "/tmp/terraform-acc-tests.xml" + } + } + } + resource "rundeck_job" "test" { + project_name = "${rundeck_project.test.name}" + name = "orchestrator-MaxPercent" + description = "A basic job" + execution_enabled = true + node_filter_query = "example" + allow_concurrent_executions = true + nodes_selected_by_default = false + max_thread_count = 1 + rank_order = "ascending" + schedule = "0 0 12 * * * *" + schedule_enabled = true + option { + name = "foo" + default_value = "bar" + } + orchestrator { + type = "maxPercentage" + percent = 10 + } + command { + job { + name = "Other Job Name" + run_for_each_node = true + node_filters { + filter = "name: tacobell" + } + } + description = "Prints Hello World" + } + notification { + type = "on_success" + email { + recipients = ["foo@foo.bar"] + } + } + } + ` + +const testOchestration_high_low = ` +resource "rundeck_project" "test" { + name = "terraform-acc-test-job" + description = "parent project for job acceptance tests" + + resource_model_source { + type = "file" + config = { + format = "resourcexml" + file = "/tmp/terraform-acc-tests.xml" + } + } + } + resource "rundeck_job" "test" { + project_name = "${rundeck_project.test.name}" + name = "orchestrator-High-Low" + description = "A basic job" + execution_enabled = true + node_filter_query = "example" + allow_concurrent_executions = true + nodes_selected_by_default = false + max_thread_count = 1 + rank_order = "ascending" + schedule = "0 0 12 * * * *" + schedule_enabled = true + option { + name = "foo" + default_value = "bar" + } + orchestrator { + type = "orchestrator-highest-lowest-attribute" + sort = "highest" + attribute = "my-attribute" + } + command { + job { + name = "Other Job Name" + run_for_each_node = true + node_filters { + filter = "name: tacobell" + } + } + description = "Prints Hello World" + } + notification { + type = "on_success" + email { + recipients = ["foo@foo.bar"] + } + } + } + ` + +const testOchestration_rank_tiered = ` +resource "rundeck_project" "test" { + name = "terraform-acc-test-job" + description = "parent project for job acceptance tests" + + resource_model_source { + type = "file" + config = { + format = "resourcexml" + file = "/tmp/terraform-acc-tests.xml" + } + } + } + resource "rundeck_job" "test" { + project_name = "${rundeck_project.test.name}" + name = "basic-job-with-node-filter" + description = "A basic job" + execution_enabled = true + node_filter_query = "example" + allow_concurrent_executions = true + nodes_selected_by_default = false + max_thread_count = 1 + rank_order = "ascending" + schedule = "0 0 12 * * * *" + schedule_enabled = true + option { + name = "foo" + default_value = "bar" + } + orchestrator { + type = "rankTiered" + } + command { + job { + name = "Other Job Name" + run_for_each_node = true + node_filters { + filter = "name: tacobell" + } + } + description = "Prints Hello World" + } + notification { + type = "on_success" + email { + recipients = ["foo@foo.bar"] + } + } + } + ` diff --git a/terraform-provider-rundeck b/terraform-provider-rundeck new file mode 100755 index 000000000..938cdee07 Binary files /dev/null and b/terraform-provider-rundeck differ diff --git a/website/docs/r/job.html.md b/website/docs/r/job.html.md index aa1ed700b..0ded08dd4 100644 --- a/website/docs/r/job.html.md +++ b/website/docs/r/job.html.md @@ -56,7 +56,12 @@ The following arguments are supported: * `schedule` - (Optional) The job's schedule in Quartz schedule cron format. Similar to unix crontab, but with seven fields instead of five: Second Minute Hour Day-of-Month Month Day-of-Week Year -* `orchestrator` - (Optional) The orchestrator for the job, described below. +* `orchestrator` - (Optional) The orchestrator for the job, described below and [here](https://docs.rundeck.com/docs/manual/orchestrator-plugins/bundled.html) + - `type`: Must be one of `subset`, `rankTiered`, `maxPercentage`, `orchestrator-highest-lowest-attribute` + - `count`: Required when types is `subset`. Selects that max number of the target nodes at random to process. + - `percent`: Required when type is `maxPercentage`. Used to determine max percentage of the nodes to process at once. + - `attribute`: Required when type is `orchestrator-highest-lowest-attribute`. The node attribute to use for sorting. + - `sort`:Required when type is `orchestrator-highest-lowest-attribute`. Values accepted are `highest` or `lowest`. * `schedule_enabled` - (Optional) Sets the job schedule to be enabled or disabled. Defaults to `true`.