commit
0a64779ee5
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
`
|
|
@ -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)
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
`
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
```
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
||||||
|
|
|
@ -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`.
|
|
@ -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>
|
||||||
|
|
|
@ -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">« 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 %>
|
Loading…
Reference in New Issue