Update `consul_catalog_nodes` to conform to normal resource guidelines.

This commit is contained in:
Sean Chittenden 2017-02-10 14:11:55 -08:00
parent 5567732814
commit 1476445593
No known key found for this signature in database
GPG Key ID: 4EBC9DC16C2E5E16
4 changed files with 265 additions and 432 deletions

View File

@ -9,315 +9,113 @@ import (
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
// Top-level consul_catalog_nodes attributes
const ( const (
catalogNodes typeKey = iota allowStale = "allow_stale"
catalogNodesAllowStale nodeMeta = "node_meta"
catalogNodesDatacenter nodesAttr = "nodes"
catalogNodesNear requireConsistent = "require_consistent"
catalogNodesRequireConsistent token = "token"
catalogNodesToken waitIndex = "wait_index"
catalogNodesWaitIndex waitTime = "wait_time"
catalogNodesWaitTime
nodeID = "id"
nodeAddress = "address"
nodeMetaAttr = "meta"
nodeName = "name"
nodeTaggedAddresses = "tagged_addresses"
apiTaggedLAN = "lan"
apiTaggedWAN = "wan"
schemaTaggedLAN = "lan"
schemaTaggedWAN = "wan"
) )
// node.* attributes
const (
catalogNodeID typeKey = iota
catalogNodeName
catalogNodeAddress
catalogNodeTaggedAddresses
catalogNodeMeta
)
// node.tagged_addresses.* attributes
const (
catalogNodeTaggedAddressesLAN typeKey = iota
catalogNodeTaggedAddressesWAN
)
var catalogNodeAttrs = map[typeKey]*typeEntry{
catalogNodeID: {
APIName: "ID",
SchemaName: "id",
Source: sourceAPIResult,
Type: schema.TypeString,
ValidateFuncs: []interface{}{
validateRegexp(`^[\S]+$`),
},
APITest: func(e *typeEntry, v interface{}) (interface{}, bool) {
node := v.(*consulapi.Node)
if id := node.ID; id != "" {
return id, true
}
// Use the node name - confusingly stored in the Node attribute - if no ID
// is available.
if name := node.Node; name != "" {
return name, true
}
return "", false
},
},
catalogNodeName: {
APIName: "Name",
SchemaName: "name",
Source: sourceAPIResult,
Type: schema.TypeString,
ValidateFuncs: []interface{}{
validateRegexp(`^[\S]+$`),
},
APITest: func(e *typeEntry, v interface{}) (interface{}, bool) {
node := v.(*consulapi.Node)
if name := node.Node; name != "" {
return name, true
}
return "", false
},
},
catalogNodeAddress: {
APIName: "Address",
SchemaName: "address",
Source: sourceAPIResult,
Type: schema.TypeString,
APITest: func(e *typeEntry, v interface{}) (interface{}, bool) {
node := v.(*consulapi.Node)
if addr := node.Address; addr != "" {
return addr, true
}
return "", false
},
},
catalogNodeTaggedAddresses: {
APIName: "TaggedAddresses",
SchemaName: "tagged_addresses",
Source: sourceAPIResult,
Type: schema.TypeMap,
SetMembers: map[typeKey]*typeEntry{
catalogNodeTaggedAddressesLAN: {
APIName: "LAN",
SchemaName: "lan",
Source: sourceAPIResult,
Type: schema.TypeString,
APITest: func(e *typeEntry, v interface{}) (interface{}, bool) {
m := v.(map[string]string)
if addr, found := m[string(e.SchemaName)]; found {
return addr, true
}
return nil, false
},
},
catalogNodeTaggedAddressesWAN: {
APIName: "WAN",
SchemaName: "wan",
Source: sourceAPIResult,
Type: schema.TypeString,
APITest: func(e *typeEntry, v interface{}) (interface{}, bool) {
m := v.(map[string]string)
if addr, found := m[string(e.SchemaName)]; found {
return addr, true
}
return nil, false
},
},
},
APITest: func(e *typeEntry, v interface{}) (interface{}, bool) {
node := v.(*consulapi.Node)
if addrs := node.TaggedAddresses; len(addrs) > 0 {
return mapStringToMapInterface(addrs), true
}
return nil, false
},
},
catalogNodeMeta: {
APIName: "Meta",
SchemaName: "meta",
Source: sourceAPIResult,
Type: schema.TypeMap,
APITest: func(e *typeEntry, v interface{}) (interface{}, bool) {
node := v.(*consulapi.Node)
if meta := node.Meta; len(meta) > 0 {
return mapStringToMapInterface(meta), true
}
return nil, false
},
},
}
var catalogNodesAttrs = map[typeKey]*typeEntry{
catalogNodesAllowStale: {
SchemaName: "allow_stale",
Source: sourceLocalFilter,
Type: schema.TypeBool,
Default: true,
ConfigRead: func(e *typeEntry, r attrReader) (interface{}, bool) {
b, ok := r.GetBoolOK(e.SchemaName)
if !ok {
return nil, false
}
return b, true
},
ConfigUse: func(e *typeEntry, v interface{}, target interface{}) error {
b := v.(bool)
queryOpts := target.(*consulapi.QueryOptions)
queryOpts.AllowStale = b
return nil
},
},
catalogNodesDatacenter: {
SchemaName: "datacenter",
Source: sourceLocalFilter,
Type: schema.TypeString,
ConfigRead: func(e *typeEntry, r attrReader) (interface{}, bool) {
s, ok := r.GetStringOK(e.SchemaName)
if !ok {
return nil, false
}
return s, true
},
ConfigUse: func(e *typeEntry, v interface{}, target interface{}) error {
s := v.(string)
queryOpts := target.(*consulapi.QueryOptions)
queryOpts.Datacenter = s
return nil
},
},
catalogNodesNear: {
SchemaName: "near",
Source: sourceLocalFilter,
Type: schema.TypeString,
ConfigRead: func(e *typeEntry, r attrReader) (interface{}, bool) {
s, ok := r.GetStringOK(e.SchemaName)
if !ok {
return nil, false
}
return s, true
},
ConfigUse: func(e *typeEntry, v interface{}, target interface{}) error {
s := v.(string)
queryOpts := target.(*consulapi.QueryOptions)
queryOpts.Near = s
return nil
},
},
catalogNodes: {
SchemaName: "nodes",
Source: sourceAPIResult,
Type: schema.TypeList,
ListSchema: catalogNodeAttrs,
},
catalogNodesRequireConsistent: {
SchemaName: "require_consistent",
Source: sourceLocalFilter,
Type: schema.TypeBool,
Default: false,
ConfigRead: func(e *typeEntry, r attrReader) (interface{}, bool) {
b, ok := r.GetBoolOK(e.SchemaName)
if !ok {
return nil, false
}
return b, true
},
ConfigUse: func(e *typeEntry, v interface{}, target interface{}) error {
b := v.(bool)
queryOpts := target.(*consulapi.QueryOptions)
queryOpts.RequireConsistent = b
return nil
},
},
catalogNodesToken: {
SchemaName: "token",
Source: sourceLocalFilter,
Type: schema.TypeString,
ConfigRead: func(e *typeEntry, r attrReader) (interface{}, bool) {
s, ok := r.GetStringOK(e.SchemaName)
if !ok {
return nil, false
}
return s, true
},
ConfigUse: func(e *typeEntry, v interface{}, target interface{}) error {
s := v.(string)
queryOpts := target.(*consulapi.QueryOptions)
queryOpts.Token = s
return nil
},
},
catalogNodesWaitIndex: {
SchemaName: "wait_index",
Source: sourceLocalFilter,
Type: schema.TypeInt,
ValidateFuncs: []interface{}{
validateIntMin(0),
},
ConfigRead: func(e *typeEntry, r attrReader) (interface{}, bool) {
i, ok := r.GetIntOK(e.SchemaName)
if !ok {
return nil, false
}
return uint64(i), true
},
ConfigUse: func(e *typeEntry, v interface{}, target interface{}) error {
i := v.(uint64)
queryOpts := target.(*consulapi.QueryOptions)
queryOpts.WaitIndex = i
return nil
},
},
catalogNodesWaitTime: {
SchemaName: "wait_time",
Source: sourceLocalFilter,
Type: schema.TypeString,
ValidateFuncs: []interface{}{
validateDurationMin("0ns"),
},
ConfigRead: func(e *typeEntry, r attrReader) (interface{}, bool) {
d, ok := r.GetDurationOK(e.SchemaName)
if !ok {
return nil, false
}
return d, true
},
ConfigUse: func(e *typeEntry, v interface{}, target interface{}) error {
d := v.(time.Duration)
queryOpts := target.(*consulapi.QueryOptions)
queryOpts.WaitTime = d
return nil
},
},
}
func dataSourceConsulCatalogNodes() *schema.Resource { func dataSourceConsulCatalogNodes() *schema.Resource {
return &schema.Resource{ return &schema.Resource{
Read: dataSourceConsulCatalogNodesRead, Read: dataSourceConsulCatalogNodesRead,
Schema: typeEntryMapToSchema(catalogNodesAttrs), Schema: map[string]*schema.Schema{
allowStale: &schema.Schema{
Optional: true,
Default: true,
Type: schema.TypeBool,
},
nodesAttr: &schema.Schema{
Computed: true,
Type: schema.TypeList,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
nodeID: &schema.Schema{
Type: schema.TypeString,
Computed: true,
ValidateFunc: makeValidationFunc(nodeID, []interface{}{validateRegexp(`^[\S]+$`)}),
},
nodeName: &schema.Schema{
Type: schema.TypeString,
Computed: true,
ValidateFunc: makeValidationFunc(nodeName, []interface{}{validateRegexp(`^[\S]+$`)}),
},
nodeAddress: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
nodeMetaAttr: &schema.Schema{
Type: schema.TypeMap,
Computed: true,
},
nodeTaggedAddresses: &schema.Schema{
Type: schema.TypeMap,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
schemaTaggedLAN: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
schemaTaggedWAN: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
},
},
},
},
},
requireConsistent: &schema.Schema{
Optional: true,
Default: false,
Type: schema.TypeBool,
},
token: &schema.Schema{
Optional: true,
Default: true,
Type: schema.TypeString,
},
waitIndex: &schema.Schema{
Optional: true,
Default: true,
Type: schema.TypeInt,
ValidateFunc: makeValidationFunc(waitIndex, []interface{}{
validateIntMin(0),
}),
},
waitTime: &schema.Schema{
Optional: true,
Default: true,
Type: schema.TypeString,
ValidateFunc: makeValidationFunc(waitTime, []interface{}{
validateDurationMin("0ns"),
}),
},
},
} }
} }
func dataSourceConsulCatalogNodesRead(d *schema.ResourceData, meta interface{}) error { func dataSourceConsulCatalogNodesRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client) client := meta.(*consulapi.Client)
// Parse out data source filters to populate Consul's query options
dc, err := getDC(d, client) dc, err := getDC(d, client)
if err != nil { if err != nil {
return err return err
@ -327,20 +125,34 @@ func dataSourceConsulCatalogNodesRead(d *schema.ResourceData, meta interface{})
Datacenter: dc, Datacenter: dc,
} }
cfgReader := newConfigReader(d) if v, ok := d.GetOk(allowStale); ok {
queryOpts.AllowStale = v.(bool)
// Construct the query options
for _, e := range catalogNodesAttrs[catalogNodes].ListSchema {
// Only evaluate attributes that impact the state
if e.Source&sourceLocalFilter == 0 {
continue
} }
if v, ok := e.ConfigRead(e, cfgReader); ok { if v, ok := d.GetOk(requireConsistent); ok {
if err := e.ConfigUse(e, v, queryOpts); err != nil { queryOpts.RequireConsistent = v.(bool)
return errwrap.Wrapf(fmt.Sprintf("error writing %q's query option: {{err}}", e.SchemaName), err)
} }
if v, ok := d.GetOk(nodeMeta); ok {
m := v.(map[string]interface{})
nodeMetaMap := make(map[string]string, len(nodeMeta))
for s, t := range m {
nodeMetaMap[s] = t.(string)
} }
queryOpts.NodeMeta = nodeMetaMap
}
if v, ok := d.GetOk(token); ok {
queryOpts.Token = v.(string)
}
if v, ok := d.GetOk(waitIndex); ok {
queryOpts.WaitIndex = uint64(v.(int))
}
if v, ok := d.GetOk(waitTime); ok {
d, _ := time.ParseDuration(v.(string))
queryOpts.WaitTime = d
} }
nodes, meta, err := client.Catalog().Nodes(queryOpts) nodes, meta, err := client.Catalog().Nodes(queryOpts)
@ -348,41 +160,51 @@ func dataSourceConsulCatalogNodesRead(d *schema.ResourceData, meta interface{})
return err return err
} }
// TODO(sean@): It'd be nice if this data source had a way of filtering out
// irrelevant data so only the important bits are persisted in the state file.
// Something like an attribute mask or even a regexp of matching schema
// attributesknames would be sufficient in the most basic case. Food for
// thought.
l := make([]interface{}, 0, len(nodes)) l := make([]interface{}, 0, len(nodes))
for _, node := range nodes { for _, node := range nodes {
mWriter := newMapWriter(make(map[string]interface{}, len(catalogNodeAttrs))) const defaultNodeAttrs = 4
m := make(map[string]interface{}, defaultNodeAttrs)
// /v1/catalog/nodes returns a list of node objects id := node.ID
for _, e := range catalogNodesAttrs[catalogNodes].ListSchema { if id == "" {
// Only evaluate attributes that impact the state id = node.Node
if e.Source&modifyState == 0 {
continue
} }
h := e.MustLookupTypeHandler() m[nodeID] = id
m[nodeName] = node.Node
m[nodeAddress] = node.Address
if v, ok := h.APITest(e, node); ok { {
if err := h.APIToState(e, v, mWriter); err != nil { const initNumTaggedAddrs = 2
return errwrap.Wrapf(fmt.Sprintf("error writing %q's data to state: {{err}}", e.SchemaName), err) taggedAddrs := make(map[string]interface{}, initNumTaggedAddrs)
if addr, found := node.TaggedAddresses[apiTaggedLAN]; found {
taggedAddrs[schemaTaggedLAN] = addr
} }
if addr, found := node.TaggedAddresses[apiTaggedWAN]; found {
taggedAddrs[schemaTaggedWAN] = addr
} }
m[nodeTaggedAddresses] = taggedAddrs
} }
l = append(l, mWriter.ToMap()) {
const initNumMetaAddrs = 4
metaVals := make(map[string]interface{}, initNumMetaAddrs)
for s, t := range node.Meta {
metaVals[s] = t
}
m[nodeMetaAttr] = metaVals
}
l = append(l, m)
} }
dataSourceWriter := newStateWriter(d)
dataSourceWriter.SetList(catalogNodesAttrs[catalogNodes].SchemaName, l)
dataSourceWriter.SetString(catalogNodesAttrs[catalogNodesDatacenter].SchemaName, dc)
const idKeyFmt = "catalog-nodes-%s" const idKeyFmt = "catalog-nodes-%s"
dataSourceWriter.SetID(fmt.Sprintf(idKeyFmt, dc)) d.SetId(fmt.Sprintf(idKeyFmt, dc))
d.Set("datacenter", dc)
if err := d.Set(nodesAttr, l); err != nil {
return errwrap.Wrapf("Unable to store nodes: {{err}}", err)
}
return nil return nil
} }

