diff --git a/builtin/providers/circonus/check.go b/builtin/providers/circonus/check.go index 6200e94bf..8058b315e 100644 --- a/builtin/providers/circonus/check.go +++ b/builtin/providers/circonus/check.go @@ -25,6 +25,7 @@ const ( const ( apiCheckTypeCAQL circonusCheckType = "caql" + apiCheckTypeConsul circonusCheckType = "consul" apiCheckTypeICMPPing circonusCheckType = "ping_icmp" apiCheckTypeHTTP circonusCheckType = "http" apiCheckTypeJSON circonusCheckType = "json" @@ -108,15 +109,24 @@ func (c *circonusCheck) Fixup() error { } func (c *circonusCheck) Validate() error { + if len(c.Metrics) == 0 { + return fmt.Errorf("At least one %s must be specified", checkMetricAttr) + } + if c.Timeout > float32(c.Period) { return fmt.Errorf("Timeout (%f) can not exceed period (%d)", c.Timeout, c.Period) } + // Check-type specific validation switch apiCheckType(c.Type) { case apiCheckTypeCloudWatchAttr: if !(c.Period == 60 || c.Period == 300) { return fmt.Errorf("Period must be either 1m or 5m for a %s check", apiCheckTypeCloudWatchAttr) } + case apiCheckTypeConsulAttr: + if v, found := c.Config[config.URL]; !found || v == "" { + return fmt.Errorf("%s must have at least one check mode set: %s, %s, or %s must be set", checkConsulAttr, checkConsulServiceAttr, checkConsulNodeAttr, checkConsulStateAttr) + } } return nil diff --git a/builtin/providers/circonus/consts.go b/builtin/providers/circonus/consts.go index 6b505482a..9dd0d248f 100644 --- a/builtin/providers/circonus/consts.go +++ b/builtin/providers/circonus/consts.go @@ -17,6 +17,19 @@ const ( providerAutoTagAttr = "auto_tag" providerKeyAttr = "key" + apiConsulCheckBlacklist = "check_name_blacklist" + apiConsulDatacenterAttr = "dc" + apiConsulNodeBlacklist = "node_blacklist" + apiConsulServiceBlacklist = "service_blacklist" + apiConsulStaleAttr = "stale" + checkConsulTokenHeader = `X-Consul-Token` + checkConsulV1NodePrefix = "node" + checkConsulV1Prefix = "/v1/health" + checkConsulV1ServicePrefix = "service" + checkConsulV1StatePrefix = "state" + defaultCheckConsulHTTPAddr = "http://consul.service.consul" + defaultCheckConsulPort = "8500" + defaultCheckJSONMethod = "GET" defaultCheckJSONPort = "443" defaultCheckJSONVersion = "1.1" diff --git a/builtin/providers/circonus/resource_circonus_check.go b/builtin/providers/circonus/resource_circonus_check.go index 0c2b6d501..06bf2e9cf 100644 --- a/builtin/providers/circonus/resource_circonus_check.go +++ b/builtin/providers/circonus/resource_circonus_check.go @@ -33,21 +33,22 @@ const ( checkCAQLAttr = "caql" checkCloudWatchAttr = "cloudwatch" checkCollectorAttr = "collector" + checkConsulAttr = "consul" checkHTTPAttr = "http" checkHTTPTrapAttr = "httptrap" checkICMPPingAttr = "icmp_ping" checkJSONAttr = "json" + checkMetricAttr = "metric" checkMetricLimitAttr = "metric_limit" checkMySQLAttr = "mysql" checkNameAttr = "name" checkNotesAttr = "notes" checkPeriodAttr = "period" checkPostgreSQLAttr = "postgresql" - checkMetricAttr = "metric" checkStatsdAttr = "statsd" + checkTCPAttr = "tcp" checkTagsAttr = "tags" checkTargetAttr = "target" - checkTCPAttr = "tcp" checkTimeoutAttr = "timeout" checkTypeAttr = "type" @@ -75,6 +76,7 @@ const ( // Circonus API constants from their API endpoints apiCheckTypeCAQLAttr apiCheckType = "caql" apiCheckTypeCloudWatchAttr apiCheckType = "cloudwatch" + apiCheckTypeConsulAttr apiCheckType = "consul" apiCheckTypeHTTPAttr apiCheckType = "http" apiCheckTypeHTTPTrapAttr apiCheckType = "httptrap" apiCheckTypeICMPPingAttr apiCheckType = "ping_icmp" @@ -90,6 +92,7 @@ var checkDescriptions = attrDescrs{ checkCAQLAttr: "CAQL check configuration", checkCloudWatchAttr: "CloudWatch check configuration", checkCollectorAttr: "The collector(s) that are responsible for gathering the metrics", + checkConsulAttr: "Consul check configuration", checkHTTPAttr: "HTTP check configuration", checkHTTPTrapAttr: "HTTP Trap check configuration", checkICMPPingAttr: "ICMP ping check configuration", @@ -157,6 +160,7 @@ func resourceCheck() *schema.Resource { }), }, }, + checkConsulAttr: schemaCheckConsul, checkHTTPAttr: schemaCheckHTTP, checkHTTPTrapAttr: schemaCheckHTTPTrap, checkJSONAttr: schemaCheckJSON, @@ -577,6 +581,7 @@ func checkConfigToAPI(c *circonusCheck, d *schema.ResourceData) error { checkTypeParseMap := map[string]func(*circonusCheck, interfaceList) error{ checkCAQLAttr: checkConfigToAPICAQL, checkCloudWatchAttr: checkConfigToAPICloudWatch, + checkConsulAttr: checkConfigToAPIConsul, checkHTTPAttr: checkConfigToAPIHTTP, checkHTTPTrapAttr: checkConfigToAPIHTTPTrap, checkICMPPingAttr: checkConfigToAPIICMPPing, @@ -589,8 +594,17 @@ func checkConfigToAPI(c *circonusCheck, d *schema.ResourceData) error { for checkType, fn := range checkTypeParseMap { if listRaw, found := d.GetOk(checkType); found { - if err := fn(c, listRaw.(*schema.Set).List()); err != nil { - return errwrap.Wrapf(fmt.Sprintf("Unable to parse type %q: {{err}}", string(checkType)), err) + switch u := listRaw.(type) { + case []interface{}: + if err := fn(c, u); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to parse type %q: {{err}}", string(checkType)), err) + } + case *schema.Set: + if err := fn(c, u.List()); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to parse type %q: {{err}}", string(checkType)), err) + } + default: + return fmt.Errorf("PROVIDER BUG: unsupported check type interface: %q", checkType) } } } @@ -604,6 +618,7 @@ func parseCheckTypeConfig(c *circonusCheck, d *schema.ResourceData) error { checkTypeConfigHandlers := map[apiCheckType]func(*circonusCheck, *schema.ResourceData) error{ apiCheckTypeCAQLAttr: checkAPIToStateCAQL, apiCheckTypeCloudWatchAttr: checkAPIToStateCloudWatch, + apiCheckTypeConsulAttr: checkAPIToStateConsul, apiCheckTypeHTTPAttr: checkAPIToStateHTTP, apiCheckTypeHTTPTrapAttr: checkAPIToStateHTTPTrap, apiCheckTypeICMPPingAttr: checkAPIToStateICMPPing, diff --git a/builtin/providers/circonus/resource_circonus_check_consul.go b/builtin/providers/circonus/resource_circonus_check_consul.go new file mode 100644 index 000000000..dd3f496c5 --- /dev/null +++ b/builtin/providers/circonus/resource_circonus_check_consul.go @@ -0,0 +1,412 @@ +package circonus + +import ( + "fmt" + "net" + "net/url" + "regexp" + "strings" + + "github.com/circonus-labs/circonus-gometrics/api/config" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/helper/schema" +) + +const ( + // circonus_check.consul.* resource attribute names + checkConsulACLTokenAttr = "acl_token" + checkConsulAllowStaleAttr = "allow_stale" + checkConsulCAChainAttr = "ca_chain" + checkConsulCertFileAttr = "certificate_file" + checkConsulCheckNameBlacklistAttr = "check_blacklist" + checkConsulCiphersAttr = "ciphers" + checkConsulDatacenterAttr = "dc" + checkConsulHTTPAddrAttr = "http_addr" + checkConsulHeadersAttr = "headers" + checkConsulKeyFileAttr = "key_file" + checkConsulNodeAttr = "node" + checkConsulNodeBlacklistAttr = "node_blacklist" + checkConsulServiceAttr = "service" + checkConsulServiceNameBlacklistAttr = "service_blacklist" + checkConsulStateAttr = "state" +) + +var checkConsulDescriptions = attrDescrs{ + checkConsulACLTokenAttr: "A Consul ACL token", + checkConsulAllowStaleAttr: "Allow Consul to read from a non-leader system", + checkConsulCAChainAttr: "A path to a file containing all the certificate authorities that should be loaded to validate the remote certificate (for TLS checks)", + checkConsulCertFileAttr: "A path to a file containing the client certificate that will be presented to the remote server (for TLS-enabled checks)", + checkConsulCheckNameBlacklistAttr: "A blacklist of check names to exclude from metric results", + checkConsulCiphersAttr: "A list of ciphers to be used in the TLS protocol (for HTTPS checks)", + checkConsulDatacenterAttr: "The Consul datacenter to extract health information from", + checkConsulHeadersAttr: "Map of HTTP Headers to send along with HTTP Requests", + checkConsulHTTPAddrAttr: "The HTTP Address of a Consul agent to query", + checkConsulKeyFileAttr: "A path to a file containing key to be used in conjunction with the cilent certificate (for TLS checks)", + checkConsulNodeAttr: "Node Name or NodeID of a Consul agent", + checkConsulNodeBlacklistAttr: "A blacklist of node names or IDs to exclude from metric results", + checkConsulServiceAttr: "Name of the Consul service to check", + checkConsulServiceNameBlacklistAttr: "A blacklist of service names to exclude from metric results", + checkConsulStateAttr: "Check for Consul services in this particular state", +} + +var consulHealthCheckRE = regexp.MustCompile(fmt.Sprintf(`^%s/(%s|%s|%s)/(.+)`, checkConsulV1Prefix, checkConsulV1NodePrefix, checkConsulV1ServicePrefix, checkConsulV1StatePrefix)) + +var schemaCheckConsul = &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: convertToHelperSchema(checkConsulDescriptions, map[schemaAttr]*schema.Schema{ + checkConsulACLTokenAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(checkConsulACLTokenAttr, `^[a-zA-Z0-9\-]+$`), + }, + checkConsulAllowStaleAttr: &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + checkConsulCAChainAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(checkConsulCAChainAttr, `.+`), + }, + checkConsulCertFileAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(checkConsulCertFileAttr, `.+`), + }, + checkConsulCheckNameBlacklistAttr: &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateRegexp(checkConsulCheckNameBlacklistAttr, `^[A-Za-z0-9_-]+$`), + }, + }, + checkConsulCiphersAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(checkConsulCiphersAttr, `.+`), + }, + checkConsulDatacenterAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(checkConsulCertFileAttr, `^[a-zA-Z0-9]+$`), + }, + checkConsulHTTPAddrAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: defaultCheckConsulHTTPAddr, + ValidateFunc: validateHTTPURL(checkConsulHTTPAddrAttr, urlIsAbs|urlWithoutPath), + }, + checkConsulHeadersAttr: &schema.Schema{ + Type: schema.TypeMap, + Elem: schema.TypeString, + Optional: true, + ValidateFunc: validateHTTPHeaders, + }, + checkConsulKeyFileAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(checkConsulKeyFileAttr, `.+`), + }, + checkConsulNodeAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(checkConsulNodeAttr, `^[a-zA-Z0-9_\-]+$`), + ConflictsWith: []string{ + checkConsulAttr + "." + checkConsulServiceAttr, + checkConsulAttr + "." + checkConsulStateAttr, + }, + }, + checkConsulNodeBlacklistAttr: &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateRegexp(checkConsulNodeBlacklistAttr, `^[A-Za-z0-9_-]+$`), + }, + }, + checkConsulServiceAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(checkConsulServiceAttr, `^[a-zA-Z0-9_\-]+$`), + ConflictsWith: []string{ + checkConsulAttr + "." + checkConsulNodeAttr, + checkConsulAttr + "." + checkConsulStateAttr, + }, + }, + checkConsulServiceNameBlacklistAttr: &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateRegexp(checkConsulServiceNameBlacklistAttr, `^[A-Za-z0-9_-]+$`), + }, + }, + checkConsulStateAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(checkConsulStateAttr, `^(any|passing|warning|critical)$`), + ConflictsWith: []string{ + checkConsulAttr + "." + checkConsulNodeAttr, + checkConsulAttr + "." + checkConsulServiceAttr, + }, + }, + }), + }, +} + +// checkAPIToStateConsul reads the Config data out of circonusCheck.CheckBundle into +// the statefile. +func checkAPIToStateConsul(c *circonusCheck, d *schema.ResourceData) error { + consulConfig := make(map[string]interface{}, len(c.Config)) + + // swamp is a sanity check: it must be empty by the time this method returns + swamp := make(map[config.Key]string, len(c.Config)) + for k, s := range c.Config { + swamp[k] = s + } + + saveStringConfigToState := func(apiKey config.Key, attrName schemaAttr) { + if s, ok := c.Config[apiKey]; ok && s != "" { + consulConfig[string(attrName)] = s + } + + delete(swamp, apiKey) + } + + saveStringConfigToState(config.CAChain, checkConsulCAChainAttr) + saveStringConfigToState(config.CertFile, checkConsulCertFileAttr) + saveStringConfigToState(config.Ciphers, checkConsulCiphersAttr) + + // httpAddrURL is used to compose the http_addr value using multiple c.Config + // values. + var httpAddrURL url.URL + + headers := make(map[string]interface{}, len(c.Config)+1) // +1 is for the ACLToken + headerPrefixLen := len(config.HeaderPrefix) + + // Explicitly handle several config parameters in sequence: URL, then port, + // then everything else. + if v, found := c.Config[config.URL]; found { + u, err := url.Parse(v) + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("unable to parse %q from config: {{err}}", config.URL), err) + } + + queryArgs := u.Query() + if vals, found := queryArgs[apiConsulStaleAttr]; found && len(vals) > 0 { + consulConfig[string(checkConsulAllowStaleAttr)] = true + } + + if dc := queryArgs.Get(apiConsulDatacenterAttr); dc != "" { + consulConfig[string(checkConsulDatacenterAttr)] = dc + } + + httpAddrURL.Host = u.Host + httpAddrURL.Scheme = u.Scheme + + md := consulHealthCheckRE.FindStringSubmatch(u.EscapedPath()) + if md == nil { + return fmt.Errorf("config %q failed to match the health regexp", config.URL) + } + + checkMode := md[1] + checkArg := md[2] + switch checkMode { + case checkConsulV1NodePrefix: + consulConfig[string(checkConsulNodeAttr)] = checkArg + case checkConsulV1ServicePrefix: + consulConfig[string(checkConsulServiceAttr)] = checkArg + case checkConsulV1StatePrefix: + consulConfig[string(checkConsulStateAttr)] = checkArg + default: + return fmt.Errorf("PROVIDER BUG: unsupported check mode %q from %q", checkMode, u.EscapedPath()) + } + + delete(swamp, config.URL) + } + + if v, found := c.Config[config.Port]; found { + hostInfo := strings.SplitN(httpAddrURL.Host, ":", 2) + switch { + case len(hostInfo) == 1 && v != defaultCheckConsulPort, len(hostInfo) > 1: + httpAddrURL.Host = net.JoinHostPort(hostInfo[0], v) + } + + delete(swamp, config.Port) + } + + if v, found := c.Config[apiConsulCheckBlacklist]; found { + consulConfig[checkConsulCheckNameBlacklistAttr] = strings.Split(v, ",") + } + + if v, found := c.Config[apiConsulNodeBlacklist]; found { + consulConfig[checkConsulNodeBlacklistAttr] = strings.Split(v, ",") + } + + if v, found := c.Config[apiConsulServiceBlacklist]; found { + consulConfig[checkConsulServiceNameBlacklistAttr] = strings.Split(v, ",") + } + + // NOTE(sean@): headers attribute processed last. See below. + + consulConfig[string(checkConsulHTTPAddrAttr)] = httpAddrURL.String() + + saveStringConfigToState(config.KeyFile, checkConsulKeyFileAttr) + + // Process the headers last in order to provide an escape hatch capible of + // overriding any other derived value above. + for k, v := range c.Config { + if len(k) <= headerPrefixLen { + continue + } + + // Handle all of the prefix variable headers, like `header_` + if strings.Compare(string(k[:headerPrefixLen]), string(config.HeaderPrefix)) == 0 { + key := k[headerPrefixLen:] + switch key { + case checkConsulTokenHeader: + consulConfig[checkConsulACLTokenAttr] = v + default: + headers[string(key)] = v + } + } + + delete(swamp, k) + } + consulConfig[string(checkConsulHeadersAttr)] = headers + + whitelistedConfigKeys := map[config.Key]struct{}{ + config.Port: struct{}{}, + config.ReverseSecretKey: struct{}{}, + config.SubmissionURL: struct{}{}, + config.URL: struct{}{}, + } + + for k := range swamp { + if _, ok := whitelistedConfigKeys[k]; ok { + delete(c.Config, k) + } + + if _, ok := whitelistedConfigKeys[k]; !ok { + return fmt.Errorf("PROVIDER BUG: API Config not empty: %#v", swamp) + } + } + + if err := d.Set(checkConsulAttr, []interface{}{consulConfig}); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to store check %q attribute: {{err}}", checkConsulAttr), err) + } + + return nil +} + +func checkConfigToAPIConsul(c *circonusCheck, l interfaceList) error { + c.Type = string(apiCheckTypeConsul) + + // Iterate over all `consul` attributes, even though we have a max of 1 in the + // schema. + for _, mapRaw := range l { + consulConfig := newInterfaceMap(mapRaw) + if v, found := consulConfig[checkConsulCAChainAttr]; found { + c.Config[config.CAChain] = v.(string) + } + + if v, found := consulConfig[checkConsulCertFileAttr]; found { + c.Config[config.CertFile] = v.(string) + } + + if v, found := consulConfig[checkConsulCheckNameBlacklistAttr]; found { + listRaw := v.([]interface{}) + checks := make([]string, 0, len(listRaw)) + for _, v := range listRaw { + checks = append(checks, v.(string)) + } + c.Config[apiConsulCheckBlacklist] = strings.Join(checks, ",") + } + + if v, found := consulConfig[checkConsulCiphersAttr]; found { + c.Config[config.Ciphers] = v.(string) + } + + if headers := consulConfig.CollectMap(checkConsulHeadersAttr); headers != nil { + for k, v := range headers { + h := config.HeaderPrefix + config.Key(k) + c.Config[h] = v + } + } + + if v, found := consulConfig[checkConsulKeyFileAttr]; found { + c.Config[config.KeyFile] = v.(string) + } + + { + // Extract all of the input attributes necessary to construct the + // Consul agent's URL. + + httpAddr := consulConfig[checkConsulHTTPAddrAttr].(string) + checkURL, err := url.Parse(httpAddr) + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to parse %s's attribute %q: {{err}}", checkConsulAttr, httpAddr), err) + } + + hostInfo := strings.SplitN(checkURL.Host, ":", 2) + if len(c.Target) == 0 { + c.Target = hostInfo[0] + } + + if len(hostInfo) > 1 { + c.Config[config.Port] = hostInfo[1] + } + + if v, found := consulConfig[checkConsulNodeAttr]; found && v.(string) != "" { + checkURL.Path = strings.Join([]string{checkConsulV1Prefix, checkConsulV1NodePrefix, v.(string)}, "/") + } + + if v, found := consulConfig[checkConsulServiceAttr]; found && v.(string) != "" { + checkURL.Path = strings.Join([]string{checkConsulV1Prefix, checkConsulV1ServicePrefix, v.(string)}, "/") + } + + if v, found := consulConfig[checkConsulStateAttr]; found && v.(string) != "" { + checkURL.Path = strings.Join([]string{checkConsulV1Prefix, checkConsulV1StatePrefix, v.(string)}, "/") + } + + q := checkURL.Query() + + if v, found := consulConfig[checkConsulAllowStaleAttr]; found && v.(bool) { + q.Set(apiConsulStaleAttr, "") + } + + if v, found := consulConfig[checkConsulDatacenterAttr]; found && v.(string) != "" { + q.Set(apiConsulDatacenterAttr, v.(string)) + } + + checkURL.RawQuery = q.Encode() + + c.Config[config.URL] = checkURL.String() + } + + if v, found := consulConfig[checkConsulNodeBlacklistAttr]; found { + listRaw := v.([]interface{}) + checks := make([]string, 0, len(listRaw)) + for _, v := range listRaw { + checks = append(checks, v.(string)) + } + c.Config[apiConsulNodeBlacklist] = strings.Join(checks, ",") + } + + if v, found := consulConfig[checkConsulServiceNameBlacklistAttr]; found { + listRaw := v.([]interface{}) + checks := make([]string, 0, len(listRaw)) + for _, v := range listRaw { + checks = append(checks, v.(string)) + } + c.Config[apiConsulServiceBlacklist] = strings.Join(checks, ",") + } + } + + return nil +} diff --git a/builtin/providers/circonus/resource_circonus_check_consul_test.go b/builtin/providers/circonus/resource_circonus_check_consul_test.go new file mode 100644 index 000000000..f7ca7993d --- /dev/null +++ b/builtin/providers/circonus/resource_circonus_check_consul_test.go @@ -0,0 +1,282 @@ +package circonus + +import ( + "fmt" + "regexp" + "testing" + + "github.com/circonus-labs/circonus-gometrics/api/config" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccCirconusCheckConsul_node(t *testing.T) { + checkName := fmt.Sprintf("Terraform test: consul.service.consul mode=state check - %s", acctest.RandString(5)) + + checkNode := fmt.Sprintf("my-node-name-or-node-id-%s", acctest.RandString(5)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDestroyCirconusCheckBundle, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccCirconusCheckConsulConfigV1HealthNodeFmt, checkName, checkNode), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("circonus_check.consul_server", "active", "true"), + resource.TestMatchResourceAttr("circonus_check.consul_server", "check_id", regexp.MustCompile(config.CheckCIDRegex)), + resource.TestCheckResourceAttr("circonus_check.consul_server", "collector.#", "1"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "collector.2084916526.id", "/broker/2110"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.#", "1"), + // resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.ca_chain", ""), + // resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.certificate_file", ""), + // resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.ciphers", ""), + // resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.key_file", ""), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.dc", "dc2"), + resource.TestCheckNoResourceAttr("circonus_check.consul_server", "consul.0.headers"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.http_addr", "http://consul.service.consul:8501"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.node", checkNode), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.node_blacklist.#", "3"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.node_blacklist.0", "a"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.node_blacklist.1", "bad"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.node_blacklist.2", "node"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "notes", ""), + resource.TestCheckResourceAttr("circonus_check.consul_server", "period", "60s"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.#", "2"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.active", "true"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.name", "KnownLeader"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.tags.#", "2"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.tags.1401442048", "lifecycle:unittest"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.tags.2058715988", "source:consul"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.type", "text"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.active", "true"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.name", "LastContact"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.tags.#", "2"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.tags.1401442048", "lifecycle:unittest"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.tags.2058715988", "source:consul"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.type", "numeric"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.unit", "seconds"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "tags.#", "2"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "tags.1401442048", "lifecycle:unittest"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "tags.2058715988", "source:consul"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "target", "consul.service.consul"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "type", "consul"), + ), + }, + }, + }) +} + +func TestAccCirconusCheckConsul_service(t *testing.T) { + checkName := fmt.Sprintf("Terraform test: consul.service.consul mode=service check - %s", acctest.RandString(5)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDestroyCirconusCheckBundle, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccCirconusCheckConsulConfigV1HealthServiceFmt, checkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("circonus_check.consul_server", "active", "true"), + resource.TestMatchResourceAttr("circonus_check.consul_server", "check_id", regexp.MustCompile(config.CheckCIDRegex)), + resource.TestCheckResourceAttr("circonus_check.consul_server", "collector.#", "1"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "collector.2084916526.id", "/broker/2110"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.#", "1"), + // resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.ca_chain", ""), + // resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.certificate_file", ""), + // resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.ciphers", ""), + // resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.key_file", ""), + resource.TestCheckNoResourceAttr("circonus_check.consul_server", "consul.0.headers"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.http_addr", "http://consul.service.consul"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.service", "consul"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.service_blacklist.#", "3"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.service_blacklist.0", "bad"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.service_blacklist.1", "hombre"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.service_blacklist.2", "service"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "name", checkName), + resource.TestCheckResourceAttr("circonus_check.consul_server", "notes", ""), + resource.TestCheckResourceAttr("circonus_check.consul_server", "period", "60s"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.#", "2"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.active", "true"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.name", "KnownLeader"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.tags.#", "2"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.tags.1401442048", "lifecycle:unittest"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.tags.2058715988", "source:consul"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.type", "text"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.active", "true"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.name", "LastContact"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.tags.#", "2"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.tags.1401442048", "lifecycle:unittest"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.tags.2058715988", "source:consul"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.type", "numeric"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.unit", "seconds"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "tags.#", "2"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "tags.1401442048", "lifecycle:unittest"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "tags.2058715988", "source:consul"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "target", "consul.service.consul"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "type", "consul"), + ), + }, + }, + }) +} + +func TestAccCirconusCheckConsul_state(t *testing.T) { + checkName := fmt.Sprintf("Terraform test: consul.service.consul mode=state check - %s", acctest.RandString(5)) + + checkState := "critical" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDestroyCirconusCheckBundle, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccCirconusCheckConsulConfigV1HealthStateFmt, checkName, checkState), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("circonus_check.consul_server", "active", "true"), + resource.TestMatchResourceAttr("circonus_check.consul_server", "check_id", regexp.MustCompile(config.CheckCIDRegex)), + resource.TestCheckResourceAttr("circonus_check.consul_server", "collector.#", "1"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "collector.2084916526.id", "/broker/2110"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.#", "1"), + // resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.ca_chain", ""), + // resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.certificate_file", ""), + // resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.ciphers", ""), + // resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.key_file", ""), + resource.TestCheckNoResourceAttr("circonus_check.consul_server", "consul.0.headers"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.http_addr", "http://consul.service.consul"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.state", checkState), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.check_blacklist.#", "2"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.check_blacklist.0", "worthless"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "consul.0.check_blacklist.1", "check"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "name", checkName), + resource.TestCheckResourceAttr("circonus_check.consul_server", "notes", ""), + resource.TestCheckResourceAttr("circonus_check.consul_server", "period", "60s"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.#", "2"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.active", "true"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.name", "KnownLeader"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.tags.#", "2"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.tags.1401442048", "lifecycle:unittest"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.tags.2058715988", "source:consul"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3333874791.type", "text"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.active", "true"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.name", "LastContact"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.tags.#", "2"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.tags.1401442048", "lifecycle:unittest"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.tags.2058715988", "source:consul"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.type", "numeric"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "metric.3148913305.unit", "seconds"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "tags.#", "2"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "tags.1401442048", "lifecycle:unittest"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "tags.2058715988", "source:consul"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "target", "consul.service.consul"), + resource.TestCheckResourceAttr("circonus_check.consul_server", "type", "consul"), + ), + }, + }, + }) +} + +const testAccCirconusCheckConsulConfigV1HealthNodeFmt = ` +resource "circonus_check" "consul_server" { + active = true + name = "%s" + period = "60s" + + collector { + id = "/broker/2110" + } + + consul { + dc = "dc2" + http_addr = "http://consul.service.consul:8501" + node = "%s" + node_blacklist = ["a","bad","node"] + } + + metric { + name = "LastContact" + tags = [ "source:consul", "lifecycle:unittest" ] + type = "numeric" + unit = "seconds" + } + + metric { + name = "KnownLeader" + tags = [ "source:consul", "lifecycle:unittest" ] + type = "text" + } + + tags = [ "source:consul", "lifecycle:unittest" ] + + target = "consul.service.consul" +} +` + +const testAccCirconusCheckConsulConfigV1HealthServiceFmt = ` +resource "circonus_check" "consul_server" { + active = true + name = "%s" + period = "60s" + + collector { + id = "/broker/2110" + } + + consul { + service = "consul" + service_blacklist = ["bad","hombre","service"] + } + + metric { + name = "LastContact" + tags = [ "source:consul", "lifecycle:unittest" ] + type = "numeric" + unit = "seconds" + } + + metric { + name = "KnownLeader" + tags = [ "source:consul", "lifecycle:unittest" ] + type = "text" + } + + tags = [ "source:consul", "lifecycle:unittest" ] + + target = "consul.service.consul" +} +` + +const testAccCirconusCheckConsulConfigV1HealthStateFmt = ` +resource "circonus_check" "consul_server" { + active = true + name = "%s" + period = "60s" + + collector { + id = "/broker/2110" + } + + consul { + state = "%s" + check_blacklist = ["worthless","check"] + } + + metric { + name = "LastContact" + tags = [ "source:consul", "lifecycle:unittest" ] + type = "numeric" + unit = "seconds" + } + + metric { + name = "KnownLeader" + tags = [ "source:consul", "lifecycle:unittest" ] + type = "text" + } + + tags = [ "source:consul", "lifecycle:unittest" ] + + target = "consul.service.consul" +} +` diff --git a/builtin/providers/circonus/resource_circonus_check_http.go b/builtin/providers/circonus/resource_circonus_check_http.go index 8a9e7d67d..7b6d68b33 100644 --- a/builtin/providers/circonus/resource_circonus_check_http.go +++ b/builtin/providers/circonus/resource_circonus_check_http.go @@ -372,6 +372,10 @@ func checkConfigToAPIHTTP(c *circonusCheck, l interfaceList) error { if len(c.Target) == 0 { c.Target = hostInfo[0] } + + if len(hostInfo) > 1 && c.Config[config.Port] == "" { + c.Config[config.Port] = hostInfo[1] + } } if v, found := httpConfig[checkHTTPVersionAttr]; found { diff --git a/builtin/providers/circonus/resource_circonus_check_json.go b/builtin/providers/circonus/resource_circonus_check_json.go index f319983cb..e377d08f3 100644 --- a/builtin/providers/circonus/resource_circonus_check_json.go +++ b/builtin/providers/circonus/resource_circonus_check_json.go @@ -355,6 +355,10 @@ func checkConfigToAPIJSON(c *circonusCheck, l interfaceList) error { if len(c.Target) == 0 { c.Target = hostInfo[0] } + + if len(hostInfo) > 1 && c.Config[config.Port] == "" { + c.Config[config.Port] = hostInfo[1] + } } if v, found := jsonConfig[checkJSONVersionAttr]; found { diff --git a/builtin/providers/circonus/validators.go b/builtin/providers/circonus/validators.go index dca2de36c..c98ec2799 100644 --- a/builtin/providers/circonus/validators.go +++ b/builtin/providers/circonus/validators.go @@ -314,6 +314,7 @@ type urlParseFlags int const ( urlIsAbs urlParseFlags = 1 << iota urlOptional + urlWithoutPath urlWithoutPort urlWithoutSchema ) @@ -345,6 +346,10 @@ func validateHTTPURL(attrName schemaAttr, checkFlags urlParseFlags) func(v inter errors = append(errors, fmt.Errorf("Schema is present on URL %q (HINT: drop the https://%s)", v.(string), v.(string))) } + if checkFlags&urlWithoutPath != 0 && u.Path != "" { + errors = append(errors, fmt.Errorf("Path is present on URL %q (HINT: drop the %s)", v.(string), u.Path)) + } + if checkFlags&urlWithoutPort != 0 { hostParts := strings.SplitN(u.Host, ":", 2) if len(hostParts) != 1 { diff --git a/website/source/docs/providers/circonus/r/check.html.markdown b/website/source/docs/providers/circonus/r/check.html.markdown index 9c1787cc9..e83d6245f 100644 --- a/website/source/docs/providers/circonus/r/check.html.markdown +++ b/website/source/docs/providers/circonus/r/check.html.markdown @@ -88,6 +88,9 @@ resource "circonus_metric" "used" { enterprise collector running in your datacenter. One collection of metrics will be automatically created for each `collector` specified. +* `consul` - (Optional) A native Consul check. See below for details on how to + configure a `consul` check. + * `http` - (Optional) A poll-based HTTP check. See below for details on how to configure the `http` check. @@ -249,6 +252,140 @@ resource "circonus_check" "rds_metrics" { } ``` +### `consul` Check Type Attributes + +* `acl_token` - (Optional) An ACL Token authenticate the API request. When an + ACL Token is set, this value is transmitted as an HTTP Header in order to not + show up in any logs. The default value is an empty string. + +* `allow_stale` - (Optional) A boolean value that indicates whether or not this + check should require the health information come from the Consul leader node. + For scalability reasons, this value defaults to `false`. See below for + details on detecting the staleness of health information. + +* `ca_chain` - (Optional) A path to a file containing all the certificate + authorities that should be loaded to validate the remote certificate (required + when `http_addr` is a TLS-enabled endpoint). + +* `certificate_file` - (Optional) A path to a file containing the client + certificate that will be presented to the remote server (required when + `http_addr` is a TLS-enabled endpoint). + +* `check_blacklist` - (Optional) A list of check names to exclude from the + result of checks (i.e. no metrics will be generated by whose check name is in + the `check_blacklist`). This blacklist is applied to the `node`, + `service`, and `state` check modes. + +* `ciphers` - (Optional) A list of ciphers to be used in the TLS protocol + (only used when `http_addr` is a TLS-enabled endpoint). + +* `dc` - (Optional) Explicitly name the Consul datacenter to use. The default + value is an empty string. When an empty value is specified, the Consul + datacenter of the agent at the `http_addr` is implicitly used. + +* `headers` - (Optional) A map of the HTTP headers to be sent when executing the + check. NOTE: the `headers` attribute is processed last and will takes + precidence over any other derived value that is transmitted as an HTTP header + to Consul (i.e. it is possible to override the `acl_token` by setting a + headers value). + +* `http_addr` - (Optional) The Consul HTTP endpoint to to query for health + information. The default value is `http://consul.service.consul:8500`. The + scheme must change from `http` to `https` when the endpoint has been + TLS-enabled. + +* `key_file` - (Optional) A path to a file containing key to be used in + conjunction with the cilent certificate (required when `http_addr` is a + TLS-enabled endpoint). + +* `node` - (Optional) Check the health of this node. The value can be either a + Consul Node ID (Consul Version >= 0.7.4) or Node Name. See also the + `service_blacklist`, `node_blacklist`, and `check_blacklist` attributes. This + attribute conflicts with the `service` and `state` attributes. + +* `node_blacklist` - (Optional) A list of node IDs or node names to exclude from + the results of checks (i.e. no metrics will be generated from nodes in the + `node_blacklist`). This blacklist is applied to the `node`, `service`, and + `state` check modes. + +* `service` - (Optional) Check the cluster-wide health of this named service. + See also the `service_blacklist`, `node_blacklist`, and `check_blacklist` + attributes. This attribute conflicts with the `node` and `state` attributes. + +* `service_blacklist` - (Optional) A list of service names to exclude from the + result of checks (i.e. no metrics will be generated by services whose service + name is in the `service_blacklist`). This blacklist is applied to the `node`, + `service`, and `state` check modes. + +* `state` - (Optional) A Circonus check to monitor Consul checks across the + entire Consul cluster. This value may be either `passing`, `warning`, or + `critical`. This `consul` check mode is intended to act as the cluster check + of last resort. This check type is useful when first starting and is intended + to act as a check of last resort before transitioning to explicitly defined + checks for individual services or nodes. The metrics returned from check will + be sorted based on the `CreateIndex` of the entry in order to have a stable + set of metrics in the array of returned values. See also the + `service_blacklist`, `node_blacklist`, and `check_blacklist` attributes. This + attribute conflicts with the `node` and `state` attributes. + +Available metrics depend on the consul check being performed (`node`, `service`, +or `state`). In addition to the data avilable from the endpoints, the `consul` +check also returns a set of metrics that are a variant of: +`{Num,Pct}{,Passing,Warning,Critical}{Checks,Nodes,Services}` (see the +`GLOB_BRACE` section of your local `glob(3)` documentation). + +Example Consul check (partial metrics collection): + +``` +resource "circonus_check" "consul_server" { + active = true + name = "%s" + period = "60s" + + collector { + # Collector ID must be an Enterprise broker able to reach the Consul agent + # listed in `http_addr`. + id = "/broker/2110" + } + + consul { + service = "consul" + + # Other consul check modes: + # node = "consul1" + # state = "critical" + } + + metric { + name = "NumNodes" + tags = [ "source:consul", "lifecycle:unittest" ] + type = "numeric" + } + + metric { + name = "LastContact" + tags = [ "source:consul", "lifecycle:unittest" ] + type = "numeric" + unit = "seconds" + } + + metric { + name = "Index" + tags = [ "source:consul", "lifecycle:unittest" ] + type = "numeric" + unit = "transactions" + } + + metric { + name = "KnownLeader" + tags = [ "source:consul", "lifecycle:unittest" ] + type = "text" + } + + tags = [ "source:consul", "lifecycle:unittest" ] +} +``` + ### `http` Check Type Attributes * `auth_method` - (Optional) HTTP Authentication method to use. When set must