Merge pull request #2412 from apparentlymart/rundeck

Rundeck Provider
This commit is contained in:
Paul Hinze 2015-09-04 13:38:20 -05:00
commit 0a64779ee5
19 changed files with 2127 additions and 0 deletions

View File

@ -0,0 +1,12 @@
package main
import (
"github.com/hashicorp/terraform/builtin/providers/rundeck"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: rundeck.Provider,
})
}

View File

@ -0,0 +1,51 @@
package rundeck
import (
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
"github.com/apparentlymart/go-rundeck-api/rundeck"
)
func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"url": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("RUNDECK_URL", nil),
Description: "URL of the root of the target Rundeck server.",
},
"auth_token": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("RUNDECK_AUTH_TOKEN", nil),
Description: "Auth token to use with the Rundeck API.",
},
"allow_unverified_ssl": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Description: "If set, the Rundeck client will permit unverifiable SSL certificates.",
},
},
ResourcesMap: map[string]*schema.Resource{
"rundeck_project": resourceRundeckProject(),
"rundeck_job": resourceRundeckJob(),
"rundeck_private_key": resourceRundeckPrivateKey(),
"rundeck_public_key": resourceRundeckPublicKey(),
},
ConfigureFunc: providerConfigure,
}
}
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := &rundeck.ClientConfig{
BaseURL: d.Get("url").(string),
AuthToken: d.Get("auth_token").(string),
AllowUnverifiedSSL: d.Get("allow_unverified_ssl").(bool),
}
return rundeck.NewClient(config)
}

View File

@ -0,0 +1,98 @@
package rundeck
import (
"os"
"testing"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
// To run these acceptance tests, you will need a Rundeck server.
// An easy way to get one is to use Rundeck's "Anvils" demo, which includes a Vagrantfile
// to get it running easily:
// https://github.com/rundeck/anvils-demo
// The anvils demo ships with some example security policies that don't have enough access to
// run the tests, so you need to either modify one of the stock users to have full access or
// create a new user with such access. The following block is an example that gives the
// 'admin' user and API clients open access.
// In the anvils demo the admin password is "admin" by default.
// Place the contents of the following comment in /etc/rundeck/terraform-test.aclpolicy
/*
description: Admin, all access.
context:
project: '.*' # all projects
for:
resource:
- allow: '*' # allow read/create all kinds
adhoc:
- allow: '*' # allow read/running/killing adhoc jobs
job:
- allow: '*' # allow read/write/delete/run/kill of all jobs
node:
- allow: '*' # allow read/run for all nodes
by:
group: admin
---
description: Admin, all access.
context:
application: 'rundeck'
for:
resource:
- allow: '*' # allow create of projects
project:
- allow: '*' # allow view/admin of all projects
storage:
- allow: '*' # allow read/create/update/delete for all /keys/* storage content
by:
group: admin
---
description: Admin API, all access.
context:
application: 'rundeck'
for:
resource:
- allow: '*' # allow create of projects
project:
- allow: '*' # allow view/admin of all projects
storage:
- allow: '*' # allow read/create/update/delete for all /keys/* storage content
by:
group: api_token_group
*/
// Once you've got a user set up, put that user's API auth token in the RUNDECK_AUTH_TOKEN
// environment variable, and put the URL of the Rundeck home page in the RUNDECK_URL variable.
// If you're using the Anvils demo in its default configuration, you can find or generate an API
// token at http://192.168.50.2:4440/user/profile once you've logged in, and RUNDECK_URL will
// be http://192.168.50.2:4440/ .
var testAccProviders map[string]terraform.ResourceProvider
var testAccProvider *schema.Provider
func init() {
testAccProvider = Provider().(*schema.Provider)
testAccProviders = map[string]terraform.ResourceProvider{
"rundeck": testAccProvider,
}
}
func TestProvider(t *testing.T) {
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = Provider()
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("RUNDECK_URL"); v == "" {
t.Fatal("RUNDECK_URL must be set for acceptance tests")
}
if v := os.Getenv("RUNDECK_AUTH_TOKEN"); v == "" {
t.Fatal("RUNDECK_AUTH_TOKEN must be set for acceptance tests")
}
}

View File

@ -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
}

View File

@ -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"
}
}
`

View File

@ -0,0 +1,114 @@
package rundeck
import (
"crypto/sha1"
"encoding/hex"
"github.com/hashicorp/terraform/helper/schema"
"github.com/apparentlymart/go-rundeck-api/rundeck"
)
func resourceRundeckPrivateKey() *schema.Resource {
return &schema.Resource{
Create: CreateOrUpdatePrivateKey,
Update: CreateOrUpdatePrivateKey,
Delete: DeletePrivateKey,
Exists: PrivateKeyExists,
Read: ReadPrivateKey,
Schema: map[string]*schema.Schema{
"path": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Path to the key within the key store",
ForceNew: true,
},
"key_material": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "The private key material to store, in PEM format",
StateFunc: func(v interface{}) string {
switch v.(type) {
case string:
hash := sha1.Sum([]byte(v.(string)))
return hex.EncodeToString(hash[:])
default:
return ""
}
},
},
},
}
}
func CreateOrUpdatePrivateKey(d *schema.ResourceData, meta interface{}) error {
client := meta.(*rundeck.Client)
path := d.Get("path").(string)
keyMaterial := d.Get("key_material").(string)
var err error
if d.Id() != "" {
err = client.ReplacePrivateKey(path, keyMaterial)
} else {
err = client.CreatePrivateKey(path, keyMaterial)
}
if err != nil {
return err
}
d.SetId(path)
return ReadPrivateKey(d, meta)
}
func DeletePrivateKey(d *schema.ResourceData, meta interface{}) error {
client := meta.(*rundeck.Client)
path := d.Id()
// The only "delete" call we have is oblivious to key type, but
// that's okay since our Exists implementation makes sure that we
// won't try to delete a key of the wrong type since we'll pretend
// that it's already been deleted.
err := client.DeleteKey(path)
if err != nil {
return err
}
d.SetId("")
return nil
}
func ReadPrivateKey(d *schema.ResourceData, meta interface{}) error {
// Nothing to read for a private key: existence is all we need to
// worry about, and PrivateKeyExists took care of that.
return nil
}
func PrivateKeyExists(d *schema.ResourceData, meta interface{}) (bool, error) {
client := meta.(*rundeck.Client)
path := d.Id()
key, err := client.GetKeyMeta(path)
if err != nil {
if _, ok := err.(rundeck.NotFoundError); ok {
err = nil
}
return false, err
}
if key.KeyType != "private" {
// If the key type isn't public then as far as this resource is
// concerned it doesn't exist. (We'll fail properly when we try to
// create a key where one already exists.)
return false, nil
}
return true, nil
}

View File

@ -0,0 +1,92 @@
package rundeck
import (
"fmt"
"strings"
"testing"
"github.com/apparentlymart/go-rundeck-api/rundeck"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccPrivateKey_basic(t *testing.T) {
var key rundeck.KeyMeta
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccPrivateKeyCheckDestroy(&key),
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccPrivateKeyConfig_basic,
Check: resource.ComposeTestCheckFunc(
testAccPrivateKeyCheckExists("rundeck_private_key.test", &key),
func(s *terraform.State) error {
if expected := "keys/terraform_acceptance_tests/private_key"; key.Path != expected {
return fmt.Errorf("wrong path; expected %v, got %v", expected, key.Path)
}
if !strings.HasSuffix(key.URL, "/storage/keys/terraform_acceptance_tests/private_key") {
return fmt.Errorf("wrong URL; expected to end with the key path")
}
if expected := "file"; key.ResourceType != expected {
return fmt.Errorf("wrong resource type; expected %v, got %v", expected, key.ResourceType)
}
if expected := "private"; key.KeyType != expected {
return fmt.Errorf("wrong key type; expected %v, got %v", expected, key.KeyType)
}
// Rundeck won't let us re-retrieve a private key payload, so we can't test
// that the key material was submitted and stored correctly.
return nil
},
),
},
},
})
}
func testAccPrivateKeyCheckDestroy(key *rundeck.KeyMeta) resource.TestCheckFunc {
return func(s *terraform.State) error {
client := testAccProvider.Meta().(*rundeck.Client)
_, err := client.GetKeyMeta(key.Path)
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 testAccPrivateKeyCheckExists(rn string, key *rundeck.KeyMeta) 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("key id not set")
}
client := testAccProvider.Meta().(*rundeck.Client)
gotKey, err := client.GetKeyMeta(rs.Primary.ID)
if err != nil {
return fmt.Errorf("error getting key metadata: %s", err)
}
*key = *gotKey
return nil
}
}
const testAccPrivateKeyConfig_basic = `
resource "rundeck_private_key" "test" {
path = "terraform_acceptance_tests/private_key"
key_material = "this is not a real private key"
}
`

View File

@ -0,0 +1,293 @@
package rundeck
import (
"fmt"
"sort"
"strconv"
"strings"
"github.com/hashicorp/terraform/helper/schema"
"github.com/apparentlymart/go-rundeck-api/rundeck"
)
var projectConfigAttributes = map[string]string{
"project.name": "name",
"project.description": "description",
"service.FileCopier.default.provider": "default_node_file_copier_plugin",
"service.NodeExecutor.default.provider": "default_node_executor_plugin",
"project.ssh-authentication": "ssh_authentication_type",
"project.ssh-key-storage-path": "ssh_key_storage_path",
"project.ssh-keypath": "ssh_key_file_path",
}
func resourceRundeckProject() *schema.Resource {
return &schema.Resource{
Create: CreateProject,
Update: UpdateProject,
Delete: DeleteProject,
Exists: ProjectExists,
Read: ReadProject,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Unique name for the project",
ForceNew: true,
},
"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Description of the project to be shown in the Rundeck UI",
Default: "Managed by Terraform",
},
"ui_url": &schema.Schema{
Type: schema.TypeString,
Required: false,
Computed: true,
},
"resource_model_source": &schema.Schema{
Type: schema.TypeList,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"type": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Name of the resource model plugin to use",
},
"config": &schema.Schema{
Type: schema.TypeMap,
Required: true,
Description: "Configuration parameters for the selected plugin",
},
},
},
},
"default_node_file_copier_plugin": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "jsch-scp",
},
"default_node_executor_plugin": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "jsch-ssh",
},
"ssh_authentication_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "privateKey",
},
"ssh_key_storage_path": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"ssh_key_file_path": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"extra_config": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
Description: "Additional raw configuration parameters to include in the project configuration, with dots replaced with slashes in the key names due to limitations in Terraform's config language.",
},
},
}
}
func CreateProject(d *schema.ResourceData, meta interface{}) error {
client := meta.(*rundeck.Client)
// Rundeck's model is a little inconsistent in that we can create
// a project via a high-level structure but yet we must update
// the project via its raw config properties.
// For simplicity's sake we create a bare minimum project here
// and then delegate to UpdateProject to fill in the rest of the
// configuration via the raw config properties.
project, err := client.CreateProject(&rundeck.Project{
Name: d.Get("name").(string),
})
if err != nil {
return err
}
d.SetId(project.Name)
d.Set("id", project.Name)
return UpdateProject(d, meta)
}
func UpdateProject(d *schema.ResourceData, meta interface{}) error {
client := meta.(*rundeck.Client)
// In Rundeck, updates are always in terms of the low-level config
// properties map, so we need to transform our data structure
// into the equivalent raw properties.
projectName := d.Id()
updateMap := map[string]string{}
slashReplacer := strings.NewReplacer("/", ".")
if extraConfig := d.Get("extra_config"); extraConfig != nil {
for k, v := range extraConfig.(map[string]interface{}) {
updateMap[slashReplacer.Replace(k)] = v.(string)
}
}
for configKey, attrKey := range projectConfigAttributes {
v := d.Get(attrKey).(string)
if v != "" {
updateMap[configKey] = v
}
}
for i, rmsi := range d.Get("resource_model_source").([]interface{}) {
rms := rmsi.(map[string]interface{})
pluginType := rms["type"].(string)
ci := rms["config"].(map[string]interface{})
attrKeyPrefix := fmt.Sprintf("resources.source.%v.", i+1)
typeKey := attrKeyPrefix + "type"
configKeyPrefix := fmt.Sprintf("%vconfig.", attrKeyPrefix)
updateMap[typeKey] = pluginType
for k, v := range ci {
updateMap[configKeyPrefix+k] = v.(string)
}
}
err := client.SetProjectConfig(projectName, updateMap)
if err != nil {
return err
}
return ReadProject(d, meta)
}
func ReadProject(d *schema.ResourceData, meta interface{}) error {
client := meta.(*rundeck.Client)
name := d.Id()
project, err := client.GetProject(name)
if err != nil {
return err
}
for configKey, attrKey := range projectConfigAttributes {
d.Set(projectConfigAttributes[configKey], nil)
if v, ok := project.Config[configKey]; ok {
d.Set(attrKey, v)
// Remove this key so it won't get included in extra_config
// later.
delete(project.Config, configKey)
}
}
resourceSourceMap := map[int]interface{}{}
configMaps := map[int]interface{}{}
for configKey, v := range project.Config {
if strings.HasPrefix(configKey, "resources.source.") {
nameParts := strings.Split(configKey, ".")
if len(nameParts) < 4 {
continue
}
index, err := strconv.Atoi(nameParts[2])
if err != nil {
continue
}
if _, ok := resourceSourceMap[index]; !ok {
configMap := map[string]interface{}{}
configMaps[index] = configMap
resourceSourceMap[index] = map[string]interface{}{
"config": configMap,
}
}
switch nameParts[3] {
case "type":
if len(nameParts) != 4 {
continue
}
m := resourceSourceMap[index].(map[string]interface{})
m["type"] = v
case "config":
if len(nameParts) != 5 {
continue
}
m := configMaps[index].(map[string]interface{})
m[nameParts[4]] = v
default:
continue
}
// Remove this key so it won't get included in extra_config
// later.
delete(project.Config, configKey)
}
}
resourceSources := []map[string]interface{}{}
resourceSourceIndices := []int{}
for k := range resourceSourceMap {
resourceSourceIndices = append(resourceSourceIndices, k)
}
sort.Ints(resourceSourceIndices)
for _, index := range resourceSourceIndices {
resourceSources = append(resourceSources, resourceSourceMap[index].(map[string]interface{}))
}
d.Set("resource_model_source", resourceSources)
extraConfig := map[string]string{}
dotReplacer := strings.NewReplacer(".", "/")
for k, v := range project.Config {
extraConfig[dotReplacer.Replace(k)] = v
}
d.Set("extra_config", extraConfig)
d.Set("name", project.Name)
d.Set("ui_url", project.URL)
return nil
}
func ProjectExists(d *schema.ResourceData, meta interface{}) (bool, error) {
client := meta.(*rundeck.Client)
name := d.Id()
_, err := client.GetProject(name)
if _, ok := err.(rundeck.NotFoundError); ok {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
func DeleteProject(d *schema.ResourceData, meta interface{}) error {
client := meta.(*rundeck.Client)
name := d.Id()
return client.DeleteProject(name)
}

View File

@ -0,0 +1,98 @@
package rundeck
import (
"fmt"
"testing"
"github.com/apparentlymart/go-rundeck-api/rundeck"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccProject_basic(t *testing.T) {
var project rundeck.Project
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccProjectCheckDestroy(&project),
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccProjectConfig_basic,
Check: resource.ComposeTestCheckFunc(
testAccProjectCheckExists("rundeck_project.main", &project),
func(s *terraform.State) error {
if expected := "terraform-acc-test-basic"; project.Name != expected {
return fmt.Errorf("wrong name; expected %v, got %v", expected, project.Name)
}
if expected := "baz"; project.Config["foo.bar"] != expected {
return fmt.Errorf("wrong foo.bar config; expected %v, got %v", expected, project.Config["foo.bar"])
}
if expected := "file"; project.Config["resources.source.1.type"] != expected {
return fmt.Errorf("wrong resources.source.1.type config; expected %v, got %v", expected, project.Config["resources.source.1.type"])
}
return nil
},
),
},
},
})
}
func testAccProjectCheckDestroy(project *rundeck.Project) resource.TestCheckFunc {
return func(s *terraform.State) error {
client := testAccProvider.Meta().(*rundeck.Client)
_, err := client.GetProject(project.Name)
if err == nil {
return fmt.Errorf("project still exists")
}
if _, ok := err.(*rundeck.NotFoundError); !ok {
return fmt.Errorf("got something other than NotFoundError (%v) when getting project", err)
}
return nil
}
}
func testAccProjectCheckExists(rn string, project *rundeck.Project) 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("project id not set")
}
client := testAccProvider.Meta().(*rundeck.Client)
gotProject, err := client.GetProject(rs.Primary.ID)
if err != nil {
return fmt.Errorf("error getting project: %s", err)
}
*project = *gotProject
return nil
}
}
const testAccProjectConfig_basic = `
resource "rundeck_project" "main" {
name = "terraform-acc-test-basic"
description = "Terraform Acceptance Tests Basic Project"
resource_model_source {
type = "file"
config = {
format = "resourcexml"
file = "/tmp/terraform-acc-tests.xml"
}
}
extra_config = {
"foo/bar" = "baz"
}
}
`

View File

@ -0,0 +1,148 @@
package rundeck
import (
"github.com/hashicorp/terraform/helper/schema"
"github.com/apparentlymart/go-rundeck-api/rundeck"
)
func resourceRundeckPublicKey() *schema.Resource {
return &schema.Resource{
Create: CreatePublicKey,
Update: UpdatePublicKey,
Delete: DeletePublicKey,
Exists: PublicKeyExists,
Read: ReadPublicKey,
Schema: map[string]*schema.Schema{
"path": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Path to the key within the key store",
ForceNew: true,
},
"key_material": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
Description: "The public key data to store, in the usual OpenSSH public key file format",
},
"url": &schema.Schema{
Type: schema.TypeString,
Computed: true,
Description: "URL at which the key content can be retrieved",
},
"delete": &schema.Schema{
Type: schema.TypeBool,
Computed: true,
Description: "True if the key should be deleted when the resource is deleted. Defaults to true if key_material is provided in the configuration.",
},
},
}
}
func CreatePublicKey(d *schema.ResourceData, meta interface{}) error {
client := meta.(*rundeck.Client)
path := d.Get("path").(string)
keyMaterial := d.Get("key_material").(string)
if keyMaterial != "" {
err := client.CreatePublicKey(path, keyMaterial)
if err != nil {
return err
}
d.Set("delete", true)
}
d.SetId(path)
return ReadPublicKey(d, meta)
}
func UpdatePublicKey(d *schema.ResourceData, meta interface{}) error {
client := meta.(*rundeck.Client)
if d.HasChange("key_material") {
path := d.Get("path").(string)
keyMaterial := d.Get("key_material").(string)
err := client.ReplacePublicKey(path, keyMaterial)
if err != nil {
return err
}
}
return ReadPublicKey(d, meta)
}
func DeletePublicKey(d *schema.ResourceData, meta interface{}) error {
client := meta.(*rundeck.Client)
path := d.Id()
// Since this resource can be used both to create and to read existing
// public keys, we'll only actually delete the key if we remember that
// we created the key in the first place, or if the user explicitly
// opted in to have an existing key deleted.
if d.Get("delete").(bool) {
// The only "delete" call we have is oblivious to key type, but
// that's okay since our Exists implementation makes sure that we
// won't try to delete a key of the wrong type since we'll pretend
// that it's already been deleted.
err := client.DeleteKey(path)
if err != nil {
return err
}
}
d.SetId("")
return nil
}
func ReadPublicKey(d *schema.ResourceData, meta interface{}) error {
client := meta.(*rundeck.Client)
path := d.Id()
key, err := client.GetKeyMeta(path)
if err != nil {
return err
}
keyMaterial, err := client.GetKeyContent(path)
if err != nil {
return err
}
d.Set("key_material", keyMaterial)
d.Set("url", key.URL)
return nil
}
func PublicKeyExists(d *schema.ResourceData, meta interface{}) (bool, error) {
client := meta.(*rundeck.Client)
path := d.Id()
key, err := client.GetKeyMeta(path)
if err != nil {
if _, ok := err.(rundeck.NotFoundError); ok {
err = nil
}
return false, err
}
if key.KeyType != "public" {
// If the key type isn't public then as far as this resource is
// concerned it doesn't exist. (We'll fail properly when we try to
// create a key where one already exists.)
return false, nil
}
return true, nil
}

View File

@ -0,0 +1,99 @@
package rundeck
import (
"fmt"
"strings"
"testing"
"github.com/apparentlymart/go-rundeck-api/rundeck"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccPublicKey_basic(t *testing.T) {
var key rundeck.KeyMeta
var keyMaterial string
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccPublicKeyCheckDestroy(&key),
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccPublicKeyConfig_basic,
Check: resource.ComposeTestCheckFunc(
testAccPublicKeyCheckExists("rundeck_public_key.test", &key, &keyMaterial),
func(s *terraform.State) error {
if expected := "keys/terraform_acceptance_tests/public_key"; key.Path != expected {
return fmt.Errorf("wrong path; expected %v, got %v", expected, key.Path)
}
if !strings.HasSuffix(key.URL, "/storage/keys/terraform_acceptance_tests/public_key") {
return fmt.Errorf("wrong URL; expected to end with the key path")
}
if expected := "file"; key.ResourceType != expected {
return fmt.Errorf("wrong resource type; expected %v, got %v", expected, key.ResourceType)
}
if expected := "public"; key.KeyType != expected {
return fmt.Errorf("wrong key type; expected %v, got %v", expected, key.KeyType)
}
if !strings.Contains(keyMaterial, "test+public+key+for+terraform") {
return fmt.Errorf("wrong key material")
}
return nil
},
),
},
},
})
}
func testAccPublicKeyCheckDestroy(key *rundeck.KeyMeta) resource.TestCheckFunc {
return func(s *terraform.State) error {
client := testAccProvider.Meta().(*rundeck.Client)
_, err := client.GetKeyMeta(key.Path)
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 testAccPublicKeyCheckExists(rn string, key *rundeck.KeyMeta, keyMaterial *string) 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("key id not set")
}
client := testAccProvider.Meta().(*rundeck.Client)
gotKey, err := client.GetKeyMeta(rs.Primary.ID)
if err != nil {
return fmt.Errorf("error getting key metadata: %s", err)
}
*key = *gotKey
*keyMaterial, err = client.GetKeyContent(rs.Primary.ID)
if err != nil {
return fmt.Errorf("error getting key contents: %s", err)
}
return nil
}
}
const testAccPublicKeyConfig_basic = `
resource "rundeck_public_key" "test" {
path = "terraform_acceptance_tests/public_key"
key_material = "ssh-rsa test+public+key+for+terraform nobody@nowhere"
}
`

View File

@ -20,6 +20,7 @@ body.layout-google,
body.layout-heroku, body.layout-heroku,
body.layout-mailgun, body.layout-mailgun,
body.layout-openstack, body.layout-openstack,
body.layout-rundeck,
body.layout-template, body.layout-template,
body.layout-docs, body.layout-docs,
body.layout-downloads, body.layout-downloads,

View File

@ -0,0 +1,75 @@
---
layout: "rundeck"
page_title: "Provider: Rundeck"
sidebar_current: "docs-rundeck-index"
description: |-
The Rundeck provider configures projects, jobs and keys in Rundeck.
---
# Rundeck Provider
The Rundeck provider allows Terraform to create and configure Projects,
Jobs and Keys in [Rundeck](http://rundeck.org/). Rundeck is a tool
for runbook automation and execution of arbitrary management tasks,
allowing operators to avoid logging in to individual machines directly
via SSH.
The provider configuration block accepts the following arguments:
* ``url`` - (Required) The root URL of a Rundeck server. May alternatively be set via the
``RUNDECK_URL`` environment variable.
* ``auth_token`` - (Required) The API auth token to use when making requests. May alternatively
be set via the ``RUNDECK_AUTH_TOKEN`` environment variable.
* ``allow_unverified_ssl`` - (Optional) Boolean that can be set to ``true`` to disable SSL
certificate verification. This should be used with care as it could allow an attacker to
intercept your auth token.
Use the navigation to the left to read about the available resources.
## Example Usage
```
provider "rundeck" {
url = "http://rundeck.example.com/"
auth_token = "abcd1234"
}
resource "rundeck_project" "anvils" {
name = "anvils"
description = "Application for managing Anvils"
ssh_key_storage_path = "${rundeck_private_key.anvils.path}"
resource_model_source {
type = "file"
config = {
format = "resourcexml"
# This path is interpreted on the Rundeck server.
file = "/var/rundeck/projects/anvils/resources.xml"
}
}
}
resource "rundeck_job" "bounceweb" {
name = "Bounce Web Servers"
project_name = "${rundeck_project.anvils.name}"
node_filter_query = "tags: web"
description = "Restart the service daemons on all the web servers"
command {
shell_command = "sudo service anvils restart"
}
}
resource "rundeck_public_key" "anvils" {
path = "anvils/id_rsa.pub"
key_material = "ssh-rsa yada-yada-yada"
}
resource "rundeck_private_key" "anvils" {
path = "anvils/id_rsa"
key_material_file = "${path.module}/id_rsa.pub"
}
```

View File

@ -0,0 +1,166 @@
---
layout: "rundeck"
page_title: "Rundeck: rundeck_job"
sidebar_current: "docs-rundeck-resource-job"
description: |-
The rundeck_job resource allows Rundeck jobs to be managed by Terraform.
---
# rundeck\_job
The job resource allows Rundeck jobs to be managed by Terraform. In Rundeck a job is a particular
named set of steps that can be executed against one or more of the nodes configured for its
associated project.
Each job belongs to a project. A project can be created with the `rundeck_project` resource.
## Example Usage
```
resource "rundeck_job" "bounceweb" {
name = "Bounce Web Servers"
project_name = "anvils"
node_filter_query = "tags: web"
description = "Restart the service daemons on all the web servers"
command {
shell_command = "sudo service anvils restart"
}
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) The name of the job, used to describe the job in the Rundeck UI.
* `description` - (Required) A longer description of the job, describing the job in the Rundeck UI.
* `project_name` - (Required) The name of the project that this job should belong to.
* `group_name` - (Optional) The name of a group within the project in which to place the job.
Setting this creates collapsable subcategories within the Rundeck UI's project job index.
* `log_level` - (Optional) The log level that Rundeck should use for this job. Defaults to "INFO".
* `allow_concurrent_executions` - (Optional) Boolean defining whether two or more executions of
this job can run concurrently. The default is `false`, meaning that jobs will only run
sequentially.
* `max_thread_count` - (Optional) The maximum number of threads to use to execute this job, which
controls on how many nodes the commands can be run simulateneously. Defaults to 1, meaning that
the nodes will be visited sequentially.
* `continue_on_error` - (Optional) Boolean defining whether Rundeck will continue to run
subsequent steps if any intermediate step fails. Defaults to `false`, meaning that execution
will stop and the execution will be considered to have failed.
* `rank_attribute` - (Optional) The name of the attribute that will be used to decide in which
order the nodes will be visited while executing the job across multiple nodes.
* `rank_order` - (Optional) Keyword deciding which direction the nodes are sorted in terms of
the chosen `rank_attribute`. May be either "ascending" (the default) or "descending".
* `preserve_options_order`: (Optional) Boolean controlling whether the configured options will
be presented in their configuration order when shown in the Rundeck UI. The default is `false`,
which means that the options will be displayed in alphabetical order by name.
* `command_ordering_strategy`: (Optional) The name of the strategy used to describe how to
traverse the matrix of nodes and commands. The default is "node-first", meaning that all commands
will be executed on a single node before moving on to the next. May also be set to "step-first",
meaning that a single step will be executed across all nodes before moving on to the next step.
* `node_filter_query` - (Optional) A query string using
[Rundeck's node filter language](http://rundeck.org/docs/manual/node-filters.html#node-filter-syntax)
that defines which subset of the project's nodes will be used to execute this job.
* `node_filter_exclude_precedence`: (Optional) Boolean controlling a deprecated Rundeck feature that controls
whether node exclusions take priority over inclusions.
* `option`: (Optional) Nested block defining an option a user may set when executing this job. A
job may have any number of options. The structure of this nested block is described below.
* `command`: (Required) Nested block defining one step in the job workflow. A job must have one or
more commands. The structure of this nested block is described below.
`option` blocks have the following structure:
* `name`: (Required) Unique name that will be shown in the UI when entering values and used as
a variable name for template substitutions.
* `default_value`: (Optional) A default value for the option.
* `value_choices`: (Optional) A list of strings giving a set of predefined values that the user
may choose from when entering a value for the option.
* `value_choices_url`: (Optional) Can be used instead of `value_choices` to cause Rundeck to
obtain a list of choices dynamically by fetching this URL.
* `require_predefined_choice`: (Optional) Boolean controlling whether the user is allowed to
enter values not included in the predefined set of choices (`false`, the default) or whether
a predefined choice is required (`true`).
* `validation_regex`: (Optional) A regular expression that a provided value must match in order
to be accepted.
* `description`: (Optional) A longer description of the option to be shown in the UI.
* `required`: (Optional) Boolean defining whether the user must provide a value for the option.
Defaults to `false`.
* `allow_multiple_values`: (Optional) Boolean defining whether the user may select multiple values
from the set of predefined values. Defaults to `false`, meaning that the user may choose only
one value.
* `multi_value_delimeter`: (Optional) Delimeter used to join together multiple values into a single
string when `allow_multiple_values` is set and the user chooses multiple values.
* `obscure_input`: (Optional) Boolean controlling whether the value of this option should be obscured
during entry and in execution logs. Defaults to `false`, but should be set to `true` when the
requested value is a password, private key or any other secret value.
* `exposed_to_scripts`: (Optional) Boolean controlling whether the value of this option is available
to scripts executed by job commands. Defaults to `false`.
`command` blocks must have any one of the following combinations of arguments as contents:
* `shell_command` gives a single shell command to execute on the nodes.
* `inline_script` gives a whole shell script, inline in the configuration, to execute on the nodes.
* `script_file` and `script_file_args` together describe a script that is already pre-installed
on the nodes which is to be executed.
* A `job` block, described below, causes another job within the same project to be executed as
a command.
* A `step_plugin` block, described below, causes a step plugin to be executed as a command.
* A `node_step_plugin` block, described below, causes a node step plugin to be executed once for
each node.
A command's `job` block has the following structure:
* `name`: (Required) The name of the job to execute. The target job must be in the same project
as the current job.
* `group_name`: (Optional) The name of the group that the target job belongs to, if any.
* `run_for_each_node`: (Optional) Boolean controlling whether the job is run only once (`false`,
the default) or whether it is run once for each node (`true`).
* `args`: (Optional) A string giving the arguments to pass to the target job, using
[Rundeck's job arguments syntax](http://rundeck.org/docs/manual/jobs.html#job-reference-step).
A command's `step_plugin` or `node_step_plugin` block both have the following structure:
* `type`: (Required) The name of the plugin to execute.
* `config`: (Optional) Map of arbitrary configuration parameters for the selected plugin.
## Attributes Reference
The following attribute is exported:
* `id` - A unique identifier for the job.

View File

@ -0,0 +1,39 @@
---
layout: "rundeck"
page_title: "Rundeck: rundeck_private_key"
sidebar_current: "docs-rundeck-resource-private-key"
description: |-
The rundeck_private_key resource allows private keys to be stored in Rundeck's key store.
---
# rundeck\_private\_key
The private key resource allows SSH private keys to be stored into Rundeck's key store.
The key store is where Rundeck keeps credentials that are needed to access the nodes on which
it runs commands.
## Example Usage
```
resource "rundeck_private_key" "anvils" {
path = "anvils/id_rsa"
key_material = "${file(\"/id_rsa\")}"
}
```
## Argument Reference
The following arguments are supported:
* `path` - (Required) The path within the key store where the key will be stored.
* `key_material` - (Required) The private key material to store, serialized in any way that is
accepted by OpenSSH.
The key material is hashed before it is stored in the state file, so sharing the resulting state
will not disclose the private key contents.
## Attributes Reference
Rundeck does not allow stored private keys to be retrieved via the API, so this resource does not
export any attributes.

View File

@ -0,0 +1,90 @@
---
layout: "rundeck"
page_title: "Rundeck: rundeck_project"
sidebar_current: "docs-rundeck-resource-project"
description: |-
The rundeck_project resource allows Rundeck projects to be managed by Terraform.
---
# rundeck\_project
The project resource allows Rundeck projects to be managed by Terraform. In Rundeck a project
is the container object for a set of jobs and the configuration for which servers those jobs
can be run on.
## Example Usage
```
resource "rundeck_project" "anvils" {
name = "anvils"
description = "Application for managing Anvils"
ssh_key_storage_path = "anvils/id_rsa"
resource_model_source {
type = "file"
config = {
format = "resourcexml"
# This path is interpreted on the Rundeck server.
file = "/var/rundeck/projects/anvils/resources.xml"
}
}
}
```
Note that the above configuration assumes the existence of a ``resources.xml`` file in the
filesystem on the Rundeck server. The Rundeck provider does not itself support creating such a file,
but one way to place it would be to use the ``file`` provisioner to copy a configuration file
from the module directory.
## Argument Reference
The following arguments are supported:
* `name` - (Required) The name of the project, used both in the UI and to uniquely identify
the project. Must therefore be unique across a single Rundeck installation.
* `resource_model_source` - (Required) Nested block instructing Rundeck on how to determine the
set of resources (nodes) for this project. The nested block structure is described below.
* `description` - (Optional) A description of the project, to be displayed in the Rundeck UI.
Defaults to "Managed by Terraform".
* `default_node_file_copier_plugin` - (Optional) The name of a plugin to use to copy files onto
nodes within this project. Defaults to `jsch-scp`, which uses the "Secure Copy" protocol
to send files over SSH.
* `default_node_executor_plugin` - (Optional) The name of a plugin to use to run commands on
nodes within this project. Defaults to `jsch-ssh`, which uses the SSH protocol to access the
nodes.
* `ssh_authentication_type` - (Optional) When the SSH-based file copier and executor plugins are
used, the type of SSH authentication to use. Defaults to `privateKey`.
* `ssh_key_storage_path` - (Optional) When the SSH-based file copier and executor plugins are
used, the location within Rundeck's key store where the SSH private key can be found. Private
keys can be uploaded to rundeck using the `rundeck_private_key` resource.
* `ssh_key_file_path` - (Optional) Like `ssh_key_storage_path` except that the key is read from
the Rundeck server's local filesystem, rather than from the key store.
* `extra_config` - (Optional) Behind the scenes a Rundeck project is really an arbitrary set of
key/value pairs. This map argument allows setting any configuration properties that aren't
explicitly supported by the other arguments described above, but due to limitations of Terraform
the key names must be written with slashes in place of dots. Do not use this argument to set
properties that the above arguments set, or undefined behavior will result.
`resource_model_source` blocks have the following nested arguments:
* `type` - (Required) The name of the resource model plugin to use.
* `config` - (Required) Map of arbitrary configuration properties for the selected resource model
plugin.
## Attributes Reference
The following attributes are exported:
* `name` - The unique name that identifies the project, as set in the arguments.
* `ui_url` - The URL of the index page for this project in the Rundeck UI.

View File

@ -0,0 +1,51 @@
---
layout: "rundeck"
page_title: "Rundeck: rundeck_public_key"
sidebar_current: "docs-rundeck-resource-public-key"
description: |-
The rundeck_public_key resource allows public keys to be stored in Rundeck's key store.
---
# rundeck\_public\_key
The public key resource allows SSH public keys to be stored into Rundeck's key store.
The key store is where Rundeck keeps credentials that are needed to access the nodes on which
it runs commands.
This resource also allows the retrieval of an existing public key from the store, so that it
may be used in the configuration of other resources such as ``aws_key_pair``.
## Example Usage
```
resource "rundeck_public_key" "anvils" {
path = "anvils/id_rsa.pub"
key_material = "ssh-rsa yada-yada-yada"
}
```
## Argument Reference
The following arguments are supported:
* `path` - (Required) The path within the key store where the key will be stored. By convention
this path name normally ends with ".pub" and otherwise has the same name as the associated
private key.
* `key_material` - (Optional) The public key string to store, serialized in any way that is accepted
by OpenSSH. If this is not included, ``key_material`` becomes an attribute that can be used
to read the already-existing key material in the Rundeck store.
The key material is included inline as a string, which is consistent with the way a public key
is provided to the `aws_key_pair`, `cloudstack_ssh_keypair`, `digitalocean_ssh_key` and
`openstack_compute_keypair_v2` resources. This means the `key_material` argument can be populated
from the interpolation of the `public_key` attribute of such a keypair resource, or vice-versa.
## Attributes Reference
The following attributes are exported:
* `url` - The URL at which the key material can be retrieved from the key store by other clients.
* `key_material` - If `key_material` is omitted in the configuration, it becomes an attribute that
exposes the key material already stored at the given `path`.

View File

@ -177,6 +177,10 @@
<a href="/docs/providers/openstack/index.html">OpenStack</a> <a href="/docs/providers/openstack/index.html">OpenStack</a>
</li> </li>
<li<%= sidebar_current("docs-providers-rundeck") %>>
<a href="/docs/providers/rundeck/index.html">Rundeck</a>
</li>
<li<%= sidebar_current("docs-providers-template") %>> <li<%= sidebar_current("docs-providers-template") %>>
<a href="/docs/providers/template/index.html">Template</a> <a href="/docs/providers/template/index.html">Template</a>
</li> </li>

View File

@ -0,0 +1,35 @@
<% wrap_layout :inner do %>
<% content_for :sidebar do %>
<div class="docs-sidebar hidden-print affix-top" role="complementary">
<ul class="nav docs-sidenav">
<li<%= sidebar_current("docs-home") %>>
<a href="/docs/providers/index.html">&laquo; Documentation Home</a>
</li>
<li<%= sidebar_current("docs-rundeck-index") %>>
<a href="/docs/providers/rundeck/index.html">Rundeck Provider</a>
</li>
<li<%= sidebar_current(/^docs-rundeck-resource/) %>>
<a href="#">Resources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-rundeck-resource-project") %>>
<a href="/docs/providers/rundeck/r/project.html">rundeck_project</a>
</li>
<li<%= sidebar_current("docs-rundeck-resource-private-key") %>>
<a href="/docs/providers/rundeck/r/private_key.html">rundeck_private_key</a>
</li>
<li<%= sidebar_current("docs-rundeck-resource-public-key") %>>
<a href="/docs/providers/rundeck/r/public_key.html">rundeck_public_key</a>
</li>
<li<%= sidebar_current("docs-rundeck-resource-job") %>>
<a href="/docs/providers/rundeck/r/job.html">rundeck_job</a>
</li>
</ul>
</li>
</ul>
</div>
<% end %>
<%= yield %>
<% end %>