View File

@ -28,7 +28,6 @@ const testAccDataConsulCatalogNodesConfig = `
data "consul_catalog_nodes" "read" { data "consul_catalog_nodes" "read" {
allow_stale = true allow_stale = true
require_consistent = false require_consistent = false
near = "_agent"
token = "" token = ""
wait_index = 0 wait_index = 0
wait_time = "1m" wait_time = "1m"

View File

@ -3,10 +3,8 @@ package consul
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"regexp"
"sort" "sort"
"strconv" "strconv"
"time"
"github.com/hashicorp/errwrap" "github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
@ -28,19 +26,6 @@ type sourceFlags int
// typeKey is the lookup mechanism for the generated schema. // typeKey is the lookup mechanism for the generated schema.
type typeKey int type typeKey int
// An array of inputs used as typed arguments and converted from their type into
// function objects that are dynamically constructed and executed.
type validatorInputs []interface{}
// validateDurationMin is the minimum duration to accept as input
type validateDurationMin string
// validateIntMin is the minimum integer value to accept as input
type validateIntMin int
// validateRegexp is a regexp pattern to use to validate schema input.
type validateRegexp string
const ( const (
// sourceUserRequired indicates the parameter must be provided by the user in // sourceUserRequired indicates the parameter must be provided by the user in
// their configuration. // their configuration.
@ -509,37 +494,6 @@ func (e *typeEntry) Validate() {
} }
} }
// MakeValidateionFunc takes a list of typed validator inputs from the receiver
// and creates a validation closure that calls each validator in serial until
// either a warning or error is returned from the first validation function.
func (e *typeEntry) MakeValidationFunc() func(v interface{}, key string) (warnings []string, errors []error) {
if len(e.ValidateFuncs) == 0 {
return nil
}
fns := make([]func(v interface{}, key string) (warnings []string, errors []error), 0, len(e.ValidateFuncs))
for _, v := range e.ValidateFuncs {
switch u := v.(type) {
case validateDurationMin:
fns = append(fns, validateDurationMinFactory(e, string(u)))
case validateIntMin:
fns = append(fns, validateIntMinFactory(e, int(u)))
case validateRegexp:
fns = append(fns, validateRegexpFactory(e, string(u)))
}
}
return func(v interface{}, key string) (warnings []string, errors []error) {
for _, fn := range fns {
warnings, errors = fn(v, key)
if len(warnings) > 0 || len(errors) > 0 {
break
}
}
return warnings, errors
}
}
func (e *typeEntry) ToSchema() *schema.Schema { func (e *typeEntry) ToSchema() *schema.Schema {
e.Validate() e.Validate()
@ -550,7 +504,7 @@ func (e *typeEntry) ToSchema() *schema.Schema {
Optional: e.Source&optionalAttrMask != 0, Optional: e.Source&optionalAttrMask != 0,
Required: e.Source&requiredAttrMask != 0, Required: e.Source&requiredAttrMask != 0,
Type: e.Type, Type: e.Type,
ValidateFunc: e.MakeValidationFunc(), // ValidateFunc: e.MakeValidationFunc(),
} }
// Fixup the type: use the real type vs a surrogate type // Fixup the type: use the real type vs a surrogate type
@ -580,45 +534,3 @@ func mapStringToMapInterface(in map[string]string) map[string]interface{} {
} }
return out return out
} }
func validateDurationMinFactory(e *typeEntry, minDuration string) func(v interface{}, key string) (warnings []string, errors []error) {
dMin, err := time.ParseDuration(minDuration)
if err != nil {
panic(fmt.Sprintf("PROVIDER BUG: duration %q not valid: %#v", minDuration, err))
}
return func(v interface{}, key string) (warnings []string, errors []error) {
d, err := time.ParseDuration(v.(string))
if err != nil {
errors = append(errors, errwrap.Wrapf(fmt.Sprintf("Invalid %s specified (%q): {{err}}", e.SchemaName), err))
}
if d < dMin {
errors = append(errors, fmt.Errorf("Invalid %s specified: duration %q less than the required minimum %s", e.SchemaName, v.(string), dMin))
}
return warnings, errors
}
}
func validateIntMinFactory(e *typeEntry, min int) func(v interface{}, key string) (warnings []string, errors []error) {
return func(v interface{}, key string) (warnings []string, errors []error) {
if v.(int) < min {
errors = append(errors, fmt.Errorf("Invalid %s specified: %d less than the required minimum %d", e.SchemaName, v.(int), min))
}
return warnings, errors
}
}
func validateRegexpFactory(e *typeEntry, reString string) func(v interface{}, key string) (warnings []string, errors []error) {
re := regexp.MustCompile(reString)
return func(v interface{}, key string) (warnings []string, errors []error) {
if !re.MatchString(v.(string)) {
errors = append(errors, fmt.Errorf("Invalid %s specified (%q): regexp failed to match string", e.SchemaName, v.(string)))
}
return warnings, errors
}
}

View File

@ -0,0 +1,100 @@
package consul
import (
"fmt"
"regexp"
"time"
"github.com/hashicorp/errwrap"
)
// An array of inputs used as typed arguments and converted from their type into
// function objects that are dynamically constructed and executed.
type validatorInputs []interface{}
// validateDurationMin is the minimum duration to accept as input
type validateDurationMin string
// validateIntMin is the minimum integer value to accept as input
type validateIntMin int
// validateRegexp is a regexp pattern to use to validate schema input.
type validateRegexp string
// makeValidateionFunc takes the name of the attribute and a list of typed
// validator inputs in order to create a validation closure that calls each
// validator in serial until either a warning or error is returned from the
// first validation function.
func makeValidationFunc(name string, validators []interface{}) func(v interface{}, key string) (warnings []string, errors []error) {
if len(validators) == 0 {
return nil
}
fns := make([]func(v interface{}, key string) (warnings []string, errors []error), 0, len(validators))
for _, v := range validators {
switch u := v.(type) {
case validateDurationMin:
fns = append(fns, validateDurationMinFactory(name, string(u)))
case validateIntMin:
fns = append(fns, validateIntMinFactory(name, int(u)))
case validateRegexp:
fns = append(fns, validateRegexpFactory(name, string(u)))
}
}
return func(v interface{}, key string) (warnings []string, errors []error) {
for _, fn := range fns {
warnings, errors = fn(v, key)
if len(warnings) > 0 || len(errors) > 0 {
break
}
}
return warnings, errors
}
}
func validateDurationMinFactory(name, minDuration string) func(v interface{}, key string) (warnings []string, errors []error) {
dMin, err := time.ParseDuration(minDuration)
if err != nil {
return func(interface{}, string) (warnings []string, errors []error) {
return nil, []error{
errwrap.Wrapf(fmt.Sprintf("PROVIDER BUG: duration %q not valid: {{err}}", minDuration), err),
}
}
}
return func(v interface{}, key string) (warnings []string, errors []error) {
d, err := time.ParseDuration(v.(string))
if err != nil {
errors = append(errors, errwrap.Wrapf(fmt.Sprintf("Invalid %s specified (%q): {{err}}", name), err))
}
if d < dMin {
errors = append(errors, fmt.Errorf("Invalid %s specified: duration %q less than the required minimum %s", name, v.(string), dMin))
}
return warnings, errors
}
}
func validateIntMinFactory(name string, min int) func(v interface{}, key string) (warnings []string, errors []error) {
return func(v interface{}, key string) (warnings []string, errors []error) {
if v.(int) < min {
errors = append(errors, fmt.Errorf("Invalid %s specified: %d less than the required minimum %d", name, v.(int), min))
}
return warnings, errors
}
}
func validateRegexpFactory(name string, reString string) func(v interface{}, key string) (warnings []string, errors []error) {
re := regexp.MustCompile(reString)
return func(v interface{}, key string) (warnings []string, errors []error) {
if !re.MatchString(v.(string)) {
errors = append(errors, fmt.Errorf("Invalid %s specified (%q): regexp failed to match string", name, v.(string)))
}
return warnings, errors
}
}