terraform/builtin/providers/circonus/resource_circonus_check.go

644 lines
20 KiB
Go

package circonus
/*
* Note to future readers: The `circonus_check` resource is actually a facade for
* the check_bundle call. check_bundle is an implementation detail that we mask
* over and expose just a "check" even though the "check" is actually a
* check_bundle.
*
* Style note: There are three directions that information flows:
*
* 1) Terraform Config file into API Objects. *Attr named objects are Config or
* Schema attribute names. In this file, all config constants should be
* named check*Attr.
*
* 2) API Objects into Statefile data. api*Attr named constants are parameters
* that originate from the API and need to be mapped into the provider's
* vernacular.
*/
import (
"fmt"
"time"
"github.com/circonus-labs/circonus-gometrics/api"
"github.com/circonus-labs/circonus-gometrics/api/config"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
)
const (
// circonus_check.* global resource attribute names
checkActiveAttr = "active"
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"
checkStatsdAttr = "statsd"
checkTCPAttr = "tcp"
checkTagsAttr = "tags"
checkTargetAttr = "target"
checkTimeoutAttr = "timeout"
checkTypeAttr = "type"
// circonus_check.collector.* resource attribute names
checkCollectorIDAttr = "id"
// circonus_check.metric.* resource attribute names are aliased to
// circonus_metric.* resource attributes.
// circonus_check.metric.* resource attribute names
// metricIDAttr = "id"
// Out parameters for circonus_check
checkOutByCollectorAttr = "check_by_collector"
checkOutIDAttr = "check_id"
checkOutChecksAttr = "checks"
checkOutCreatedAttr = "created"
checkOutLastModifiedAttr = "last_modified"
checkOutLastModifiedByAttr = "last_modified_by"
checkOutReverseConnectURLsAttr = "reverse_connect_urls"
checkOutCheckUUIDsAttr = "uuids"
)
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"
apiCheckTypeJSONAttr apiCheckType = "json"
apiCheckTypeMySQLAttr apiCheckType = "mysql"
apiCheckTypePostgreSQLAttr apiCheckType = "postgres"
apiCheckTypeStatsdAttr apiCheckType = "statsd"
apiCheckTypeTCPAttr apiCheckType = "tcp"
)
var checkDescriptions = attrDescrs{
checkActiveAttr: "If the check is activate or disabled",
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",
checkJSONAttr: "JSON check configuration",
checkMetricAttr: "Configuration for a stream of metrics",
checkMetricLimitAttr: `Setting a metric_limit will enable all (-1), disable (0), or allow up to the specified limit of metrics for this check ("N+", where N is a positive integer)`,
checkMySQLAttr: "MySQL check configuration",
checkNameAttr: "The name of the check bundle that will be displayed in the web interface",
checkNotesAttr: "Notes about this check bundle",
checkPeriodAttr: "The period between each time the check is made",
checkPostgreSQLAttr: "PostgreSQL check configuration",
checkStatsdAttr: "statsd check configuration",
checkTCPAttr: "TCP check configuration",
checkTagsAttr: "A list of tags assigned to the check",
checkTargetAttr: "The target of the check (e.g. hostname, URL, IP, etc)",
checkTimeoutAttr: "The length of time in seconds (and fractions of a second) before the check will timeout if no response is returned to the collector",
checkTypeAttr: "The check type",
checkOutByCollectorAttr: "",
checkOutCheckUUIDsAttr: "",
checkOutChecksAttr: "",
checkOutCreatedAttr: "",
checkOutIDAttr: "",
checkOutLastModifiedAttr: "",
checkOutLastModifiedByAttr: "",
checkOutReverseConnectURLsAttr: "",
}
var checkCollectorDescriptions = attrDescrs{
checkCollectorIDAttr: "The ID of the collector",
}
var checkMetricDescriptions = metricDescriptions
func resourceCheck() *schema.Resource {
return &schema.Resource{
Create: checkCreate,
Read: checkRead,
Update: checkUpdate,
Delete: checkDelete,
Exists: checkExists,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: convertToHelperSchema(checkDescriptions, map[schemaAttr]*schema.Schema{
checkActiveAttr: &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
checkCAQLAttr: schemaCheckCAQL,
checkCloudWatchAttr: schemaCheckCloudWatch,
checkCollectorAttr: &schema.Schema{
Type: schema.TypeSet,
Optional: true,
MinItems: 1,
Elem: &schema.Resource{
Schema: convertToHelperSchema(checkCollectorDescriptions, map[schemaAttr]*schema.Schema{
checkCollectorIDAttr: &schema.Schema{
Type: schema.TypeString,
Required: true,
ValidateFunc: validateRegexp(checkCollectorIDAttr, config.BrokerCIDRegex),
},
}),
},
},
checkConsulAttr: schemaCheckConsul,
checkHTTPAttr: schemaCheckHTTP,
checkHTTPTrapAttr: schemaCheckHTTPTrap,
checkJSONAttr: schemaCheckJSON,
checkICMPPingAttr: schemaCheckICMPPing,
checkMetricAttr: &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Set: checkMetricChecksum,
MinItems: 1,
Elem: &schema.Resource{
Schema: convertToHelperSchema(checkMetricDescriptions, map[schemaAttr]*schema.Schema{
metricActiveAttr: &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
metricNameAttr: &schema.Schema{
Type: schema.TypeString,
Required: true,
ValidateFunc: validateRegexp(metricNameAttr, `[\S]+`),
},
metricTagsAttr: tagMakeConfigSchema(metricTagsAttr),
metricTypeAttr: &schema.Schema{
Type: schema.TypeString,
Required: true,
ValidateFunc: validateMetricType,
},
metricUnitAttr: &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: metricUnit,
ValidateFunc: validateRegexp(metricUnitAttr, metricUnitRegexp),
},
}),
},
},
checkMetricLimitAttr: &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ValidateFunc: validateFuncs(
validateIntMin(checkMetricLimitAttr, -1),
),
},
checkMySQLAttr: schemaCheckMySQL,
checkNameAttr: &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
checkNotesAttr: &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
StateFunc: suppressWhitespace,
},
checkPeriodAttr: &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
StateFunc: normalizeTimeDurationStringToSeconds,
ValidateFunc: validateFuncs(
validateDurationMin(checkPeriodAttr, defaultCirconusCheckPeriodMin),
validateDurationMax(checkPeriodAttr, defaultCirconusCheckPeriodMax),
),
},
checkPostgreSQLAttr: schemaCheckPostgreSQL,
checkStatsdAttr: schemaCheckStatsd,
checkTagsAttr: tagMakeConfigSchema(checkTagsAttr),
checkTargetAttr: &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: validateRegexp(checkTagsAttr, `.+`),
},
checkTCPAttr: schemaCheckTCP,
checkTimeoutAttr: &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
StateFunc: normalizeTimeDurationStringToSeconds,
ValidateFunc: validateFuncs(
validateDurationMin(checkTimeoutAttr, defaultCirconusTimeoutMin),
validateDurationMax(checkTimeoutAttr, defaultCirconusTimeoutMax),
),
},
checkTypeAttr: &schema.Schema{
Type: schema.TypeString,
Computed: true,
Optional: true,
ForceNew: true,
ValidateFunc: validateCheckType,
},
// Out parameters
checkOutIDAttr: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
checkOutByCollectorAttr: &schema.Schema{
Type: schema.TypeMap,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
checkOutCheckUUIDsAttr: &schema.Schema{
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
checkOutChecksAttr: &schema.Schema{
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
checkOutCreatedAttr: &schema.Schema{
Type: schema.TypeInt,
Computed: true,
},
checkOutLastModifiedAttr: &schema.Schema{
Type: schema.TypeInt,
Computed: true,
},
checkOutLastModifiedByAttr: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
checkOutReverseConnectURLsAttr: &schema.Schema{
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
}),
}
}
func checkCreate(d *schema.ResourceData, meta interface{}) error {
ctxt := meta.(*providerContext)
c := newCheck()
if err := c.ParseConfig(d); err != nil {
return errwrap.Wrapf("error parsing check schema during create: {{err}}", err)
}
if err := c.Create(ctxt); err != nil {
return errwrap.Wrapf("error creating check: {{err}}", err)
}
d.SetId(c.CID)
return checkRead(d, meta)
}
func checkExists(d *schema.ResourceData, meta interface{}) (bool, error) {
ctxt := meta.(*providerContext)
cid := d.Id()
cb, err := ctxt.client.FetchCheckBundle(api.CIDType(&cid))
if err != nil {
return false, err
}
if cb.CID == "" {
return false, nil
}
return true, nil
}
// checkRead pulls data out of the CheckBundle object and stores it into the
// appropriate place in the statefile.
func checkRead(d *schema.ResourceData, meta interface{}) error {
ctxt := meta.(*providerContext)
cid := d.Id()
c, err := loadCheck(ctxt, api.CIDType(&cid))
if err != nil {
return err
}
d.SetId(c.CID)
// Global circonus_check attributes are saved first, followed by the check
// type specific attributes handled below in their respective checkRead*().
checkIDsByCollector := make(map[string]interface{}, len(c.Checks))
for i, b := range c.Brokers {
checkIDsByCollector[b] = c.Checks[i]
}
var checkID string
if len(c.Checks) == 1 {
checkID = c.Checks[0]
}
metrics := schema.NewSet(checkMetricChecksum, nil)
for _, m := range c.Metrics {
metricAttrs := map[string]interface{}{
string(metricActiveAttr): metricAPIStatusToBool(m.Status),
string(metricNameAttr): m.Name,
string(metricTagsAttr): tagsToState(apiToTags(m.Tags)),
string(metricTypeAttr): m.Type,
string(metricUnitAttr): indirect(m.Units),
}
metrics.Add(metricAttrs)
}
// Write the global circonus_check parameters followed by the check
// type-specific parameters.
d.Set(checkActiveAttr, checkAPIStatusToBool(c.Status))
if err := d.Set(checkCollectorAttr, stringListToSet(c.Brokers, checkCollectorIDAttr)); err != nil {
return errwrap.Wrapf(fmt.Sprintf("Unable to store check %q attribute: {{err}}", checkCollectorAttr), err)
}
d.Set(checkMetricLimitAttr, c.MetricLimit)
d.Set(checkNameAttr, c.DisplayName)
d.Set(checkNotesAttr, c.Notes)
d.Set(checkPeriodAttr, fmt.Sprintf("%ds", c.Period))
if err := d.Set(checkMetricAttr, metrics); err != nil {
return errwrap.Wrapf(fmt.Sprintf("Unable to store check %q attribute: {{err}}", checkMetricAttr), err)
}
if err := d.Set(checkTagsAttr, c.Tags); err != nil {
return errwrap.Wrapf(fmt.Sprintf("Unable to store check %q attribute: {{err}}", checkTagsAttr), err)
}
d.Set(checkTargetAttr, c.Target)
{
t, _ := time.ParseDuration(fmt.Sprintf("%fs", c.Timeout))
d.Set(checkTimeoutAttr, t.String())
}
d.Set(checkTypeAttr, c.Type)
// Last step: parse a check_bundle's config into the statefile.
if err := parseCheckTypeConfig(&c, d); err != nil {
return errwrap.Wrapf("Unable to parse check config: {{err}}", err)
}
// Out parameters
if err := d.Set(checkOutByCollectorAttr, checkIDsByCollector); err != nil {
return errwrap.Wrapf(fmt.Sprintf("Unable to store check %q attribute: {{err}}", checkOutByCollectorAttr), err)
}
if err := d.Set(checkOutCheckUUIDsAttr, c.CheckUUIDs); err != nil {
return errwrap.Wrapf(fmt.Sprintf("Unable to store check %q attribute: {{err}}", checkOutCheckUUIDsAttr), err)
}
if err := d.Set(checkOutChecksAttr, c.Checks); err != nil {
return errwrap.Wrapf(fmt.Sprintf("Unable to store check %q attribute: {{err}}", checkOutChecksAttr), err)
}
if checkID != "" {
d.Set(checkOutIDAttr, checkID)
}
d.Set(checkOutCreatedAttr, c.Created)
d.Set(checkOutLastModifiedAttr, c.LastModified)
d.Set(checkOutLastModifiedByAttr, c.LastModifedBy)
if err := d.Set(checkOutReverseConnectURLsAttr, c.ReverseConnectURLs); err != nil {
return errwrap.Wrapf(fmt.Sprintf("Unable to store check %q attribute: {{err}}", checkOutReverseConnectURLsAttr), err)
}
return nil
}
func checkUpdate(d *schema.ResourceData, meta interface{}) error {
ctxt := meta.(*providerContext)
c := newCheck()
if err := c.ParseConfig(d); err != nil {
return err
}
c.CID = d.Id()
if err := c.Update(ctxt); err != nil {
return errwrap.Wrapf(fmt.Sprintf("unable to update check %q: {{err}}", d.Id()), err)
}
return checkRead(d, meta)
}
func checkDelete(d *schema.ResourceData, meta interface{}) error {
ctxt := meta.(*providerContext)
if _, err := ctxt.client.Delete(d.Id()); err != nil {
return errwrap.Wrapf(fmt.Sprintf("unable to delete check %q: {{err}}", d.Id()), err)
}
d.SetId("")
return nil
}
func checkMetricChecksum(v interface{}) int {
m := v.(map[string]interface{})
csum := metricChecksum(m)
return csum
}
// ParseConfig reads Terraform config data and stores the information into a
// Circonus CheckBundle object.
func (c *circonusCheck) ParseConfig(d *schema.ResourceData) error {
if v, found := d.GetOk(checkActiveAttr); found {
c.Status = checkActiveToAPIStatus(v.(bool))
}
if v, found := d.GetOk(checkCollectorAttr); found {
l := v.(*schema.Set).List()
c.Brokers = make([]string, 0, len(l))
for _, mapRaw := range l {
mapAttrs := mapRaw.(map[string]interface{})
if mv, mapFound := mapAttrs[checkCollectorIDAttr]; mapFound {
c.Brokers = append(c.Brokers, mv.(string))
}
}
}
if v, found := d.GetOk(checkMetricLimitAttr); found {
c.MetricLimit = v.(int)
}
if v, found := d.GetOk(checkNameAttr); found {
c.DisplayName = v.(string)
}
if v, found := d.GetOk(checkNotesAttr); found {
s := v.(string)
c.Notes = &s
}
if v, found := d.GetOk(checkPeriodAttr); found {
d, err := time.ParseDuration(v.(string))
if err != nil {
return errwrap.Wrapf(fmt.Sprintf("unable to parse %q as a duration: {{err}}", checkPeriodAttr), err)
}
c.Period = uint(d.Seconds())
}
if v, found := d.GetOk(checkMetricAttr); found {
metricList := v.(*schema.Set).List()
c.Metrics = make([]api.CheckBundleMetric, 0, len(metricList))
for _, metricListRaw := range metricList {
metricAttrs := metricListRaw.(map[string]interface{})
var id string
if av, found := metricAttrs[metricIDAttr]; found {
id = av.(string)
} else {
var err error
id, err = newMetricID()
if err != nil {
return errwrap.Wrapf("unable to create a new metric ID: {{err}}", err)
}
}
m := newMetric()
if err := m.ParseConfigMap(id, metricAttrs); err != nil {
return errwrap.Wrapf("unable to parse config: {{err}}", err)
}
c.Metrics = append(c.Metrics, m.CheckBundleMetric)
}
}
if v, found := d.GetOk(checkTagsAttr); found {
c.Tags = derefStringList(flattenSet(v.(*schema.Set)))
}
if v, found := d.GetOk(checkTargetAttr); found {
c.Target = v.(string)
}
if v, found := d.GetOk(checkTimeoutAttr); found {
d, err := time.ParseDuration(v.(string))
if err != nil {
return errwrap.Wrapf(fmt.Sprintf("unable to parse %q as a duration: {{err}}", checkTimeoutAttr), err)
}
t := float32(d.Seconds())
c.Timeout = t
}
// Last step: parse the individual check types
if err := checkConfigToAPI(c, d); err != nil {
return errwrap.Wrapf("unable to parse check type: {{err}}", err)
}
if err := c.Fixup(); err != nil {
return err
}
if err := c.Validate(); err != nil {
return err
}
return nil
}
// checkConfigToAPI parses the Terraform config into the respective per-check
// type api.Config attributes.
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,
checkJSONAttr: checkConfigToAPIJSON,
checkMySQLAttr: checkConfigToAPIMySQL,
checkPostgreSQLAttr: checkConfigToAPIPostgreSQL,
checkStatsdAttr: checkConfigToAPIStatsd,
checkTCPAttr: checkConfigToAPITCP,
}
for checkType, fn := range checkTypeParseMap {
if listRaw, found := d.GetOk(checkType); found {
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)
}
}
}
return nil
}
// parseCheckTypeConfig parses an API Config object and stores the result in the
// statefile.
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,
apiCheckTypeJSONAttr: checkAPIToStateJSON,
apiCheckTypeMySQLAttr: checkAPIToStateMySQL,
apiCheckTypePostgreSQLAttr: checkAPIToStatePostgreSQL,
apiCheckTypeStatsdAttr: checkAPIToStateStatsd,
apiCheckTypeTCPAttr: checkAPIToStateTCP,
}
var checkType apiCheckType = apiCheckType(c.Type)
fn, ok := checkTypeConfigHandlers[checkType]
if !ok {
return fmt.Errorf("check type %q not supported", c.Type)
}
if err := fn(c, d); err != nil {
return errwrap.Wrapf(fmt.Sprintf("unable to parse the API config for %q: {{err}}", c.Type), err)
}
return nil
}