294 lines
7.3 KiB
Go
294 lines
7.3 KiB
Go
|
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)
|
||
|
}
|