diff --git a/builtin/providers/rundeck/resource_job.go b/builtin/providers/rundeck/resource_job.go
index f751f07da3..e3a35b062c 100644
--- a/builtin/providers/rundeck/resource_job.go
+++ b/builtin/providers/rundeck/resource_job.go
@@ -2,6 +2,7 @@ package rundeck
import (
"fmt"
+ "strings"
"github.com/hashicorp/terraform/helper/schema"
@@ -99,6 +100,11 @@ func resourceRundeckJob() *schema.Resource {
Optional: true,
},
+ "schedule": &schema.Schema{
+ Type: schema.TypeString,
+ 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/
@@ -455,6 +461,30 @@ func jobFromResourceData(d *schema.ResourceData) (*rundeck.JobDetail, error) {
}
}
+ if d.Get("schedule").(string) != "" {
+ schedule := strings.Split(d.Get("schedule").(string), " ")
+ if len(schedule) != 7 {
+ return nil, fmt.Errorf("Rundeck schedule must be formated like a cron expression, as defined here: http://www.quartz-scheduler.org/documentation/quartz-2.2.x/tutorials/tutorial-lesson-06.html")
+ }
+ job.Schedule = &rundeck.JobSchedule{
+ Time: rundeck.JobScheduleTime{
+ Seconds: schedule[0],
+ Minute: schedule[1],
+ Hour: schedule[2],
+ },
+ Month: rundeck.JobScheduleMonth{
+ Day: schedule[3],
+ Month: schedule[4],
+ },
+ WeekDay: &rundeck.JobScheduleWeekDay{
+ Day: schedule[5],
+ },
+ Year: rundeck.JobScheduleYear{
+ Year: schedule[6],
+ },
+ }
+ }
+
return job, nil
}
@@ -562,5 +592,22 @@ func jobToResourceData(job *rundeck.JobDetail, d *schema.ResourceData) error {
}
d.Set("command", commandConfigsI)
+ if job.Schedule != nil {
+ schedule := []string{}
+ schedule = append(schedule, job.Schedule.Time.Seconds)
+ schedule = append(schedule, job.Schedule.Time.Minute)
+ schedule = append(schedule, job.Schedule.Time.Hour)
+ schedule = append(schedule, job.Schedule.Month.Day)
+ schedule = append(schedule, job.Schedule.Month.Month)
+ if job.Schedule.WeekDay != nil {
+ schedule = append(schedule, job.Schedule.WeekDay.Day)
+ } else {
+ schedule = append(schedule, "*")
+ }
+ schedule = append(schedule, job.Schedule.Year.Year)
+
+ d.Set("schedule", strings.Join(schedule, " "))
+ }
+
return nil
}
diff --git a/builtin/providers/rundeck/resource_job_test.go b/builtin/providers/rundeck/resource_job_test.go
index 66482ef0f1..182f4a3cfa 100644
--- a/builtin/providers/rundeck/resource_job_test.go
+++ b/builtin/providers/rundeck/resource_job_test.go
@@ -92,6 +92,7 @@ resource "rundeck_job" "test" {
allow_concurrent_executions = 1
max_thread_count = 1
rank_order = "ascending"
+ schedule = "0 0 12 * * * *"
option {
name = "foo"
default_value = "bar"
diff --git a/vendor/github.com/apparentlymart/go-rundeck-api/README.md b/vendor/github.com/apparentlymart/go-rundeck-api/README.md
new file mode 100644
index 0000000000..18abfb3d33
--- /dev/null
+++ b/vendor/github.com/apparentlymart/go-rundeck-api/README.md
@@ -0,0 +1,9 @@
+# go-rundeck-api
+
+This is a Go client for the Rundeck HTTP API. It was primarily developed to back the Rundeck provider in [Terraform](https://terraform.io), but can be used standalone too.
+
+It should ``go install`` just like any other Go package:
+
+* ``go install github.com/apparentlymart/go-rundeck-api/rundeck``
+
+For reference documentation, see [godoc](https://godoc.org/github.com/apparentlymart/go-rundeck-api/rundeck).
diff --git a/vendor/github.com/apparentlymart/go-rundeck-api/rundeck/job.go b/vendor/github.com/apparentlymart/go-rundeck-api/rundeck/job.go
index ca04ac35a1..275db85016 100644
--- a/vendor/github.com/apparentlymart/go-rundeck-api/rundeck/job.go
+++ b/vendor/github.com/apparentlymart/go-rundeck-api/rundeck/job.go
@@ -30,12 +30,57 @@ type JobDetail struct {
GroupName string `xml:"group,omitempty"`
ProjectName string `xml:"context>project,omitempty"`
OptionsConfig *JobOptions `xml:"context>options,omitempty"`
- Description string `xml:"description,omitempty"`
+ Description string `xml:"description"`
LogLevel string `xml:"loglevel,omitempty"`
- AllowConcurrentExecutions bool `xml:"multipleExecutions"`
- Dispatch *JobDispatch `xml:"dispatch"`
+ AllowConcurrentExecutions bool `xml:"multipleExecutions,omitempty"`
+ Dispatch *JobDispatch `xml:"dispatch,omitempty"`
CommandSequence *JobCommandSequence `xml:"sequence,omitempty"`
+ Timeout string `xml:"timeout,omitempty"`
+ Retry string `xml:"retry,omitempty"`
NodeFilter *JobNodeFilter `xml:"nodefilters,omitempty"`
+
+ /* If Dispatch is enabled, nodesSelectedByDefault is always present with true/false.
+ * by this reason omitempty cannot be present.
+ * This has to be handle by the user.
+ */
+ NodesSelectedByDefault bool `xml:"nodesSelectedByDefault"`
+ Schedule *JobSchedule `xml:"schedule,omitempty"`
+}
+
+type JobSchedule struct {
+ XMLName xml.Name `xml:"schedule"`
+ DayOfMonth *JobScheduleDayOfMonth `xml:"dayofmonth,omitempty"`
+ Time JobScheduleTime `xml:"time"`
+ Month JobScheduleMonth `xml:"month"`
+ WeekDay *JobScheduleWeekDay `xml:"weekday,omitempty"`
+ Year JobScheduleYear `xml:"year"`
+}
+
+type JobScheduleDayOfMonth struct {
+ XMLName xml.Name `xml:"dayofmonth"`
+}
+
+type JobScheduleMonth struct {
+ XMLName xml.Name `xml:"month"`
+ Day string `xml:"day,attr,omitempty"`
+ Month string `xml:"month,attr"`
+}
+
+type JobScheduleYear struct {
+ XMLName xml.Name `xml:"year"`
+ Year string `xml:"year,attr"`
+}
+
+type JobScheduleWeekDay struct {
+ XMLName xml.Name `xml:"weekday"`
+ Day string `xml:"day,attr"`
+}
+
+type JobScheduleTime struct {
+ XMLName xml.Name `xml:"time"`
+ Hour string `xml:"hour,attr"`
+ Minute string `xml:"minute,attr"`
+ Seconds string `xml:"seconds,attr"`
}
type jobDetailList struct {
@@ -53,48 +98,52 @@ type JobOptions struct {
type JobOption struct {
XMLName xml.Name `xml:"option"`
- // The name of the option, which can be used to interpolate its value
- // into job commands.
- Name string `xml:"name,attr,omitempty"`
-
- // The default value of the option.
- DefaultValue string `xml:"value,attr,omitempty"`
-
- // A sequence of predefined choices for this option. Mutually exclusive with ValueChoicesURL.
- ValueChoices JobValueChoices `xml:"values,attr"`
-
- // A URL from which the predefined choices for this option will be retrieved.
- // Mutually exclusive with ValueChoices
- ValueChoicesURL string `xml:"valuesUrl,attr,omitempty"`
+ // If AllowsMultipleChoices is set, the string that will be used to delimit the multiple
+ // chosen options.
+ MultiValueDelimiter string `xml:"delimiter,attr,omitempty"`
// If set, Rundeck will reject values that are not in the set of predefined choices.
RequirePredefinedChoice bool `xml:"enforcedvalues,attr,omitempty"`
+ // When either ValueChoices or ValueChoicesURL is set, controls whether more than one
+ // choice may be selected as the value.
+ AllowsMultipleValues bool `xml:"multivalued,attr,omitempty"`
+
+ // The name of the option, which can be used to interpolate its value
+ // into job commands.
+ Name string `xml:"name,attr,omitempty"`
+
// Regular expression to be used to validate the option value.
ValidationRegex string `xml:"regex,attr,omitempty"`
- // Description of the value to be shown in the Rundeck UI.
- Description string `xml:"description,omitempty"`
-
// If set, Rundeck requires a value to be set for this option.
IsRequired bool `xml:"required,attr,omitempty"`
- // When either ValueChoices or ValueChoicesURL is set, controls whether more than one
- // choice may be selected as the value.
- AllowsMultipleValues bool `xml:"multivalued,attr,omitempty"`
-
- // If AllowsMultipleChoices is set, the string that will be used to delimit the multiple
- // chosen options.
- MultiValueDelimiter string `xml:"delimeter,attr,omitempty"`
-
// If set, the input for this field will be obscured in the UI. Useful for passwords
// and other secrets.
ObscureInput bool `xml:"secure,attr,omitempty"`
+ // If ObscureInput is set, StoragePath can be used to point out credentials.
+ StoragePath string `xml:"storagePath,attr,omitempty"`
+
+ // The default value of the option.
+ DefaultValue string `xml:"value,attr,omitempty"`
+
// If set, the value can be accessed from scripts.
ValueIsExposedToScripts bool `xml:"valueExposed,attr,omitempty"`
+
+ // A sequence of predefined choices for this option. Mutually exclusive with ValueChoicesURL.
+ ValueChoices JobValueChoices `xml:"values,attr"`
+
+ // A URL from which the predefined choices for this option will be retrieved.
+ // Mutually exclusive with ValueChoices
+ ValueChoicesURL string `xml:"valuesUrl,attr,omitempty"`
+
+ // Description of the value to be shown in the Rundeck UI.
+ Description string `xml:"description,omitempty"`
}
+
// JobValueChoices is a specialization of []string representing a sequence of predefined values
// for a job option.
type JobValueChoices []string
@@ -112,6 +161,9 @@ type JobCommandSequence struct {
// Sequence of commands to run in the sequence.
Commands []JobCommand `xml:"command"`
+
+ // Description
+ Description string `xml:"description,omitempty"`
}
// JobCommand describes a particular command to run within the sequence of commands on a job.
@@ -120,9 +172,21 @@ type JobCommandSequence struct {
type JobCommand struct {
XMLName xml.Name
+ // If the Workflow keepgoing is false, this allows the Workflow to continue when the Error Handler is successful.
+ ContinueOnError bool `xml:"keepgoingOnSuccess,attr,omitempty"`
+
+ // Description
+ Description string `xml:"description,omitempty"`
+
+ // On error:
+ ErrorHandler *JobCommand `xml:"errorhandler,omitempty"`
+
// A literal shell command to run.
ShellCommand string `xml:"exec,omitempty"`
+ // Add extension to the temporary filename.
+ FileExtension string `xml:"fileExtension,omitempty"`
+
// An inline program to run. This will be written to disk and executed, so if it is
// a shell script it should have an appropriate #! line.
Script string `xml:"script,omitempty"`
@@ -133,6 +197,9 @@ type JobCommand struct {
// When ScriptFile is set, the arguments to provide to the script when executing it.
ScriptFileArgs string `xml:"scriptargs,omitempty"`
+ // ScriptInterpreter is used to execute (Script)File with.
+ ScriptInterpreter *JobCommandScriptInterpreter `xml:"scriptinterpreter,omitempty"`
+
// A reference to another job to run as this command.
Job *JobCommandJobRef `xml:"jobref"`
@@ -143,12 +210,20 @@ type JobCommand struct {
NodeStepPlugin *JobPlugin `xml:"node-step-plugin"`
}
+// (Inline) Script interpreter
+type JobCommandScriptInterpreter struct {
+ XMLName xml.Name `xml:"scriptinterpreter"`
+ InvocationString string `xml:",chardata"`
+ ArgsQuoted bool `xml:"argsquoted,attr,omitempty"`
+}
+
// JobCommandJobRef is a reference to another job that will run as one of the commands of a job.
type JobCommandJobRef struct {
XMLName xml.Name `xml:"jobref"`
Name string `xml:"name,attr"`
GroupName string `xml:"group,attr"`
RunForEachNode bool `xml:"nodeStep,attr"`
+ NodeFilter *JobNodeFilter `xml:"nodefilters,omitempty"`
Arguments JobCommandJobRefArguments `xml:"arg"`
}
diff --git a/vendor/github.com/apparentlymart/go-rundeck-api/rundeck/job_test.go b/vendor/github.com/apparentlymart/go-rundeck-api/rundeck/job_test.go
new file mode 100644
index 0000000000..aa54351b3d
--- /dev/null
+++ b/vendor/github.com/apparentlymart/go-rundeck-api/rundeck/job_test.go
@@ -0,0 +1,314 @@
+package rundeck
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestUnmarshalJobDetail(t *testing.T) {
+ testUnmarshalXML(t, []unmarshalTest{
+ unmarshalTest{
+ "with-config",
+ `bazascending`,
+ &JobDetail{},
+ func (rv interface {}) error {
+ v := rv.(*JobDetail)
+ if v.ID != "baz" {
+ return fmt.Errorf("got ID %s, but expecting baz", v.ID)
+ }
+ if v.Dispatch.RankOrder != "ascending" {
+ return fmt.Errorf("Dispatch.RankOrder = \"%v\", but expecting \"ascending\"", v.Dispatch.RankOrder)
+ }
+ return nil
+ },
+ },
+ unmarshalTest{
+ "with-empty-config",
+ ``,
+ &JobPlugin{},
+ func (rv interface {}) error {
+ v := rv.(*JobPlugin)
+ if v.Type != "foo-plugin" {
+ return fmt.Errorf("got Type %s, but expecting foo-plugin", v.Type)
+ }
+ if len(v.Config) != 0 {
+ return fmt.Errorf("got %i Config values, but expecting 0", len(v.Config))
+ }
+ return nil
+ },
+ },
+ })
+}
+
+func TestMarshalJobPlugin(t *testing.T) {
+ testMarshalXML(t, []marshalTest{
+ marshalTest{
+ "with-config",
+ JobPlugin{
+ Type: "foo-plugin",
+ Config: map[string]string{
+ "woo": "foo",
+ "bar": "baz",
+ },
+ },
+ ``,
+ },
+ marshalTest{
+ "with-empty-config",
+ JobPlugin{
+ Type: "foo-plugin",
+ Config: map[string]string{},
+ },
+ ``,
+ },
+ marshalTest{
+ "with-zero-value-config",
+ JobPlugin{
+ Type: "foo-plugin",
+ },
+ ``,
+ },
+ })
+}
+
+func TestUnmarshalJobPlugin(t *testing.T) {
+ testUnmarshalXML(t, []unmarshalTest{
+ unmarshalTest{
+ "with-config",
+ ``,
+ &JobPlugin{},
+ func (rv interface {}) error {
+ v := rv.(*JobPlugin)
+ if v.Type != "foo-plugin" {
+ return fmt.Errorf("got Type %s, but expecting foo-plugin", v.Type)
+ }
+ if len(v.Config) != 2 {
+ return fmt.Errorf("got %v Config values, but expecting 2", len(v.Config))
+ }
+ if v.Config["woo"] != "foo" {
+ return fmt.Errorf("Config[\"woo\"] = \"%s\", but expecting \"foo\"", v.Config["woo"])
+ }
+ if v.Config["bar"] != "baz" {
+ return fmt.Errorf("Config[\"bar\"] = \"%s\", but expecting \"baz\"", v.Config["bar"])
+ }
+ return nil
+ },
+ },
+ unmarshalTest{
+ "with-empty-config",
+ ``,
+ &JobPlugin{},
+ func (rv interface {}) error {
+ v := rv.(*JobPlugin)
+ if v.Type != "foo-plugin" {
+ return fmt.Errorf("got Type %s, but expecting foo-plugin", v.Type)
+ }
+ if len(v.Config) != 0 {
+ return fmt.Errorf("got %i Config values, but expecting 0", len(v.Config))
+ }
+ return nil
+ },
+ },
+ })
+}
+
+func TestMarshalJobCommand(t *testing.T) {
+ testMarshalXML(t, []marshalTest{
+ marshalTest{
+ "with-shell",
+ JobCommand{
+ ShellCommand: "command",
+ },
+ `command`,
+ },
+ marshalTest{
+ "with-script",
+ JobCommand{
+ Script: "script",
+ },
+ ``,
+ },
+ marshalTest{
+ "with-script-interpreter",
+ JobCommand{
+ FileExtension: "sh",
+ Script: "Hello World!",
+ ScriptInterpreter: &JobCommandScriptInterpreter{
+ InvocationString: "sudo",
+ },
+ },
+ `shsudo`,
+ },
+ })
+}
+
+func TestUnmarshalJobCommand(t *testing.T) {
+ testUnmarshalXML(t, []unmarshalTest{
+ unmarshalTest{
+ "with-shell",
+ `command`,
+ &JobCommand{},
+ func (rv interface {}) error {
+ v := rv.(*JobCommand)
+ if v.ShellCommand != "command" {
+ return fmt.Errorf("got ShellCommand %s, but expecting command", v.ShellCommand)
+ }
+ return nil
+ },
+ },
+ unmarshalTest{
+ "with-script",
+ ``,
+ &JobCommand{},
+ func (rv interface {}) error {
+ v := rv.(*JobCommand)
+ if v.Script != "script" {
+ return fmt.Errorf("got Script %s, but expecting script", v.Script)
+ }
+ return nil
+ },
+ },
+ unmarshalTest{
+ "with-script-interpreter",
+ `shsudo`,
+ &JobCommand{},
+ func (rv interface {}) error {
+ v := rv.(*JobCommand)
+ if v.FileExtension != "sh" {
+ return fmt.Errorf("got FileExtension %s, but expecting sh", v.FileExtension)
+ }
+ if v.Script != "Hello World!" {
+ return fmt.Errorf("got Script %s, but expecting Hello World!", v.Script)
+ }
+ if v.ScriptInterpreter == nil {
+ return fmt.Errorf("got %s, but expecting not nil", v.ScriptInterpreter)
+ }
+ if v.ScriptInterpreter.InvocationString != "sudo" {
+ return fmt.Errorf("got InvocationString %s, but expecting sudo", v.ScriptInterpreter.InvocationString)
+ }
+ return nil
+ },
+ },
+ })
+}
+
+func TestMarshalScriptInterpreter(t *testing.T) {
+ testMarshalXML(t, []marshalTest{
+ marshalTest{
+ "with-script-interpreter",
+ JobCommandScriptInterpreter{
+ InvocationString: "sudo",
+ },
+ `sudo`,
+ },
+ marshalTest{
+ "with-script-interpreter-quoted",
+ JobCommandScriptInterpreter{
+ ArgsQuoted: true,
+ InvocationString: "sudo",
+ },
+ `sudo`,
+ },
+ })
+}
+
+func TestUnmarshalScriptInterpreter(t *testing.T) {
+ testUnmarshalXML(t, []unmarshalTest{
+ unmarshalTest{
+ "with-script-interpreter",
+ `sudo`,
+ &JobCommandScriptInterpreter{},
+ func (rv interface {}) error {
+ v := rv.(*JobCommandScriptInterpreter)
+ if v.InvocationString != "sudo" {
+ return fmt.Errorf("got InvocationString %s, but expecting sudo", v.InvocationString)
+ }
+ if v.ArgsQuoted {
+ return fmt.Errorf("got ArgsQuoted %s, but expecting false", v.ArgsQuoted)
+ }
+ return nil
+ },
+ },
+ unmarshalTest{
+ "with-script-interpreter-quoted",
+ `sudo`,
+ &JobCommandScriptInterpreter{},
+ func (rv interface {}) error {
+ v := rv.(*JobCommandScriptInterpreter)
+ if v.InvocationString != "sudo" {
+ return fmt.Errorf("got InvocationString %s, but expecting sudo", v.InvocationString)
+ }
+ if ! v.ArgsQuoted {
+ return fmt.Errorf("got ArgsQuoted %s, but expecting true", v.ArgsQuoted)
+ }
+ return nil
+ },
+ },
+ })
+}
+
+func TestMarshalErrorHanlder(t *testing.T) {
+ testMarshalXML(t, []marshalTest{
+ marshalTest{
+ "with-errorhandler",
+ JobCommandSequence{
+ ContinueOnError: true,
+ OrderingStrategy: "step-first",
+ Commands: []JobCommand{
+ JobCommand{
+ Script: "inline_script",
+ ErrorHandler: &JobCommand{
+ ContinueOnError: true,
+ Script: "error_script",
+ },
+ },
+ },
+ },
+ ``,
+ },
+ })
+}
+
+
+func TestMarshalJobOption(t *testing.T) {
+ testMarshalXML(t, []marshalTest{
+ marshalTest{
+ "with-option-basic",
+ JobOption{
+ Name: "basic",
+ },
+ ``,
+ },
+ marshalTest{
+ "with-option-multivalued",
+ JobOption{
+ Name: "Multivalued",
+ MultiValueDelimiter: "|",
+ RequirePredefinedChoice: true,
+ AllowsMultipleValues: true,
+ IsRequired: true,
+ ValueChoices: JobValueChoices([]string{"myValues"}),
+ },
+ ``,
+ },
+ marshalTest{
+ "with-all-attributes",
+ JobOption{
+ Name: "advanced",
+ MultiValueDelimiter: "|",
+ RequirePredefinedChoice: true,
+ AllowsMultipleValues: true,
+ ValidationRegex: ".+",
+ IsRequired: true,
+ ObscureInput: true,
+ StoragePath: "myKey",
+ DefaultValue: "myValue",
+ ValueIsExposedToScripts: true,
+ ValueChoices: JobValueChoices([]string{"myValues"}),
+ ValueChoicesURL: "myValuesUrl",
+ },
+ ``,
+ },
+ })
+}
+
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 07d7e90da1..d919c25688 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -365,6 +365,12 @@
"path": "github.com/apparentlymart/go-grafana-api",
"revision": "d49f95c81c580a4e7a15244b9b12dce8f60750f4"
},
+ {
+ "checksumSHA1": "+2yCNqbcf7VcavAptooQReTGiHY=",
+ "path": "github.com/apparentlymart/go-rundeck-api",
+ "revision": "f6af74d34d1ef69a511c59173876fc1174c11f0d",
+ "revisionTime": "2016-08-26T14:30:32Z"
+ },
{
"comment": "v0.0.1-1-g43fcd8f",
"path": "github.com/apparentlymart/go-rundeck-api/rundeck",