diff --git a/builtin/providers/rundeck/provider.go b/builtin/providers/rundeck/provider.go index 71506ef1f5..8c4701a047 100644 --- a/builtin/providers/rundeck/provider.go +++ b/builtin/providers/rundeck/provider.go @@ -30,8 +30,8 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "rundeck_project": resourceRundeckProject(), - //"rundeck_job": resourceRundeckJob(), + "rundeck_project": resourceRundeckProject(), + "rundeck_job": resourceRundeckJob(), "rundeck_private_key": resourceRundeckPrivateKey(), "rundeck_public_key": resourceRundeckPublicKey(), }, diff --git a/builtin/providers/rundeck/resource_job.go b/builtin/providers/rundeck/resource_job.go new file mode 100644 index 0000000000..253414dba5 --- /dev/null +++ b/builtin/providers/rundeck/resource_job.go @@ -0,0 +1,558 @@ +package rundeck + +import ( + "fmt" + + "github.com/hashicorp/terraform/helper/schema" + + "github.com/apparentlymart/go-rundeck-api/rundeck" +) + +func resourceRundeckJob() *schema.Resource { + return &schema.Resource{ + Create: CreateJob, + Update: UpdateJob, + Delete: DeleteJob, + Exists: JobExists, + Read: ReadJob, + + Schema: map[string]*schema.Schema{ + "id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "group_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "project_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "log_level": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "INFO", + }, + + "allow_concurrent_executions": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "max_thread_count": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 1, + }, + + "continue_on_error": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "rank_order": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "ascending", + }, + + "rank_attribute": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "preserve_options_order": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "command_ordering_strategy": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "node-first", + }, + + "node_filter_query": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "node_filter_exclude_precedence": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "option": &schema.Schema{ + // This is a list because order is important when preserve_options_order is + // set. When it's not set the order is unimportant but preserved by Rundeck/ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "default_value": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "value_choices": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "value_choices_url": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "require_predefined_choice": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "validation_regex": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "required": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "allow_multiple_values": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "multi_value_delimiter": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "obscure_input": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "exposed_to_scripts": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + }, + }, + }, + + "command": &schema.Schema{ + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "shell_command": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "inline_script": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "script_file": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "script_file_args": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "job": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "group_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "run_for_each_node": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "args": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + + "step_plugin": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: resourceRundeckJobPluginResource(), + }, + + "node_step_plugin": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: resourceRundeckJobPluginResource(), + }, + }, + }, + }, + }, + } +} + +func resourceRundeckJobPluginResource() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "config": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + }, + }, + } +} + +func CreateJob(d *schema.ResourceData, meta interface{}) error { + client := meta.(*rundeck.Client) + + job, err := jobFromResourceData(d) + if err != nil { + return err + } + + jobSummary, err := client.CreateJob(job) + if err != nil { + return err + } + + d.SetId(jobSummary.ID) + d.Set("id", jobSummary.ID) + + return ReadJob(d, meta) +} + +func UpdateJob(d *schema.ResourceData, meta interface{}) error { + client := meta.(*rundeck.Client) + + job, err := jobFromResourceData(d) + if err != nil { + return err + } + + jobSummary, err := client.CreateOrUpdateJob(job) + if err != nil { + return err + } + + d.SetId(jobSummary.ID) + d.Set("id", jobSummary.ID) + + return ReadJob(d, meta) +} + +func DeleteJob(d *schema.ResourceData, meta interface{}) error { + client := meta.(*rundeck.Client) + + err := client.DeleteJob(d.Id()) + if err != nil { + return err + } + + d.SetId("") + + return nil +} + +func JobExists(d *schema.ResourceData, meta interface{}) (bool, error) { + client := meta.(*rundeck.Client) + + _, err := client.GetJob(d.Id()) + if err != nil { + if _, ok := err.(rundeck.NotFoundError); ok { + err = nil + } + return false, err + } + + return true, nil +} + +func ReadJob(d *schema.ResourceData, meta interface{}) error { + client := meta.(*rundeck.Client) + + job, err := client.GetJob(d.Id()) + if err != nil { + return err + } + + return jobToResourceData(job, d) +} + +func jobFromResourceData(d *schema.ResourceData) (*rundeck.JobDetail, error) { + job := &rundeck.JobDetail{ + ID: d.Id(), + Name: d.Get("name").(string), + GroupName: d.Get("group_name").(string), + ProjectName: d.Get("project_name").(string), + Description: d.Get("description").(string), + LogLevel: d.Get("log_level").(string), + AllowConcurrentExecutions: d.Get("allow_concurrent_executions").(bool), + Dispatch: &rundeck.JobDispatch{ + MaxThreadCount: d.Get("max_thread_count").(int), + ContinueOnError: d.Get("continue_on_error").(bool), + RankAttribute: d.Get("rank_attribute").(string), + RankOrder: d.Get("rank_order").(string), + }, + } + + sequence := &rundeck.JobCommandSequence{ + ContinueOnError: d.Get("continue_on_error").(bool), + OrderingStrategy: d.Get("command_ordering_strategy").(string), + Commands: []rundeck.JobCommand{}, + } + + commandConfigs := d.Get("command").([]interface{}) + for _, commandI := range commandConfigs { + commandMap := commandI.(map[string]interface{}) + command := rundeck.JobCommand{ + ShellCommand: commandMap["shell_command"].(string), + Script: commandMap["inline_script"].(string), + ScriptFile: commandMap["script_file"].(string), + ScriptFileArgs: commandMap["script_file_args"].(string), + } + + jobRefsI := commandMap["job"].([]interface{}) + if len(jobRefsI) > 1 { + return nil, fmt.Errorf("rundeck command may have no more than one job") + } + if len(jobRefsI) > 0 { + jobRefMap := jobRefsI[0].(map[string]interface{}) + command.Job = &rundeck.JobCommandJobRef{ + Name: jobRefMap["name"].(string), + GroupName: jobRefMap["group_name"].(string), + RunForEachNode: jobRefMap["run_for_each_node"].(bool), + Arguments: rundeck.JobCommandJobRefArguments(jobRefMap["args"].(string)), + } + } + + stepPluginsI := commandMap["step_plugin"].([]interface{}) + if len(stepPluginsI) > 1 { + return nil, fmt.Errorf("rundeck command may have no more than one step plugin") + } + if len(stepPluginsI) > 0 { + stepPluginMap := stepPluginsI[0].(map[string]interface{}) + configI := stepPluginMap["config"].(map[string]interface{}) + config := map[string]string{} + for k, v := range configI { + config[k] = v.(string) + } + command.StepPlugin = &rundeck.JobPlugin{ + Type: stepPluginMap["type"].(string), + Config: config, + } + } + + stepPluginsI = commandMap["node_step_plugin"].([]interface{}) + if len(stepPluginsI) > 1 { + return nil, fmt.Errorf("rundeck command may have no more than one node step plugin") + } + if len(stepPluginsI) > 0 { + stepPluginMap := stepPluginsI[0].(map[string]interface{}) + configI := stepPluginMap["config"].(map[string]interface{}) + config := map[string]string{} + for k, v := range configI { + config[k] = v.(string) + } + command.NodeStepPlugin = &rundeck.JobPlugin{ + Type: stepPluginMap["type"].(string), + Config: config, + } + } + + sequence.Commands = append(sequence.Commands, command) + } + job.CommandSequence = sequence + + optionConfigsI := d.Get("option").([]interface{}) + if len(optionConfigsI) > 0 { + optionsConfig := &rundeck.JobOptions{ + PreserveOrder: d.Get("preserve_options_order").(bool), + Options: []rundeck.JobOption{}, + } + for _, optionI := range optionConfigsI { + optionMap := optionI.(map[string]interface{}) + option := rundeck.JobOption{ + Name: optionMap["name"].(string), + DefaultValue: optionMap["default_value"].(string), + ValueChoices: rundeck.JobValueChoices([]string{}), + ValueChoicesURL: optionMap["value_choices_url"].(string), + RequirePredefinedChoice: optionMap["require_predefined_choice"].(bool), + ValidationRegex: optionMap["validation_regex"].(string), + Description: optionMap["description"].(string), + IsRequired: optionMap["required"].(bool), + AllowsMultipleValues: optionMap["allow_multiple_values"].(bool), + MultiValueDelimiter: optionMap["multi_value_delimiter"].(string), + ObscureInput: optionMap["obscure_input"].(bool), + ValueIsExposedToScripts: optionMap["exposed_to_scripts"].(bool), + } + + for _, iv := range optionMap["value_choices"].([]interface{}) { + option.ValueChoices = append(option.ValueChoices, iv.(string)) + } + + optionsConfig.Options = append(optionsConfig.Options, option) + } + job.OptionsConfig = optionsConfig + } + + if d.Get("node_filter_query").(string) != "" { + job.NodeFilter = &rundeck.JobNodeFilter{ + ExcludePrecedence: d.Get("node_filter_exclude_precedence").(bool), + Query: d.Get("node_filter_query").(string), + } + } + + return job, nil +} + +func jobToResourceData(job *rundeck.JobDetail, d *schema.ResourceData) error { + + d.SetId(job.ID) + d.Set("id", job.ID) + d.Set("name", job.Name) + d.Set("group_name", job.GroupName) + d.Set("project_name", job.ProjectName) + d.Set("description", job.Description) + d.Set("log_level", job.LogLevel) + d.Set("allow_concurrent_executions", job.AllowConcurrentExecutions) + if job.Dispatch != nil { + d.Set("max_thread_count", job.Dispatch.MaxThreadCount) + d.Set("continue_on_error", job.Dispatch.ContinueOnError) + d.Set("rank_attribute", job.Dispatch.RankAttribute) + d.Set("rank_order", job.Dispatch.RankOrder) + } else { + d.Set("max_thread_count", nil) + d.Set("continue_on_error", nil) + d.Set("rank_attribute", nil) + d.Set("rank_order", nil) + } + + d.Set("node_filter_query", nil) + d.Set("node_filter_exclude_precedence", nil) + if job.NodeFilter != nil { + d.Set("node_filter_query", job.NodeFilter.Query) + d.Set("node_filter_exclude_precedence", job.NodeFilter.ExcludePrecedence) + } + + optionConfigsI := []interface{}{} + if job.OptionsConfig != nil { + d.Set("preserve_options_order", job.OptionsConfig.PreserveOrder) + for _, option := range job.OptionsConfig.Options { + optionConfigI := map[string]interface{}{ + "name": option.Name, + "default_value": option.DefaultValue, + "value_choices": option.ValueChoices, + "value_choices_url": option.ValueChoicesURL, + "require_predefined_choice": option.RequirePredefinedChoice, + "validation_regex": option.ValidationRegex, + "decription": option.Description, + "required": option.IsRequired, + "allow_multiple_values": option.AllowsMultipleValues, + "multi_value_delimeter": option.MultiValueDelimiter, + "obscure_input": option.ObscureInput, + "exposed_to_scripts": option.ValueIsExposedToScripts, + } + optionConfigsI = append(optionConfigsI, optionConfigI) + } + } + d.Set("option", optionConfigsI) + + commandConfigsI := []interface{}{} + if job.CommandSequence != nil { + d.Set("command_ordering_strategy", job.CommandSequence.OrderingStrategy) + for _, command := range job.CommandSequence.Commands { + commandConfigI := map[string]interface{}{ + "shell_command": command.ShellCommand, + "inline_script": command.Script, + "script_file": command.ScriptFile, + "script_file_args": command.ScriptFileArgs, + } + + if command.Job != nil { + commandConfigI["job"] = []interface{}{ + map[string]interface{}{ + "name": command.Job.Name, + "group_name": command.Job.GroupName, + "run_for_each_node": command.Job.RunForEachNode, + "args": command.Job.Arguments, + }, + } + } + + if command.StepPlugin != nil { + commandConfigI["step_plugin"] = []interface{}{ + map[string]interface{}{ + "type": command.StepPlugin.Type, + "config": map[string]string(command.StepPlugin.Config), + }, + } + } + + if command.NodeStepPlugin != nil { + commandConfigI["node_step_plugin"] = []interface{}{ + map[string]interface{}{ + "type": command.NodeStepPlugin.Type, + "config": map[string]string(command.NodeStepPlugin.Config), + }, + } + } + + commandConfigsI = append(commandConfigsI, commandConfigI) + } + } + d.Set("command", commandConfigsI) + + return nil +} diff --git a/builtin/providers/rundeck/resource_job_test.go b/builtin/providers/rundeck/resource_job_test.go new file mode 100644 index 0000000000..66482ef0f1 --- /dev/null +++ b/builtin/providers/rundeck/resource_job_test.go @@ -0,0 +1,103 @@ +package rundeck + +import ( + "fmt" + "testing" + + "github.com/apparentlymart/go-rundeck-api/rundeck" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccJob_basic(t *testing.T) { + var job rundeck.JobDetail + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccJobCheckDestroy(&job), + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccJobConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccJobCheckExists("rundeck_job.test", &job), + func(s *terraform.State) error { + if expected := "basic-job"; job.Name != expected { + return fmt.Errorf("wrong name; expected %v, got %v", expected, job.Name) + } + return nil + }, + ), + }, + }, + }) +} + +func testAccJobCheckDestroy(job *rundeck.JobDetail) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*rundeck.Client) + _, err := client.GetJob(job.ID) + if err == nil { + return fmt.Errorf("key still exists") + } + if _, ok := err.(*rundeck.NotFoundError); !ok { + return fmt.Errorf("got something other than NotFoundError (%v) when getting key", err) + } + + return nil + } +} + +func testAccJobCheckExists(rn string, job *rundeck.JobDetail) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[rn] + if !ok { + return fmt.Errorf("resource not found: %s", rn) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("job id not set") + } + + client := testAccProvider.Meta().(*rundeck.Client) + gotJob, err := client.GetJob(rs.Primary.ID) + if err != nil { + return fmt.Errorf("error getting job details: %s", err) + } + + *job = *gotJob + + return nil + } +} + +const testAccJobConfig_basic = ` +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" + description = "A basic job" + node_filter_query = "example" + allow_concurrent_executions = 1 + max_thread_count = 1 + rank_order = "ascending" + option { + name = "foo" + default_value = "bar" + } + command { + shell_command = "echo Hello World" + } +} +`