provider/consul: first pass
This commit is contained in:
parent
f8520d1446
commit
dd58896d07
|
@ -24,7 +24,8 @@ func (c *Config) Client() (*consulapi.Client, error) {
|
|||
}
|
||||
client, err := consulapi.NewClient(config)
|
||||
|
||||
log.Printf("[INFO] Consul Client configured with: %#v", config)
|
||||
log.Printf("[INFO] Consul Client configured with address: '%s', datacenter: '%s'",
|
||||
config.Address, config.Datacenter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -1,35 +1,174 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform/helper/config"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/armon/consul-api"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
func resource_consul_keys_validation() *config.Validator {
|
||||
return &config.Validator{
|
||||
Optional: []string{
|
||||
"datacenter",
|
||||
"*.key",
|
||||
"*.value",
|
||||
"*.default",
|
||||
"*.delete",
|
||||
},
|
||||
type consulKeys map[string]*consulKey
|
||||
|
||||
type consulKey struct {
|
||||
Key string
|
||||
Value string
|
||||
Default string
|
||||
Delete bool
|
||||
|
||||
SetValue bool `mapstructure:"-"`
|
||||
SetDefault bool `mapstructure:"-"`
|
||||
}
|
||||
|
||||
func resource_consul_keys_validate(c *terraform.ResourceConfig) (ws []string, es []error) {
|
||||
conf := c.Raw
|
||||
for k, v := range conf {
|
||||
// datacenter is special and can be ignored
|
||||
if k == "datacenter" {
|
||||
continue
|
||||
}
|
||||
|
||||
keyList, ok := v.([]map[string]interface{})
|
||||
if !ok {
|
||||
es = append(es, fmt.Errorf("Field '%s' must be map containing a key", k))
|
||||
continue
|
||||
}
|
||||
if len(keyList) > 1 {
|
||||
es = append(es, fmt.Errorf("Field '%s' is defined more than once", k))
|
||||
continue
|
||||
}
|
||||
key := keyList[0]
|
||||
|
||||
for sub, val := range key {
|
||||
// Verify the sub-key is supported
|
||||
switch sub {
|
||||
case "key":
|
||||
case "value":
|
||||
case "default":
|
||||
case "delete":
|
||||
default:
|
||||
es = append(es, fmt.Errorf("Field '%s' has unsupported config '%s'", k, sub))
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify value is of the correct type
|
||||
_, isStr := val.(string)
|
||||
_, isBool := val.(bool)
|
||||
if !isStr && sub != "delete" {
|
||||
es = append(es, fmt.Errorf("Field '%s' must set '%s' as a string", key, sub))
|
||||
}
|
||||
if !isBool && sub == "delete" {
|
||||
es = append(es, fmt.Errorf("Field '%s' must set '%s' as a bool", key, sub))
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func resource_consul_keys_create(
|
||||
s *terraform.ResourceState,
|
||||
d *terraform.ResourceDiff,
|
||||
meta interface{}) (*terraform.ResourceState, error) {
|
||||
// Merge the diff into the state so that we have all the attributes
|
||||
// properly.
|
||||
rs := s.MergeDiff(d)
|
||||
return rs, nil
|
||||
p := meta.(*ResourceProvider)
|
||||
if s.Attributes == nil {
|
||||
s.Attributes = make(map[string]string)
|
||||
}
|
||||
|
||||
// Load the configuration
|
||||
var config map[string]interface{}
|
||||
for _, attr := range d.Attributes {
|
||||
if attr.NewExtra != nil {
|
||||
config = attr.NewExtra.(map[string]interface{})
|
||||
break
|
||||
}
|
||||
}
|
||||
if config == nil {
|
||||
return s, fmt.Errorf("Missing configuration state")
|
||||
}
|
||||
dc, keys, err := partsFromConfig(config)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
// Check if we are missing a datacenter
|
||||
if dc == "" {
|
||||
dc, err = get_dc(p.client)
|
||||
}
|
||||
s.Attributes["datacenter"] = dc
|
||||
|
||||
// Handle each of the keys
|
||||
kv := p.client.KV()
|
||||
qOpts := consulapi.QueryOptions{Datacenter: dc}
|
||||
wOpts := consulapi.WriteOptions{Datacenter: dc}
|
||||
for name, conf := range keys {
|
||||
if conf.SetValue {
|
||||
log.Printf("[DEBUG] Setting key '%s' to '%v' in %s", conf.Key, conf.Value, dc)
|
||||
pair := consulapi.KVPair{Key: conf.Key, Value: []byte(conf.Value)}
|
||||
if _, err := kv.Put(&pair, &wOpts); err != nil {
|
||||
return s, fmt.Errorf("Failed to set Consul key '%s': %v", conf.Key, err)
|
||||
}
|
||||
s.Attributes[name] = conf.Value
|
||||
} else {
|
||||
log.Printf("[DEBUG] Getting key '%s' in %s", conf.Key, dc)
|
||||
pair, _, err := kv.Get(conf.Key, &qOpts)
|
||||
if err != nil {
|
||||
return s, fmt.Errorf("Failed to get Consul key '%s': %v", conf.Key, err)
|
||||
}
|
||||
if pair == nil && conf.SetDefault {
|
||||
s.Attributes[name] = conf.Default
|
||||
} else if pair == nil {
|
||||
s.Attributes[name] = ""
|
||||
} else {
|
||||
s.Attributes[name] = string(pair.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set an ID, store the config
|
||||
s.ID = "consul"
|
||||
s.Extra = config
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// get_dc is used to get the datacenter of the local agent
|
||||
func get_dc(client *consulapi.Client) (string, error) {
|
||||
info, err := client.Agent().Self()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to get datacenter from Consul agent: %v", err)
|
||||
}
|
||||
dc := info["Config"]["Datacenter"].(string)
|
||||
return dc, nil
|
||||
}
|
||||
|
||||
func resource_consul_keys_destroy(
|
||||
s *terraform.ResourceState,
|
||||
meta interface{}) error {
|
||||
p := meta.(*ResourceProvider)
|
||||
client := p.client
|
||||
kv := client.KV()
|
||||
|
||||
// Restore our configuration
|
||||
dc, keys, err := partsFromConfig(s.Extra)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Load the DC if not given
|
||||
if dc == "" {
|
||||
dc = s.Attributes["datacenter"]
|
||||
}
|
||||
opts := consulapi.WriteOptions{Datacenter: dc}
|
||||
for _, key := range keys {
|
||||
// Skip any non-managed keys
|
||||
if !key.Delete {
|
||||
continue
|
||||
}
|
||||
log.Printf("[DEBUG] Deleting key '%s' in %s", key.Key, dc)
|
||||
if _, err := kv.Delete(key.Key, &opts); err != nil {
|
||||
return fmt.Errorf("Failed to delete Consul key '%s': %v", key.Key, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -37,21 +176,165 @@ func resource_consul_keys_update(
|
|||
s *terraform.ResourceState,
|
||||
d *terraform.ResourceDiff,
|
||||
meta interface{}) (*terraform.ResourceState, error) {
|
||||
// Merge the diff into the state so that we have all the attributes
|
||||
// properly.
|
||||
rs := s.MergeDiff(d)
|
||||
return rs, nil
|
||||
// TODO
|
||||
panic("update not supported")
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func resource_consul_keys_diff(
|
||||
s *terraform.ResourceState,
|
||||
c *terraform.ResourceConfig,
|
||||
meta interface{}) (*terraform.ResourceDiff, error) {
|
||||
return nil, nil
|
||||
// Parse the configuration
|
||||
dc, keys, err := partsFromConfig(c.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the old values
|
||||
oldValues := s.Attributes
|
||||
|
||||
// Initialize the diff set
|
||||
attrs := make(map[string]*terraform.ResourceAttrDiff)
|
||||
diff := &terraform.ResourceDiff{Attributes: attrs}
|
||||
|
||||
// Handle removed attributes
|
||||
for key, oldVal := range oldValues {
|
||||
if key == "datacenter" {
|
||||
continue
|
||||
}
|
||||
if _, keep := keys[key]; !keep {
|
||||
attrs[key] = &terraform.ResourceAttrDiff{
|
||||
Old: oldVal,
|
||||
NewRemoved: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle added or changed attributes
|
||||
for key, conf := range keys {
|
||||
aDiff := &terraform.ResourceAttrDiff{
|
||||
Type: terraform.DiffAttrInput,
|
||||
}
|
||||
oldVal, ok := oldValues[key]
|
||||
if conf.SetValue {
|
||||
aDiff.New = conf.Value
|
||||
} else {
|
||||
aDiff.NewComputed = true
|
||||
}
|
||||
if ok {
|
||||
aDiff.Old = oldVal
|
||||
}
|
||||
|
||||
// If this is new or changed we need to refresh
|
||||
if !ok || (conf.SetValue && oldVal != conf.Value) {
|
||||
attrs[key] = aDiff
|
||||
}
|
||||
}
|
||||
|
||||
// If the DC has changed, require a destroy!
|
||||
if old := oldValues["datacenter"]; dc != old {
|
||||
aDiff := &terraform.ResourceAttrDiff{
|
||||
Old: old,
|
||||
New: dc,
|
||||
RequiresNew: true,
|
||||
Type: terraform.DiffAttrInput,
|
||||
}
|
||||
if aDiff.New == "" {
|
||||
aDiff.NewComputed = true
|
||||
}
|
||||
attrs["datacenter"] = aDiff
|
||||
}
|
||||
|
||||
// Make sure one of the attributes contains the configuration
|
||||
if len(attrs) > 0 {
|
||||
for _, aDiff := range attrs {
|
||||
aDiff.NewExtra = c.Config
|
||||
break
|
||||
}
|
||||
}
|
||||
return diff, nil
|
||||
}
|
||||
|
||||
func resource_consul_keys_refresh(
|
||||
s *terraform.ResourceState,
|
||||
meta interface{}) (*terraform.ResourceState, error) {
|
||||
p := meta.(*ResourceProvider)
|
||||
client := p.client
|
||||
agent := client.Agent()
|
||||
kv := client.KV()
|
||||
|
||||
// Restore our configuration
|
||||
dc, keys, err := partsFromConfig(s.Extra)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
// Check if we are missing a datacenter
|
||||
if dc == "" {
|
||||
info, err := agent.Self()
|
||||
if err != nil {
|
||||
return s, fmt.Errorf("Failed to get datacenter from Consul agent: %v", err)
|
||||
}
|
||||
dc = info["Config"]["Datacenter"].(string)
|
||||
}
|
||||
|
||||
// Update the attributes
|
||||
s.Attributes["datacenter"] = dc
|
||||
opts := consulapi.QueryOptions{Datacenter: dc}
|
||||
for name, key := range keys {
|
||||
pair, _, err := kv.Get(key.Key, &opts)
|
||||
if err != nil {
|
||||
return s, fmt.Errorf("Failed to get key '%s' from Consul: %v", key.Key, err)
|
||||
}
|
||||
if pair == nil && key.SetDefault {
|
||||
s.Attributes[name] = key.Default
|
||||
} else if pair == nil {
|
||||
s.Attributes[name] = ""
|
||||
} else {
|
||||
s.Attributes[name] = string(pair.Value)
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// partsFromConfig extracts the relevant configuration from the raw format
|
||||
func partsFromConfig(raw map[string]interface{}) (string, consulKeys, error) {
|
||||
var dc string
|
||||
keys := make(map[string]*consulKey)
|
||||
for k, v := range raw {
|
||||
// datacenter is special and can be ignored
|
||||
if k == "datacenter" {
|
||||
vStr, ok := v.(string)
|
||||
if !ok {
|
||||
return "", nil, fmt.Errorf("datacenter must be a string")
|
||||
}
|
||||
dc = vStr
|
||||
continue
|
||||
}
|
||||
|
||||
confs, ok := v.([]map[string]interface{})
|
||||
if !ok {
|
||||
return "", nil, fmt.Errorf("Field '%s' must be map containing a key", k)
|
||||
}
|
||||
if len(confs) > 1 {
|
||||
return "", nil, fmt.Errorf("Field '%s' has duplicate definitions", k)
|
||||
}
|
||||
conf := confs[0]
|
||||
|
||||
key := &consulKey{}
|
||||
if err := mapstructure.WeakDecode(conf, key); err != nil {
|
||||
return "", nil, fmt.Errorf("Field '%s' failed to decode: %v", k, err)
|
||||
}
|
||||
for sub := range conf {
|
||||
switch sub {
|
||||
case "value":
|
||||
key.SetValue = true
|
||||
case "default":
|
||||
key.SetDefault = true
|
||||
}
|
||||
}
|
||||
keys[k] = key
|
||||
}
|
||||
return dc, keys, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/armon/consul-api"
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestAccConsulKeys(t *testing.T) {
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() {},
|
||||
Providers: testAccProviders,
|
||||
CheckDestroy: testAccCheckConsulKeysDestroy,
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAccConsulKeysConfig,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccCheckConsulKeysExists(),
|
||||
testAccCheckConsulKeysValue("consul_keys.app", "time", "<any>"),
|
||||
testAccCheckConsulKeysValue("consul_keys.app", "enabled", "true"),
|
||||
testAccCheckConsulKeysValue("consul_keys.app", "set", "acceptance"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccCheckConsulKeysDestroy(s *terraform.State) error {
|
||||
kv := testAccProvider.client.KV()
|
||||
opts := &consulapi.QueryOptions{Datacenter: "nyc1"}
|
||||
pair, _, err := kv.Get("test/set", opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pair != nil {
|
||||
return fmt.Errorf("Key still exists: %#v", pair)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func testAccCheckConsulKeysExists() resource.TestCheckFunc {
|
||||
return func(s *terraform.State) error {
|
||||
kv := testAccProvider.client.KV()
|
||||
opts := &consulapi.QueryOptions{Datacenter: "nyc1"}
|
||||
pair, _, err := kv.Get("test/set", opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pair == nil {
|
||||
return fmt.Errorf("Key 'test/set' does not exist")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testAccCheckConsulKeysValue(n, attr, val string) resource.TestCheckFunc {
|
||||
return func(s *terraform.State) error {
|
||||
rn, ok := s.Resources[n]
|
||||
if !ok {
|
||||
return fmt.Errorf("Resource not found")
|
||||
}
|
||||
out, ok := rn.Attributes[attr]
|
||||
if !ok {
|
||||
return fmt.Errorf("Attribute '%s' not found: %#v", attr, rn.Attributes)
|
||||
}
|
||||
if val != "<any>" && out != val {
|
||||
return fmt.Errorf("Attribute '%s' value '%s' != '%s'", attr, out, val)
|
||||
}
|
||||
if val == "<any>" && out == "" {
|
||||
return fmt.Errorf("Attribute '%s' value '%s'", attr, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
const testAccConsulKeysConfig = `
|
||||
resource "consul_keys" "app" {
|
||||
datacenter = "nyc1"
|
||||
time {
|
||||
key = "global/time"
|
||||
}
|
||||
enabled {
|
||||
key = "test/enabled"
|
||||
default = "true"
|
||||
}
|
||||
set {
|
||||
key = "test/set"
|
||||
value = "acceptance"
|
||||
delete = true
|
||||
}
|
||||
}
|
||||
`
|
|
@ -25,7 +25,12 @@ func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []er
|
|||
|
||||
func (p *ResourceProvider) ValidateResource(
|
||||
t string, c *terraform.ResourceConfig) ([]string, []error) {
|
||||
return resourceMap.Validate(t, c)
|
||||
switch t {
|
||||
case "consul_keys":
|
||||
return resource_consul_keys_validate(c)
|
||||
default:
|
||||
return resourceMap.Validate(t, c)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error {
|
||||
|
@ -33,7 +38,7 @@ func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error {
|
|||
return err
|
||||
}
|
||||
|
||||
log.Println("[INFO] Initializing Consul client")
|
||||
log.Printf("[INFO] Initializing Consul client")
|
||||
var err error
|
||||
p.client, err = p.Config.Client()
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
var testAccProviders map[string]terraform.ResourceProvider
|
||||
var testAccProvider *ResourceProvider
|
||||
|
||||
func init() {
|
||||
testAccProvider = new(ResourceProvider)
|
||||
testAccProvider.Config.Address = "demo.consul.io:80"
|
||||
testAccProviders = map[string]terraform.ResourceProvider{
|
||||
"consul": testAccProvider,
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceProvider_impl(t *testing.T) {
|
||||
var _ terraform.ResourceProvider = new(ResourceProvider)
|
||||
}
|
||||
|
||||
func TestResourceProvider_Configure(t *testing.T) {
|
||||
rp := new(ResourceProvider)
|
||||
|
||||
raw := map[string]interface{}{
|
||||
"address": "demo.consul.io:80",
|
||||
"datacenter": "nyc1",
|
||||
}
|
||||
|
||||
rawConfig, err := config.NewRawConfig(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
err = rp.Configure(terraform.NewResourceConfig(rawConfig))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expected := Config{
|
||||
Address: "demo.consul.io:80",
|
||||
Datacenter: "nyc1",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(rp.Config, expected) {
|
||||
t.Fatalf("bad: %#v", rp.Config)
|
||||
}
|
||||
}
|
|
@ -12,12 +12,11 @@ func init() {
|
|||
resourceMap = &resource.Map{
|
||||
Mapping: map[string]resource.Resource{
|
||||
"consul_keys": resource.Resource{
|
||||
ConfigValidator: resource_consul_keys_validation(),
|
||||
Create: resource_consul_keys_create,
|
||||
Destroy: resource_consul_keys_destroy,
|
||||
Update: resource_consul_keys_update,
|
||||
Diff: resource_consul_keys_diff,
|
||||
Refresh: resource_consul_keys_refresh,
|
||||
Create: resource_consul_keys_create,
|
||||
Destroy: resource_consul_keys_destroy,
|
||||
Update: resource_consul_keys_update,
|
||||
Diff: resource_consul_keys_diff,
|
||||
Refresh: resource_consul_keys_refresh,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue