diff --git a/builtin/providers/circonus/GNUmakefile b/builtin/providers/circonus/GNUmakefile new file mode 100644 index 000000000..bb9209add --- /dev/null +++ b/builtin/providers/circonus/GNUmakefile @@ -0,0 +1,3 @@ +# env TESTARGS='-test.parallel=1 -run TestAccCirconusCheckBundle' TF_LOG=debug make test +test:: + 2>&1 make -C ../../.. testacc TEST=./builtin/providers/circonus | tee test.log diff --git a/builtin/providers/circonus/check.go b/builtin/providers/circonus/check.go new file mode 100644 index 000000000..039dc7452 --- /dev/null +++ b/builtin/providers/circonus/check.go @@ -0,0 +1,122 @@ +package circonus + +import ( + "fmt" + "log" + + "github.com/circonus-labs/circonus-gometrics/api" + "github.com/circonus-labs/circonus-gometrics/api/config" + "github.com/hashicorp/errwrap" +) + +// The circonusCheck type is the backing store of the `circonus_check` resource. + +type circonusCheck struct { + api.CheckBundle +} + +type circonusCheckType string + +const ( + // CheckBundle.Status can be one of these values + checkStatusActive = "active" + checkStatusDisabled = "disabled" +) + +const ( + apiCheckTypeCAQL circonusCheckType = "caql" + apiCheckTypeICMPPing circonusCheckType = "ping_icmp" + apiCheckTypeHTTP circonusCheckType = "http" + apiCheckTypeJSON circonusCheckType = "json" + apiCheckTypeMySQL circonusCheckType = "mysql" + apiCheckTypePostgreSQL circonusCheckType = "postgres" + apiCheckTypeTCP circonusCheckType = "tcp" +) + +func newCheck() circonusCheck { + return circonusCheck{ + CheckBundle: *api.NewCheckBundle(), + } +} + +func loadCheck(ctxt *providerContext, cid api.CIDType) (circonusCheck, error) { + var c circonusCheck + cb, err := ctxt.client.FetchCheckBundle(cid) + if err != nil { + return circonusCheck{}, err + } + c.CheckBundle = *cb + + return c, nil +} + +func checkAPIStatusToBool(s string) bool { + var active bool + switch s { + case checkStatusActive: + active = true + case checkStatusDisabled: + active = false + default: + log.Printf("[ERROR] PROVIDER BUG: check status %q unsupported", s) + } + + return active +} + +func checkActiveToAPIStatus(active bool) string { + if active { + return checkStatusActive + } + + return checkStatusDisabled +} + +func (c *circonusCheck) Create(ctxt *providerContext) error { + cb, err := ctxt.client.CreateCheckBundle(&c.CheckBundle) + if err != nil { + return err + } + + c.CID = cb.CID + + return nil +} + +func (c *circonusCheck) Update(ctxt *providerContext) error { + _, err := ctxt.client.UpdateCheckBundle(&c.CheckBundle) + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to update check bundle %s: {{err}}", c.CID), err) + } + + return nil +} + +func (c *circonusCheck) Fixup() error { + switch apiCheckType(c.Type) { + case apiCheckTypeCloudWatchAttr: + switch c.Period { + case 60: + c.Config[config.Granularity] = "1" + case 300: + c.Config[config.Granularity] = "5" + } + } + + return nil +} + +func (c *circonusCheck) Validate() error { + if c.Timeout > float32(c.Period) { + return fmt.Errorf("Timeout (%f) can not exceed period (%d)", c.Timeout, c.Period) + } + + 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) + } + } + + return nil +} diff --git a/builtin/providers/circonus/consts.go b/builtin/providers/circonus/consts.go new file mode 100644 index 000000000..6b505482a --- /dev/null +++ b/builtin/providers/circonus/consts.go @@ -0,0 +1,127 @@ +package circonus + +const ( + // Provider-level constants + + // defaultAutoTag determines the default behavior of circonus.auto_tag. + defaultAutoTag = false + + // When auto_tag is enabled, the default tag category and value will be set to + // the following value unless overriden. + defaultCirconusTag circonusTag = "author:terraform" + + // When hashing a Set, default to a buffer this size + defaultHashBufSize = 512 + + providerAPIURLAttr = "api_url" + providerAutoTagAttr = "auto_tag" + providerKeyAttr = "key" + + defaultCheckJSONMethod = "GET" + defaultCheckJSONPort = "443" + defaultCheckJSONVersion = "1.1" + + defaultCheckICMPPingAvailability = 100.0 + defaultCheckICMPPingCount = 5 + defaultCheckICMPPingInterval = "2s" + + defaultCheckCAQLTarget = "q._caql" + + defaultCheckHTTPCodeRegexp = `^200$` + defaultCheckHTTPMethod = "GET" + defaultCheckHTTPVersion = "1.1" + + defaultCheckHTTPTrapAsync = false + + defaultCheckCloudWatchVersion = "2010-08-01" + + defaultCollectorDetailAttrs = 10 + + defaultGraphDatapoints = 8 + defaultGraphLineStyle = "stepped" + defaultGraphStyle = "line" + defaultGraphFunction = "gauge" + + metricUnit = "" + metricUnitRegexp = `^.*$` + + defaultRuleSetLast = "300s" + defaultRuleSetMetricType = "numeric" + defaultRuleSetRuleLen = 4 + defaultAlertSeverity = 1 + defaultRuleSetWindowFunc = "average" + ruleSetAbsentMin = "70s" +) + +// Consts and their close relative, Go pseudo-consts. + +// validMetricTypes: See `type`: https://login.circonus.com/resources/api/calls/check_bundle +var validMetricTypes = validStringValues{ + `caql`, + `composite`, + `histogram`, + `numeric`, + `text`, +} + +// validAggregateFuncs: See `aggregate_function`: https://login.circonus.com/resources/api/calls/graph +var validAggregateFuncs = validStringValues{ + `none`, + `min`, + `max`, + `sum`, + `mean`, + `geometric_mean`, +} + +// validGraphLineStyles: See `line_style`: https://login.circonus.com/resources/api/calls/graph +var validGraphLineStyles = validStringValues{ + `stepped`, + `interpolated`, +} + +// validGraphStyles: See `style`: https://login.circonus.com/resources/api/calls/graph +var validGraphStyles = validStringValues{ + `area`, + `line`, +} + +// validAxisAttrs: See `line_style`: https://login.circonus.com/resources/api/calls/graph +var validAxisAttrs = validStringValues{ + `left`, + `right`, +} + +// validGraphFunctionValues: See `derive`: https://login.circonus.com/resources/api/calls/graph +var validGraphFunctionValues = validStringValues{ + `counter`, + `derive`, + `gauge`, +} + +// validRuleSetWindowFuncs: See `derive` or `windowing_func`: https://login.circonus.com/resources/api/calls/rule_set +var validRuleSetWindowFuncs = validStringValues{ + `average`, + `stddev`, + `derive`, + `derive_stddev`, + `counter`, + `counter_stddev`, + `derive_2`, + `derive_2_stddev`, + `counter_2`, + `counter_2_stddev`, +} + +const ( + // Supported circonus_trigger.metric_types. See `metric_type`: + // https://login.circonus.com/resources/api/calls/rule_set + ruleSetMetricTypeNumeric = "numeric" + ruleSetMetricTypeText = "text" +) + +// validRuleSetMetricTypes: See `metric_type`: https://login.circonus.com/resources/api/calls/rule_set +var validRuleSetMetricTypes = validStringValues{ + ruleSetMetricTypeNumeric, + ruleSetMetricTypeText, +} diff --git a/builtin/providers/circonus/data_source_circonus_account.go b/builtin/providers/circonus/data_source_circonus_account.go new file mode 100644 index 000000000..c7a9121bc --- /dev/null +++ b/builtin/providers/circonus/data_source_circonus_account.go @@ -0,0 +1,271 @@ +package circonus + +import ( + "fmt" + + "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 ( + accountAddress1Attr = "address1" + accountAddress2Attr = "address2" + accountCCEmailAttr = "cc_email" + accountCityAttr = "city" + accountContactGroupsAttr = "contact_groups" + accountCountryAttr = "country" + accountCurrentAttr = "current" + accountDescriptionAttr = "description" + accountEmailAttr = "email" + accountIDAttr = "id" + accountInvitesAttr = "invites" + accountLimitAttr = "limit" + accountNameAttr = "name" + accountOwnerAttr = "owner" + accountRoleAttr = "role" + accountStateProvAttr = "state" + accountTimezoneAttr = "timezone" + accountTypeAttr = "type" + accountUIBaseURLAttr = "ui_base_url" + accountUsageAttr = "usage" + accountUsedAttr = "used" + accountUserIDAttr = "id" + accountUsersAttr = "users" +) + +var accountDescription = map[schemaAttr]string{ + accountContactGroupsAttr: "Contact Groups in this account", + accountInvitesAttr: "Outstanding invites attached to the account", + accountUsageAttr: "Account's usage limits", + accountUsersAttr: "Users attached to this account", +} + +func dataSourceCirconusAccount() *schema.Resource { + return &schema.Resource{ + Read: dataSourceCirconusAccountRead, + + Schema: map[string]*schema.Schema{ + accountAddress1Attr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: accountDescription[accountAddress1Attr], + }, + accountAddress2Attr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: accountDescription[accountAddress2Attr], + }, + accountCCEmailAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: accountDescription[accountCCEmailAttr], + }, + accountIDAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{accountCurrentAttr}, + ValidateFunc: validateFuncs( + validateRegexp(accountIDAttr, config.AccountCIDRegex), + ), + Description: accountDescription[accountIDAttr], + }, + accountCityAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: accountDescription[accountCityAttr], + }, + accountContactGroupsAttr: &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: accountDescription[accountContactGroupsAttr], + }, + accountCountryAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: accountDescription[accountCountryAttr], + }, + accountCurrentAttr: &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Computed: true, + ConflictsWith: []string{accountIDAttr}, + Description: accountDescription[accountCurrentAttr], + }, + accountDescriptionAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: accountDescription[accountDescriptionAttr], + }, + accountInvitesAttr: &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Description: accountDescription[accountInvitesAttr], + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + accountEmailAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: accountDescription[accountEmailAttr], + }, + accountRoleAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: accountDescription[accountRoleAttr], + }, + }, + }, + }, + accountNameAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: accountDescription[accountNameAttr], + }, + accountOwnerAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: accountDescription[accountOwnerAttr], + }, + accountStateProvAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: accountDescription[accountStateProvAttr], + }, + accountTimezoneAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: accountDescription[accountTimezoneAttr], + }, + accountUIBaseURLAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: accountDescription[accountUIBaseURLAttr], + }, + accountUsageAttr: &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Description: accountDescription[accountUsageAttr], + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + accountLimitAttr: &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + Description: accountDescription[accountLimitAttr], + }, + accountTypeAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: accountDescription[accountTypeAttr], + }, + accountUsedAttr: &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + Description: accountDescription[accountUsedAttr], + }, + }, + }, + }, + accountUsersAttr: &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Description: accountDescription[accountUsersAttr], + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + accountUserIDAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: accountDescription[accountUserIDAttr], + }, + accountRoleAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: accountDescription[accountRoleAttr], + }, + }, + }, + }, + }, + } +} + +func dataSourceCirconusAccountRead(d *schema.ResourceData, meta interface{}) error { + c := meta.(*providerContext) + + var cid string + + var a *api.Account + var err error + if v, ok := d.GetOk(accountIDAttr); ok { + cid = v.(string) + } + + if v, ok := d.GetOk(accountCurrentAttr); ok { + if v.(bool) { + cid = "" + } + } + + a, err = c.client.FetchAccount(api.CIDType(&cid)) + if err != nil { + return err + } + + invitesList := make([]interface{}, 0, len(a.Invites)) + for i := range a.Invites { + invitesList = append(invitesList, map[string]interface{}{ + accountEmailAttr: a.Invites[i].Email, + accountRoleAttr: a.Invites[i].Role, + }) + } + + usageList := make([]interface{}, 0, len(a.Usage)) + for i := range a.Usage { + usageList = append(usageList, map[string]interface{}{ + accountLimitAttr: a.Usage[i].Limit, + accountTypeAttr: a.Usage[i].Type, + accountUsedAttr: a.Usage[i].Used, + }) + } + + usersList := make([]interface{}, 0, len(a.Users)) + for i := range a.Users { + usersList = append(usersList, map[string]interface{}{ + accountUserIDAttr: a.Users[i].UserCID, + accountRoleAttr: a.Users[i].Role, + }) + } + + d.SetId(a.CID) + + d.Set(accountAddress1Attr, a.Address1) + d.Set(accountAddress2Attr, a.Address2) + d.Set(accountCCEmailAttr, a.CCEmail) + d.Set(accountIDAttr, a.CID) + d.Set(accountCityAttr, a.City) + d.Set(accountContactGroupsAttr, a.ContactGroups) + d.Set(accountCountryAttr, a.Country) + d.Set(accountDescriptionAttr, a.Description) + + if err := d.Set(accountInvitesAttr, invitesList); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to store account %q attribute: {{err}}", accountInvitesAttr), err) + } + + d.Set(accountNameAttr, a.Name) + d.Set(accountOwnerAttr, a.OwnerCID) + d.Set(accountStateProvAttr, a.StateProv) + d.Set(accountTimezoneAttr, a.Timezone) + d.Set(accountUIBaseURLAttr, a.UIBaseURL) + + if err := d.Set(accountUsageAttr, usageList); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to store account %q attribute: {{err}}", accountUsageAttr), err) + } + + if err := d.Set(accountUsersAttr, usersList); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to store account %q attribute: {{err}}", accountUsersAttr), err) + } + + return nil +} diff --git a/builtin/providers/circonus/data_source_circonus_account_test.go b/builtin/providers/circonus/data_source_circonus_account_test.go new file mode 100644 index 000000000..78b08c52d --- /dev/null +++ b/builtin/providers/circonus/data_source_circonus_account_test.go @@ -0,0 +1,66 @@ +package circonus + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccDataSourceCirconusAccount(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDataSourceCirconusAccountCurrentConfig, + Check: resource.ComposeTestCheckFunc( + testAccDataSourceCirconusAccountCheck("data.circonus_account.by_current", "/account/3081"), + ), + }, + }, + }) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDataSourceCirconusAccountIDConfig, + Check: resource.ComposeTestCheckFunc( + testAccDataSourceCirconusAccountCheck("data.circonus_account.by_id", "/account/3081"), + ), + }, + }, + }) +} + +func testAccDataSourceCirconusAccountCheck(name, cid string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("root module has no resource called %s", name) + } + + attr := rs.Primary.Attributes + + if attr[accountIDAttr] != cid { + return fmt.Errorf("bad %s %s", accountIDAttr, attr[accountIDAttr]) + } + + return nil + } +} + +const testAccDataSourceCirconusAccountCurrentConfig = ` +data "circonus_account" "by_current" { + current = true +} +` + +const testAccDataSourceCirconusAccountIDConfig = ` +data "circonus_account" "by_id" { + id = "/account/3081" +} +` diff --git a/builtin/providers/circonus/data_source_circonus_collector.go b/builtin/providers/circonus/data_source_circonus_collector.go new file mode 100644 index 000000000..6dda66a82 --- /dev/null +++ b/builtin/providers/circonus/data_source_circonus_collector.go @@ -0,0 +1,214 @@ +package circonus + +import ( + "fmt" + + "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 ( + collectorCNAttr = "cn" + collectorIDAttr = "id" + collectorDetailsAttr = "details" + collectorExternalHostAttr = "external_host" + collectorExternalPortAttr = "external_port" + collectorIPAttr = "ip" + collectorLatitudeAttr = "latitude" + collectorLongitudeAttr = "longitude" + collectorMinVersionAttr = "min_version" + collectorModulesAttr = "modules" + collectorNameAttr = "name" + collectorPortAttr = "port" + collectorSkewAttr = "skew" + collectorStatusAttr = "status" + collectorTagsAttr = "tags" + collectorTypeAttr = "type" + collectorVersionAttr = "version" +) + +var collectorDescription = map[schemaAttr]string{ + collectorDetailsAttr: "Details associated with individual collectors (a.k.a. broker)", + collectorTagsAttr: "Tags assigned to a collector", +} + +func dataSourceCirconusCollector() *schema.Resource { + return &schema.Resource{ + Read: dataSourceCirconusCollectorRead, + + Schema: map[string]*schema.Schema{ + collectorDetailsAttr: &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Description: collectorDescription[collectorDetailsAttr], + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + collectorCNAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: collectorDescription[collectorCNAttr], + }, + collectorExternalHostAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: collectorDescription[collectorExternalHostAttr], + }, + collectorExternalPortAttr: &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + Description: collectorDescription[collectorExternalPortAttr], + }, + collectorIPAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: collectorDescription[collectorIPAttr], + }, + collectorMinVersionAttr: &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + Description: collectorDescription[collectorMinVersionAttr], + }, + collectorModulesAttr: &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: collectorDescription[collectorModulesAttr], + }, + collectorPortAttr: &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + Description: collectorDescription[collectorPortAttr], + }, + collectorSkewAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: collectorDescription[collectorSkewAttr], + }, + collectorStatusAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: collectorDescription[collectorStatusAttr], + }, + collectorVersionAttr: &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + Description: collectorDescription[collectorVersionAttr], + }, + }, + }, + }, + collectorIDAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validateRegexp(collectorIDAttr, config.BrokerCIDRegex), + Description: collectorDescription[collectorIDAttr], + }, + collectorLatitudeAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: collectorDescription[collectorLatitudeAttr], + }, + collectorLongitudeAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: collectorDescription[collectorLongitudeAttr], + }, + collectorNameAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: collectorDescription[collectorNameAttr], + }, + collectorTagsAttr: tagMakeConfigSchema(collectorTagsAttr), + collectorTypeAttr: &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: collectorDescription[collectorTypeAttr], + }, + }, + } +} + +func dataSourceCirconusCollectorRead(d *schema.ResourceData, meta interface{}) error { + ctxt := meta.(*providerContext) + + var collector *api.Broker + var err error + cid := d.Id() + if cidRaw, ok := d.GetOk(collectorIDAttr); ok { + cid = cidRaw.(string) + } + collector, err = ctxt.client.FetchBroker(api.CIDType(&cid)) + if err != nil { + return err + } + + d.SetId(collector.CID) + + if err := d.Set(collectorDetailsAttr, collectorDetailsToState(collector)); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to store collector %q attribute: {{err}}", collectorDetailsAttr), err) + } + + d.Set(collectorIDAttr, collector.CID) + d.Set(collectorLatitudeAttr, collector.Latitude) + d.Set(collectorLongitudeAttr, collector.Longitude) + d.Set(collectorNameAttr, collector.Name) + d.Set(collectorTagsAttr, collector.Tags) + d.Set(collectorTypeAttr, collector.Type) + + return nil +} + +func collectorDetailsToState(c *api.Broker) []interface{} { + details := make([]interface{}, 0, len(c.Details)) + + for _, collector := range c.Details { + collectorDetails := make(map[string]interface{}, defaultCollectorDetailAttrs) + + collectorDetails[collectorCNAttr] = collector.CN + + if collector.ExternalHost != nil { + collectorDetails[collectorExternalHostAttr] = *collector.ExternalHost + } + + if collector.ExternalPort != 0 { + collectorDetails[collectorExternalPortAttr] = collector.ExternalPort + } + + if collector.IP != nil { + collectorDetails[collectorIPAttr] = *collector.IP + } + + if collector.MinVer != 0 { + collectorDetails[collectorMinVersionAttr] = collector.MinVer + } + + if len(collector.Modules) > 0 { + collectorDetails[collectorModulesAttr] = collector.Modules + } + + if collector.Port != nil { + collectorDetails[collectorPortAttr] = *collector.Port + } + + if collector.Skew != nil { + collectorDetails[collectorSkewAttr] = *collector.Skew + } + + if collector.Status != "" { + collectorDetails[collectorStatusAttr] = collector.Status + } + + if collector.Version != nil { + collectorDetails[collectorVersionAttr] = *collector.Version + } + + details = append(details, collectorDetails) + } + + return details +} diff --git a/builtin/providers/circonus/data_source_circonus_collector_test.go b/builtin/providers/circonus/data_source_circonus_collector_test.go new file mode 100644 index 000000000..54d5feba9 --- /dev/null +++ b/builtin/providers/circonus/data_source_circonus_collector_test.go @@ -0,0 +1,47 @@ +package circonus + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccDataSourceCirconusCollector(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDataSourceCirconusCollectorConfig, + Check: resource.ComposeTestCheckFunc( + testAccDataSourceCirconusCollectorCheck("data.circonus_collector.by_id", "/broker/1"), + ), + }, + }, + }) +} + +func testAccDataSourceCirconusCollectorCheck(name, cid string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("root module has no resource called %s", name) + } + + attr := rs.Primary.Attributes + + if attr[collectorIDAttr] != cid { + return fmt.Errorf("bad id %s", attr[collectorIDAttr]) + } + + return nil + } +} + +const testAccDataSourceCirconusCollectorConfig = ` +data "circonus_collector" "by_id" { + id = "/broker/1" +} +` diff --git a/builtin/providers/circonus/interface.go b/builtin/providers/circonus/interface.go new file mode 100644 index 000000000..d5777cf81 --- /dev/null +++ b/builtin/providers/circonus/interface.go @@ -0,0 +1,83 @@ +package circonus + +import "log" + +type interfaceList []interface{} +type interfaceMap map[string]interface{} + +// newInterfaceMap returns a helper type that has methods for common operations +// for accessing data. +func newInterfaceMap(l interface{}) interfaceMap { + return interfaceMap(l.(map[string]interface{})) +} + +// CollectList returns []string of values that matched the key attrName. +// interfaceList most likely came from a schema.TypeSet. +func (l interfaceList) CollectList(attrName schemaAttr) []string { + stringList := make([]string, 0, len(l)) + + for _, mapRaw := range l { + mapAttrs := mapRaw.(map[string]interface{}) + + if v, ok := mapAttrs[string(attrName)]; ok { + stringList = append(stringList, v.(string)) + } + } + + return stringList +} + +// List returns a list of values in a Set as a string slice +func (l interfaceList) List() []string { + stringList := make([]string, 0, len(l)) + for _, e := range l { + switch e.(type) { + case string: + stringList = append(stringList, e.(string)) + case []interface{}: + for _, v := range e.([]interface{}) { + stringList = append(stringList, v.(string)) + } + default: + log.Printf("[ERROR] PROVIDER BUG: unable to convert %#v to list", e) + return nil + } + } + return stringList +} + +// CollectList returns []string of values that matched the key attrName. +// interfaceMap most likely came from a schema.TypeSet. +func (m interfaceMap) CollectList(attrName schemaAttr) []string { + stringList := make([]string, 0, len(m)) + + for _, mapRaw := range m { + mapAttrs := mapRaw.(map[string]interface{}) + + if v, ok := mapAttrs[string(attrName)]; ok { + stringList = append(stringList, v.(string)) + } + } + + return stringList +} + +// CollectMap returns map[string]string of values that matched the key attrName. +// interfaceMap most likely came from a schema.TypeSet. +func (m interfaceMap) CollectMap(attrName schemaAttr) map[string]string { + var mergedMap map[string]string + + if attrRaw, ok := m[string(attrName)]; ok { + attrMap := attrRaw.(map[string]interface{}) + mergedMap = make(map[string]string, len(m)) + for k, v := range attrMap { + mergedMap[k] = v.(string) + } + } + + if len(mergedMap) == 0 { + return nil + } + + return mergedMap +} diff --git a/builtin/providers/circonus/metric.go b/builtin/providers/circonus/metric.go new file mode 100644 index 000000000..78483ec79 --- /dev/null +++ b/builtin/providers/circonus/metric.go @@ -0,0 +1,182 @@ +package circonus + +// The circonusMetric type is the backing store of the `circonus_metric` resource. + +import ( + "bytes" + "fmt" + + "github.com/circonus-labs/circonus-gometrics/api" + "github.com/hashicorp/errwrap" + uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +type circonusMetric struct { + ID metricID + api.CheckBundleMetric +} + +func newMetric() circonusMetric { + return circonusMetric{} +} + +func (m *circonusMetric) Create(d *schema.ResourceData) error { + return m.SaveState(d) +} + +func (m *circonusMetric) ParseConfig(id string, d *schema.ResourceData) error { + m.ID = metricID(id) + + if v, found := d.GetOk(metricNameAttr); found { + m.Name = v.(string) + } + + if v, found := d.GetOk(metricActiveAttr); found { + m.Status = metricActiveToAPIStatus(v.(bool)) + } + + if v, found := d.GetOk(metricTagsAttr); found { + m.Tags = derefStringList(flattenSet(v.(*schema.Set))) + } + + if v, found := d.GetOk(metricTypeAttr); found { + m.Type = v.(string) + } + + if v, found := d.GetOk(metricUnitAttr); found { + s := v.(string) + m.Units = &s + } + + if m.Units != nil && *m.Units == "" { + m.Units = nil + } + + return nil +} + +func (m *circonusMetric) ParseConfigMap(id string, attrMap map[string]interface{}) error { + m.ID = metricID(id) + + if v, found := attrMap[metricNameAttr]; found { + m.Name = v.(string) + } + + if v, found := attrMap[metricActiveAttr]; found { + m.Status = metricActiveToAPIStatus(v.(bool)) + } + + if v, found := attrMap[metricTagsAttr]; found { + m.Tags = derefStringList(flattenSet(v.(*schema.Set))) + } + + if v, found := attrMap[metricTypeAttr]; found { + m.Type = v.(string) + } + + if v, found := attrMap[metricUnitAttr]; found { + s := v.(string) + m.Units = &s + } + + if m.Units != nil && *m.Units == "" { + m.Units = nil + } + + return nil +} + +func (m *circonusMetric) SaveState(d *schema.ResourceData) error { + d.SetId(string(m.ID)) + + d.Set(metricActiveAttr, metricAPIStatusToBool(m.Status)) + d.Set(metricNameAttr, m.Name) + d.Set(metricTagsAttr, tagsToState(apiToTags(m.Tags))) + d.Set(metricTypeAttr, m.Type) + d.Set(metricUnitAttr, indirect(m.Units)) + + return nil +} + +func (m *circonusMetric) Update(d *schema.ResourceData) error { + // NOTE: there are no "updates" to be made against an API server, so we just + // pass through a call to SaveState. Keep this method around for API + // symmetry. + return m.SaveState(d) +} + +func metricAPIStatusToBool(s string) bool { + switch s { + case metricStatusActive: + return true + case metricStatusAvailable: + return false + default: + // log.Printf("PROVIDER BUG: metric status %q unsupported", s) + return false + } +} + +func metricActiveToAPIStatus(active bool) string { + if active { + return metricStatusActive + } + + return metricStatusAvailable +} + +func newMetricID() (string, error) { + id, err := uuid.GenerateUUID() + if err != nil { + return "", errwrap.Wrapf("metric ID creation failed: {{err}}", err) + } + + return id, nil +} + +func metricChecksum(m interfaceMap) int { + b := &bytes.Buffer{} + b.Grow(defaultHashBufSize) + + // Order writes to the buffer using lexically sorted list for easy visual + // reconciliation with other lists. + if v, found := m[metricActiveAttr]; found { + fmt.Fprintf(b, "%t", v.(bool)) + } + + if v, found := m[metricNameAttr]; found { + fmt.Fprint(b, v.(string)) + } + + if v, found := m[metricTagsAttr]; found { + tags := derefStringList(flattenSet(v.(*schema.Set))) + for _, tag := range tags { + fmt.Fprint(b, tag) + } + } + + if v, found := m[metricTypeAttr]; found { + fmt.Fprint(b, v.(string)) + } + + if v, found := m[metricUnitAttr]; found { + if v != nil { + var s string + switch v.(type) { + case string: + s = v.(string) + case *string: + s = *v.(*string) + } + + if s != "" { + fmt.Fprint(b, s) + } + } + } + + s := b.String() + return hashcode.String(s) +} diff --git a/builtin/providers/circonus/metric_cluster.go b/builtin/providers/circonus/metric_cluster.go new file mode 100644 index 000000000..6d06834f4 --- /dev/null +++ b/builtin/providers/circonus/metric_cluster.go @@ -0,0 +1,57 @@ +package circonus + +import ( + "fmt" + + "github.com/circonus-labs/circonus-gometrics/api" + "github.com/hashicorp/errwrap" +) + +type circonusMetricCluster struct { + api.MetricCluster +} + +func newMetricCluster() circonusMetricCluster { + return circonusMetricCluster{ + MetricCluster: api.MetricCluster{}, + } +} + +func loadMetricCluster(ctxt *providerContext, cid api.CIDType) (circonusMetricCluster, error) { + var mc circonusMetricCluster + cmc, err := ctxt.client.FetchMetricCluster(cid, "") + if err != nil { + return circonusMetricCluster{}, err + } + mc.MetricCluster = *cmc + + return mc, nil +} + +func (mc *circonusMetricCluster) Create(ctxt *providerContext) error { + cmc, err := ctxt.client.CreateMetricCluster(&mc.MetricCluster) + if err != nil { + return err + } + + mc.CID = cmc.CID + + return nil +} + +func (mc *circonusMetricCluster) Update(ctxt *providerContext) error { + _, err := ctxt.client.UpdateMetricCluster(&mc.MetricCluster) + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to update stream group %s: {{err}}", mc.CID), err) + } + + return nil +} + +func (mc *circonusMetricCluster) Validate() error { + if len(mc.Queries) < 1 { + return fmt.Errorf("there must be at least one stream group query present") + } + + return nil +} diff --git a/builtin/providers/circonus/metric_test.go b/builtin/providers/circonus/metric_test.go new file mode 100644 index 000000000..b7b28efb8 --- /dev/null +++ b/builtin/providers/circonus/metric_test.go @@ -0,0 +1,19 @@ +package circonus + +import "testing" + +func Test_MetricChecksum(t *testing.T) { + unit := "qty" + m := interfaceMap{ + string(metricActiveAttr): true, + string(metricNameAttr): "asdf", + string(metricTagsAttr): tagsToState(apiToTags([]string{"foo", "bar"})), + string(metricTypeAttr): "json", + string(metricUnitAttr): &unit, + } + + csum := metricChecksum(m) + if csum != 4250221491 { + t.Fatalf("Checksum mismatch") + } +} diff --git a/builtin/providers/circonus/provider.go b/builtin/providers/circonus/provider.go new file mode 100644 index 000000000..9796d6be0 --- /dev/null +++ b/builtin/providers/circonus/provider.go @@ -0,0 +1,135 @@ +package circonus + +import ( + "bytes" + "fmt" + + "github.com/circonus-labs/circonus-gometrics/api" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +const ( + defaultCirconus404ErrorString = "API response code 404:" + defaultCirconusAggregationWindow = "300s" + defaultCirconusAlertMinEscalateAfter = "300s" + defaultCirconusCheckPeriodMax = "300s" + defaultCirconusCheckPeriodMin = "30s" + defaultCirconusHTTPFormat = "json" + defaultCirconusHTTPMethod = "POST" + defaultCirconusSlackUsername = "Circonus" + defaultCirconusTimeoutMax = "300s" + defaultCirconusTimeoutMin = "0s" + maxSeverity = 5 + minSeverity = 1 +) + +var providerDescription = map[string]string{ + providerAPIURLAttr: "URL of the Circonus API", + providerAutoTagAttr: "Signals that the provider should automatically add a tag to all API calls denoting that the resource was created by Terraform", + providerKeyAttr: "API token used to authenticate with the Circonus API", +} + +// Constants that want to be a constant but can't in Go +var ( + validContactHTTPFormats = validStringValues{"json", "params"} + validContactHTTPMethods = validStringValues{"GET", "POST"} +) + +type contactMethods string + +// globalAutoTag controls whether or not the provider should automatically add a +// tag to each resource. +// +// NOTE(sean): This is done as a global variable because the diff suppress +// functions does not have access to the providerContext, only the key, old, and +// new values. +var globalAutoTag bool + +type providerContext struct { + // Circonus API client + client *api.API + + // autoTag, when true, automatically appends defaultCirconusTag + autoTag bool + + // defaultTag make up the tag to be used when autoTag tags a tag. + defaultTag circonusTag +} + +// Provider returns a terraform.ResourceProvider. +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + providerAPIURLAttr: { + Type: schema.TypeString, + Optional: true, + Default: "https://api.circonus.com/v2", + Description: providerDescription[providerAPIURLAttr], + }, + providerAutoTagAttr: { + Type: schema.TypeBool, + Optional: true, + Default: defaultAutoTag, + Description: providerDescription[providerAutoTagAttr], + }, + providerKeyAttr: { + Type: schema.TypeString, + Required: true, + Sensitive: true, + DefaultFunc: schema.EnvDefaultFunc("CIRCONUS_API_TOKEN", nil), + Description: providerDescription[providerKeyAttr], + }, + }, + + DataSourcesMap: map[string]*schema.Resource{ + "circonus_account": dataSourceCirconusAccount(), + "circonus_collector": dataSourceCirconusCollector(), + }, + + ResourcesMap: map[string]*schema.Resource{ + "circonus_check": resourceCheck(), + "circonus_contact_group": resourceContactGroup(), + "circonus_graph": resourceGraph(), + "circonus_metric": resourceMetric(), + "circonus_metric_cluster": resourceMetricCluster(), + "circonus_rule_set": resourceRuleSet(), + }, + + ConfigureFunc: providerConfigure, + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + globalAutoTag = d.Get(providerAutoTagAttr).(bool) + + config := &api.Config{ + URL: d.Get(providerAPIURLAttr).(string), + TokenKey: d.Get(providerKeyAttr).(string), + TokenApp: tfAppName(), + } + + client, err := api.NewAPI(config) + if err != nil { + return nil, errwrap.Wrapf("Error initializing Circonus: %s", err) + } + + return &providerContext{ + client: client, + autoTag: d.Get(providerAutoTagAttr).(bool), + defaultTag: defaultCirconusTag, + }, nil +} + +func tfAppName() string { + const VersionPrerelease = terraform.VersionPrerelease + var versionString bytes.Buffer + + fmt.Fprintf(&versionString, "Terraform v%s", terraform.Version) + if VersionPrerelease != "" { + fmt.Fprintf(&versionString, "-%s", VersionPrerelease) + } + + return versionString.String() +} diff --git a/builtin/providers/circonus/provider_test.go b/builtin/providers/circonus/provider_test.go new file mode 100644 index 000000000..4a30f4877 --- /dev/null +++ b/builtin/providers/circonus/provider_test.go @@ -0,0 +1,35 @@ +package circonus + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "circonus": testAccProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +func testAccPreCheck(t *testing.T) { + if apiToken := os.Getenv("CIRCONUS_API_TOKEN"); apiToken == "" { + t.Fatal("CIRCONUS_API_TOKEN must be set for acceptance tests") + } +} diff --git a/builtin/providers/circonus/resource_circonus_check.go b/builtin/providers/circonus/resource_circonus_check.go new file mode 100644 index 000000000..26eebc0b7 --- /dev/null +++ b/builtin/providers/circonus/resource_circonus_check.go @@ -0,0 +1,607 @@ +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" + checkHTTPAttr = "http" + checkHTTPTrapAttr = "httptrap" + checkICMPPingAttr = "icmp_ping" + checkJSONAttr = "json" + checkMetricLimitAttr = "metric_limit" + checkMySQLAttr = "mysql" + checkNameAttr = "name" + checkNotesAttr = "notes" + checkPeriodAttr = "period" + checkPostgreSQLAttr = "postgresql" + checkMetricAttr = "metric" + checkTagsAttr = "tags" + checkTargetAttr = "target" + checkTCPAttr = "tcp" + 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" + checkOutCheckUUIDsAttr = "uuids" + checkOutChecksAttr = "checks" + checkOutCreatedAttr = "created" + checkOutLastModifiedAttr = "last_modified" + checkOutLastModifiedByAttr = "last_modified_by" + checkOutReverseConnectURLsAttr = "reverse_connect_urls" +) + +const ( + // Circonus API constants from their API endpoints + apiCheckTypeCAQLAttr apiCheckType = "caql" + apiCheckTypeCloudWatchAttr apiCheckType = "cloudwatch" + apiCheckTypeHTTPAttr apiCheckType = "http" + apiCheckTypeHTTPTrapAttr apiCheckType = "httptrap" + apiCheckTypeICMPPingAttr apiCheckType = "ping_icmp" + apiCheckTypeJSONAttr apiCheckType = "json" + apiCheckTypeMySQLAttr apiCheckType = "mysql" + apiCheckTypePostgreSQLAttr apiCheckType = "postgres" + 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", + checkHTTPAttr: "HTTP check configuration", + checkHTTPTrapAttr: "HTTP Trap check configuration", + checkICMPPingAttr: "ICMP ping check configuration", + checkJSONAttr: "JSON check configuration", + 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", + checkMetricAttr: "Configuration for a stream of metrics", + checkTagsAttr: "A list of tags assigned to the check", + checkTargetAttr: "The target of the check (e.g. hostname, URL, IP, etc)", + checkTCPAttr: "TCP check configuration", + 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", + + checkOutChecksAttr: "", + checkOutByCollectorAttr: "", + checkOutCheckUUIDsAttr: "", + checkOutCreatedAttr: "", + 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), + }, + }), + }, + }, + checkHTTPAttr: schemaCheckHTTP, + checkHTTPTrapAttr: schemaCheckHTTPTrap, + checkJSONAttr: schemaCheckJSON, + checkICMPPingAttr: schemaCheckICMPPing, + 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, + 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), + }, + }), + }, + }, + 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 + 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] + } + + 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) + } + + 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, + checkHTTPAttr: checkConfigToAPIHTTP, + checkHTTPTrapAttr: checkConfigToAPIHTTPTrap, + checkICMPPingAttr: checkConfigToAPIICMPPing, + checkJSONAttr: checkConfigToAPIJSON, + checkMySQLAttr: checkConfigToAPIMySQL, + checkPostgreSQLAttr: checkConfigToAPIPostgreSQL, + checkTCPAttr: checkConfigToAPITCP, + } + + 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) + } + } + } + + 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, + apiCheckTypeHTTPAttr: checkAPIToStateHTTP, + apiCheckTypeHTTPTrapAttr: checkAPIToStateHTTPTrap, + apiCheckTypeICMPPingAttr: checkAPIToStateICMPPing, + apiCheckTypeJSONAttr: checkAPIToStateJSON, + apiCheckTypeMySQLAttr: checkAPIToStateMySQL, + apiCheckTypePostgreSQLAttr: checkAPIToStatePostgreSQL, + 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 +} diff --git a/builtin/providers/circonus/resource_circonus_check_caql.go b/builtin/providers/circonus/resource_circonus_check_caql.go new file mode 100644 index 000000000..addf794ba --- /dev/null +++ b/builtin/providers/circonus/resource_circonus_check_caql.go @@ -0,0 +1,89 @@ +package circonus + +import ( + "bytes" + "fmt" + "strings" + + "github.com/circonus-labs/circonus-gometrics/api/config" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +const ( + // circonus_check.caql.* resource attribute names + checkCAQLQueryAttr = "query" +) + +var checkCAQLDescriptions = attrDescrs{ + checkCAQLQueryAttr: "The query definition", +} + +var schemaCheckCAQL = &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + MinItems: 1, + Set: hashCheckCAQL, + Elem: &schema.Resource{ + Schema: convertToHelperSchema(checkCAQLDescriptions, map[schemaAttr]*schema.Schema{ + checkCAQLQueryAttr: &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateRegexp(checkCAQLQueryAttr, `.+`), + }, + }), + }, +} + +// checkAPIToStateCAQL reads the Config data out of circonusCheck.CheckBundle +// into the statefile. +func checkAPIToStateCAQL(c *circonusCheck, d *schema.ResourceData) error { + caqlConfig := make(map[string]interface{}, len(c.Config)) + + caqlConfig[string(checkCAQLQueryAttr)] = c.Config[config.Query] + + if err := d.Set(checkCAQLAttr, schema.NewSet(hashCheckCAQL, []interface{}{caqlConfig})); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to store check %q attribute: {{err}}", checkCAQLAttr), err) + } + + return nil +} + +// hashCheckCAQL creates a stable hash of the normalized values +func hashCheckCAQL(v interface{}) int { + m := v.(map[string]interface{}) + b := &bytes.Buffer{} + b.Grow(defaultHashBufSize) + + writeString := func(attrName schemaAttr) { + if v, ok := m[string(attrName)]; ok && v.(string) != "" { + fmt.Fprint(b, strings.TrimSpace(v.(string))) + } + } + + // Order writes to the buffer using lexically sorted list for easy visual + // reconciliation with other lists. + writeString(checkCAQLQueryAttr) + + s := b.String() + return hashcode.String(s) +} + +func checkConfigToAPICAQL(c *circonusCheck, l interfaceList) error { + c.Type = string(apiCheckTypeCAQL) + c.Target = defaultCheckCAQLTarget + + // Iterate over all `icmp_ping` attributes, even though we have a max of 1 in + // the schema. + for _, mapRaw := range l { + caqlConfig := newInterfaceMap(mapRaw) + + if v, found := caqlConfig[checkCAQLQueryAttr]; found { + c.Config[config.Query] = v.(string) + } + } + + return nil +} diff --git a/builtin/providers/circonus/resource_circonus_check_caql_test.go b/builtin/providers/circonus/resource_circonus_check_caql_test.go new file mode 100644 index 000000000..5efcb6ad9 --- /dev/null +++ b/builtin/providers/circonus/resource_circonus_check_caql_test.go @@ -0,0 +1,74 @@ +package circonus + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccCirconusCheckCAQL_basic(t *testing.T) { + checkName := fmt.Sprintf("Consul's Go GC latency (Merged Histogram) - %s", acctest.RandString(5)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDestroyCirconusCheckBundle, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccCirconusCheckCAQLConfigFmt, checkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("circonus_check.go_gc_latency", "active", "true"), + resource.TestCheckResourceAttr("circonus_check.go_gc_latency", "collector.#", "1"), + resource.TestCheckResourceAttr("circonus_check.go_gc_latency", "collector.36214388.id", "/broker/1490"), + resource.TestCheckResourceAttr("circonus_check.go_gc_latency", "caql.#", "1"), + resource.TestCheckResourceAttr("circonus_check.go_gc_latency", "caql.4060628048.query", `search:metric:histogram("*consul*runtime`+"`"+`gc_pause_ns* (active:1)") | histogram:merge() | histogram:percentile(99)`), + resource.TestCheckResourceAttr("circonus_check.go_gc_latency", "name", checkName), + resource.TestCheckResourceAttr("circonus_check.go_gc_latency", "period", "60s"), + resource.TestCheckResourceAttr("circonus_check.go_gc_latency", "metric.#", "1"), + + resource.TestCheckResourceAttr("circonus_check.go_gc_latency", "tags.#", "4"), + resource.TestCheckResourceAttr("circonus_check.go_gc_latency", "tags.3728194417", "app:consul"), + resource.TestCheckResourceAttr("circonus_check.go_gc_latency", "tags.2087084518", "author:terraform"), + resource.TestCheckResourceAttr("circonus_check.go_gc_latency", "tags.1401442048", "lifecycle:unittest"), + resource.TestCheckResourceAttr("circonus_check.go_gc_latency", "tags.3480593708", "source:goruntime"), + resource.TestCheckResourceAttr("circonus_check.go_gc_latency", "target", "q._caql"), + resource.TestCheckResourceAttr("circonus_check.go_gc_latency", "type", "caql"), + ), + }, + }, + }) +} + +const testAccCirconusCheckCAQLConfigFmt = ` +variable "test_tags" { + type = "list" + default = [ "app:consul", "author:terraform", "lifecycle:unittest", "source:goruntime" ] +} + +resource "circonus_check" "go_gc_latency" { + active = true + name = "%s" + period = "60s" + + collector { + id = "/broker/1490" + } + + caql { + query = <"), + resource.TestCheckResourceAttr("circonus_check.usage", "json.2626248092.version", "1.0"), + resource.TestCheckResourceAttr("circonus_check.usage", "json.2626248092.method", "GET"), + resource.TestCheckResourceAttr("circonus_check.usage", "json.2626248092.port", "443"), + resource.TestCheckResourceAttr("circonus_check.usage", "json.2626248092.read_limit", "1048576"), + resource.TestCheckResourceAttr("circonus_check.usage", "json.2626248092.url", "https://api.circonus.com/account/current"), + resource.TestCheckResourceAttr("circonus_check.usage", "name", "Terraform test: api.circonus.com metric usage check"), + resource.TestCheckResourceAttr("circonus_check.usage", "notes", ""), + resource.TestCheckResourceAttr("circonus_check.usage", "period", "60s"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.#", "2"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.1992097900.active", "true"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.1992097900.name", "_usage`0`_limit"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.1992097900.tags.#", "1"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.1992097900.tags.3241999189", "source:circonus"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.1992097900.type", "numeric"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.1992097900.unit", "qty"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.3280673139.active", "true"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.3280673139.name", "_usage`0`_used"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.3280673139.tags.#", "1"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.3280673139.tags.3241999189", "source:circonus"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.3280673139.type", "numeric"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.3280673139.unit", "qty"), + resource.TestCheckResourceAttr("circonus_check.usage", "tags.#", "2"), + resource.TestCheckResourceAttr("circonus_check.usage", "tags.3241999189", "source:circonus"), + resource.TestCheckResourceAttr("circonus_check.usage", "tags.3839162439", "source:unittest"), + resource.TestCheckResourceAttr("circonus_check.usage", "target", "api.circonus.com"), + resource.TestCheckResourceAttr("circonus_check.usage", "type", "json"), + ), + }, + { + Config: testAccCirconusCheckJSONConfig2, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("circonus_check.usage", "active", "true"), + resource.TestCheckResourceAttr("circonus_check.usage", "collector.#", "1"), + resource.TestCheckResourceAttr("circonus_check.usage", "collector.2388330941.id", "/broker/1"), + resource.TestCheckResourceAttr("circonus_check.usage", "json.#", "1"), + // resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.auth_method", ""), + // resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.auth_password", ""), + // resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.auth_user", ""), + // resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.ca_chain", ""), + // resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.certificate_file", ""), + // resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.ciphers", ""), + // resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.key_file", ""), + // resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.payload", ""), + resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.headers.%", "3"), + resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.headers.Accept", "application/json"), + resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.headers.X-Circonus-App-Name", "TerraformCheck"), + resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.headers.X-Circonus-Auth-Token", ""), + resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.version", "1.1"), + resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.method", "GET"), + resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.port", "443"), + resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.read_limit", "1048576"), + resource.TestCheckResourceAttr("circonus_check.usage", "json.3951979786.url", "https://api.circonus.com/account/current"), + resource.TestCheckResourceAttr("circonus_check.usage", "name", "Terraform test: api.circonus.com metric usage check"), + resource.TestCheckResourceAttr("circonus_check.usage", "notes", "notes!"), + resource.TestCheckResourceAttr("circonus_check.usage", "period", "300s"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.#", "2"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.1992097900.active", "true"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.1992097900.name", "_usage`0`_limit"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.1992097900.tags.#", "1"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.1992097900.tags.3241999189", "source:circonus"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.1992097900.type", "numeric"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.1992097900.unit", "qty"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.3280673139.active", "true"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.3280673139.name", "_usage`0`_used"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.3280673139.tags.#", "1"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.3280673139.tags.3241999189", "source:circonus"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.3280673139.type", "numeric"), + resource.TestCheckResourceAttr("circonus_check.usage", "metric.3280673139.unit", "qty"), + resource.TestCheckResourceAttr("circonus_check.usage", "tags.#", "2"), + resource.TestCheckResourceAttr("circonus_check.usage", "tags.3241999189", "source:circonus"), + resource.TestCheckResourceAttr("circonus_check.usage", "tags.3839162439", "source:unittest"), + resource.TestCheckResourceAttr("circonus_check.usage", "target", "api.circonus.com"), + resource.TestCheckResourceAttr("circonus_check.usage", "type", "json"), + ), + }, + }, + }) +} + +const testAccCirconusCheckJSONConfig1 = ` +variable "usage_default_unit" { + default = "qty" +} + +resource "circonus_metric" "limit" { + name = "_usage` + "`0`" + `_limit" + tags = [ "source:circonus" ] + type = "numeric" + unit = "${var.usage_default_unit}" +} + +resource "circonus_metric" "used" { + name = "_usage` + "`0`" + `_used" + tags = [ "source:circonus" ] + type = "numeric" + unit = "${var.usage_default_unit}" +} + +resource "circonus_check" "usage" { + active = true + name = "Terraform test: api.circonus.com metric usage check" + period = "60s" + + collector { + id = "/broker/1" + } + + json { + url = "https://api.circonus.com/account/current" + headers = { + Accept = "application/json", + X-Circonus-App-Name = "TerraformCheck", + X-Circonus-Auth-Token = "", + } + version = "1.0" + method = "GET" + port = 443 + read_limit = 1048576 + } + + metric { + name = "${circonus_metric.used.name}" + tags = [ "${circonus_metric.used.tags}" ] + type = "${circonus_metric.used.type}" + unit = "${coalesce(circonus_metric.used.unit, var.usage_default_unit)}" + } + + metric { + name = "${circonus_metric.limit.name}" + tags = [ "${circonus_metric.limit.tags}" ] + type = "${circonus_metric.limit.type}" + unit = "${coalesce(circonus_metric.limit.unit, var.usage_default_unit)}" + } + + tags = [ "source:circonus", "source:unittest" ] +} +` + +const testAccCirconusCheckJSONConfig2 = ` +variable "usage_default_unit" { + default = "qty" +} + +resource "circonus_metric" "limit" { + name = "_usage` + "`0`" + `_limit" + tags = [ "source:circonus" ] + type = "numeric" + unit = "${var.usage_default_unit}" +} + +resource "circonus_metric" "used" { + name = "_usage` + "`0`" + `_used" + tags = [ "source:circonus" ] + type = "numeric" + unit = "${var.usage_default_unit}" +} + +resource "circonus_check" "usage" { + active = true + name = "Terraform test: api.circonus.com metric usage check" + notes = "notes!" + period = "300s" + + collector { + id = "/broker/1" + } + + json { + url = "https://api.circonus.com/account/current" + headers = { + Accept = "application/json", + X-Circonus-App-Name = "TerraformCheck", + X-Circonus-Auth-Token = "", + } + version = "1.1" + method = "GET" + port = 443 + read_limit = 1048576 + } + + metric { + name = "${circonus_metric.used.name}" + tags = [ "${circonus_metric.used.tags}" ] + type = "${circonus_metric.used.type}" + unit = "${coalesce(circonus_metric.used.unit, var.usage_default_unit)}" + } + + metric { + name = "${circonus_metric.limit.name}" + tags = [ "${circonus_metric.limit.tags}" ] + type = "${circonus_metric.limit.type}" + unit = "${coalesce(circonus_metric.limit.unit, var.usage_default_unit)}" + } + + tags = [ "source:circonus", "source:unittest" ] +} +` diff --git a/builtin/providers/circonus/resource_circonus_check_mysql.go b/builtin/providers/circonus/resource_circonus_check_mysql.go new file mode 100644 index 000000000..4414e1c71 --- /dev/null +++ b/builtin/providers/circonus/resource_circonus_check_mysql.go @@ -0,0 +1,102 @@ +package circonus + +import ( + "bytes" + "fmt" + "strings" + + "github.com/circonus-labs/circonus-gometrics/api/config" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +const ( + // circonus_check.mysql.* resource attribute names + checkMySQLDSNAttr = "dsn" + checkMySQLQueryAttr = "query" +) + +var checkMySQLDescriptions = attrDescrs{ + checkMySQLDSNAttr: "The connect DSN for the MySQL instance", + checkMySQLQueryAttr: "The SQL to use as the query", +} + +var schemaCheckMySQL = &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + MinItems: 1, + Set: hashCheckMySQL, + Elem: &schema.Resource{ + Schema: convertToHelperSchema(checkMySQLDescriptions, map[schemaAttr]*schema.Schema{ + checkMySQLDSNAttr: &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateRegexp(checkMySQLDSNAttr, `^.+$`), + }, + checkMySQLQueryAttr: &schema.Schema{ + Type: schema.TypeString, + Required: true, + StateFunc: func(v interface{}) string { return strings.TrimSpace(v.(string)) }, + ValidateFunc: validateRegexp(checkMySQLQueryAttr, `.+`), + }, + }), + }, +} + +// checkAPIToStateMySQL reads the Config data out of circonusCheck.CheckBundle into the +// statefile. +func checkAPIToStateMySQL(c *circonusCheck, d *schema.ResourceData) error { + MySQLConfig := make(map[string]interface{}, len(c.Config)) + + MySQLConfig[string(checkMySQLDSNAttr)] = c.Config[config.DSN] + MySQLConfig[string(checkMySQLQueryAttr)] = c.Config[config.SQL] + + if err := d.Set(checkMySQLAttr, schema.NewSet(hashCheckMySQL, []interface{}{MySQLConfig})); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to store check %q attribute: {{err}}", checkMySQLAttr), err) + } + + return nil +} + +// hashCheckMySQL creates a stable hash of the normalized values +func hashCheckMySQL(v interface{}) int { + m := v.(map[string]interface{}) + b := &bytes.Buffer{} + b.Grow(defaultHashBufSize) + + writeString := func(attrName schemaAttr) { + if v, ok := m[string(attrName)]; ok && v.(string) != "" { + fmt.Fprint(b, strings.TrimSpace(v.(string))) + } + } + + // Order writes to the buffer using lexically sorted list for easy visual + // reconciliation with other lists. + writeString(checkMySQLDSNAttr) + writeString(checkMySQLQueryAttr) + + s := b.String() + return hashcode.String(s) +} + +func checkConfigToAPIMySQL(c *circonusCheck, l interfaceList) error { + c.Type = string(apiCheckTypeMySQL) + + // Iterate over all `postgres` attributes, even though we have a max of 1 in + // the schema. + for _, mapRaw := range l { + mysqlConfig := newInterfaceMap(mapRaw) + + if v, found := mysqlConfig[checkMySQLDSNAttr]; found { + c.Config[config.DSN] = v.(string) + } + + if v, found := mysqlConfig[checkMySQLQueryAttr]; found { + c.Config[config.SQL] = v.(string) + } + } + + return nil +} diff --git a/builtin/providers/circonus/resource_circonus_check_mysql_test.go b/builtin/providers/circonus/resource_circonus_check_mysql_test.go new file mode 100644 index 000000000..063cc54b4 --- /dev/null +++ b/builtin/providers/circonus/resource_circonus_check_mysql_test.go @@ -0,0 +1,80 @@ +package circonus + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccCirconusCheckMySQL_basic(t *testing.T) { + checkName := fmt.Sprintf("MySQL binlog total - %s", acctest.RandString(5)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDestroyCirconusCheckBundle, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccCirconusCheckMySQLConfigFmt, checkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("circonus_check.table_ops", "active", "true"), + resource.TestCheckResourceAttr("circonus_check.table_ops", "collector.#", "1"), + resource.TestCheckResourceAttr("circonus_check.table_ops", "collector.2388330941.id", "/broker/1"), + resource.TestCheckResourceAttr("circonus_check.table_ops", "mysql.#", "1"), + resource.TestCheckResourceAttr("circonus_check.table_ops", "mysql.3110376931.dsn", "user=mysql host=mydb1.example.org port=3306 password=12345 sslmode=require"), + resource.TestCheckResourceAttr("circonus_check.table_ops", "mysql.3110376931.query", `select 'binlog', total from (select variable_value as total from information_schema.global_status where variable_name='BINLOG_CACHE_USE') total`), + resource.TestCheckResourceAttr("circonus_check.table_ops", "name", checkName), + resource.TestCheckResourceAttr("circonus_check.table_ops", "period", "300s"), + resource.TestCheckResourceAttr("circonus_check.table_ops", "metric.#", "1"), + + resource.TestCheckResourceAttr("circonus_check.table_ops", "metric.885029470.name", "binlog`total"), + resource.TestCheckResourceAttr("circonus_check.table_ops", "metric.885029470.tags.#", "2"), + resource.TestCheckResourceAttr("circonus_check.table_ops", "metric.885029470.tags.2087084518", "author:terraform"), + resource.TestCheckResourceAttr("circonus_check.table_ops", "metric.885029470.tags.1401442048", "lifecycle:unittest"), + resource.TestCheckResourceAttr("circonus_check.table_ops", "metric.885029470.type", "numeric"), + + resource.TestCheckResourceAttr("circonus_check.table_ops", "tags.#", "2"), + resource.TestCheckResourceAttr("circonus_check.table_ops", "tags.2087084518", "author:terraform"), + resource.TestCheckResourceAttr("circonus_check.table_ops", "tags.1401442048", "lifecycle:unittest"), + resource.TestCheckResourceAttr("circonus_check.table_ops", "target", "mydb.example.org"), + resource.TestCheckResourceAttr("circonus_check.table_ops", "type", "mysql"), + ), + }, + }, + }) +} + +const testAccCirconusCheckMySQLConfigFmt = ` +variable "test_tags" { + type = "list" + default = [ "author:terraform", "lifecycle:unittest" ] +} + +resource "circonus_check" "table_ops" { + active = true + name = "%s" + period = "300s" + + collector { + id = "/broker/1" + } + + mysql { + dsn = "user=mysql host=mydb1.example.org port=3306 password=12345 sslmode=require" + query = < 1 { + alertOptionsList = append(alertOptionsList, *alertOptions[i]) + } + } + + return alertOptionsList +} + +func contactGroupEmailToState(cg *api.ContactGroup) []interface{} { + emailContacts := make([]interface{}, 0, len(cg.Contacts.Users)+len(cg.Contacts.External)) + + for _, ext := range cg.Contacts.External { + switch ext.Method { + case circonusMethodEmail: + emailContacts = append(emailContacts, map[string]interface{}{ + contactEmailAddressAttr: ext.Info, + }) + } + } + + for _, user := range cg.Contacts.Users { + switch user.Method { + case circonusMethodEmail: + emailContacts = append(emailContacts, map[string]interface{}{ + contactUserCIDAttr: user.UserCID, + }) + } + } + + return emailContacts +} + +func contactGroupHTTPToState(cg *api.ContactGroup) ([]interface{}, error) { + httpContacts := make([]interface{}, 0, len(cg.Contacts.External)) + + for _, ext := range cg.Contacts.External { + switch ext.Method { + case contactHTTPAttr: + url := contactHTTPInfo{} + if err := json.Unmarshal([]byte(ext.Info), &url); err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("unable to decode external %s JSON (%q): {{err}}", contactHTTPAttr, ext.Info), err) + } + + httpContacts = append(httpContacts, map[string]interface{}{ + string(contactHTTPAddressAttr): url.Address, + string(contactHTTPFormatAttr): url.Format, + string(contactHTTPMethodAttr): url.Method, + }) + } + } + + return httpContacts, nil +} + +func getContactGroupInput(d *schema.ResourceData) (*api.ContactGroup, error) { + cg := api.NewContactGroup() + if v, ok := d.GetOk(contactAggregationWindowAttr); ok { + aggWindow, _ := time.ParseDuration(v.(string)) + cg.AggregationWindow = uint(aggWindow.Seconds()) + } + + if v, ok := d.GetOk(contactAlertOptionAttr); ok { + alertOptionsRaw := v.(*schema.Set).List() + + ensureEscalationSeverity := func(severity int) { + if cg.Escalations[severity] == nil { + cg.Escalations[severity] = &api.ContactGroupEscalation{} + } + } + + for _, alertOptionRaw := range alertOptionsRaw { + alertOptionsMap := alertOptionRaw.(map[string]interface{}) + + severityIndex := -1 + + if optRaw, ok := alertOptionsMap[contactSeverityAttr]; ok { + severityIndex = optRaw.(int) - 1 + } + + if optRaw, ok := alertOptionsMap[contactEscalateAfterAttr]; ok { + if optRaw.(string) != "" { + d, _ := time.ParseDuration(optRaw.(string)) + if d != 0 { + ensureEscalationSeverity(severityIndex) + cg.Escalations[severityIndex].After = uint(d.Seconds()) + } + } + } + + if optRaw, ok := alertOptionsMap[contactEscalateToAttr]; ok && optRaw.(string) != "" { + ensureEscalationSeverity(severityIndex) + cg.Escalations[severityIndex].ContactGroupCID = optRaw.(string) + } + + if optRaw, ok := alertOptionsMap[contactReminderAttr]; ok { + if optRaw.(string) == "" { + optRaw = "0s" + } + + d, _ := time.ParseDuration(optRaw.(string)) + cg.Reminders[severityIndex] = uint(d.Seconds()) + } + } + } + + if v, ok := d.GetOk(contactNameAttr); ok { + cg.Name = v.(string) + } + + if v, ok := d.GetOk(contactEmailAttr); ok { + emailListRaw := v.(*schema.Set).List() + for _, emailMapRaw := range emailListRaw { + emailMap := emailMapRaw.(map[string]interface{}) + + var requiredAttrFound bool + if v, ok := emailMap[contactEmailAddressAttr]; ok && v.(string) != "" { + requiredAttrFound = true + cg.Contacts.External = append(cg.Contacts.External, api.ContactGroupContactsExternal{ + Info: v.(string), + Method: circonusMethodEmail, + }) + } + + if v, ok := emailMap[contactUserCIDAttr]; ok && v.(string) != "" { + requiredAttrFound = true + cg.Contacts.Users = append(cg.Contacts.Users, api.ContactGroupContactsUser{ + Method: circonusMethodEmail, + UserCID: v.(string), + }) + } + + // Can't mark two attributes that are conflicting as required so we do our + // own validation check here. + if !requiredAttrFound { + return nil, fmt.Errorf("In type %s, either %s or %s must be specified", contactEmailAttr, contactEmailAddressAttr, contactUserCIDAttr) + } + } + } + + if v, ok := d.GetOk(contactHTTPAttr); ok { + httpListRaw := v.(*schema.Set).List() + for _, httpMapRaw := range httpListRaw { + httpMap := httpMapRaw.(map[string]interface{}) + + httpInfo := contactHTTPInfo{} + + if v, ok := httpMap[string(contactHTTPAddressAttr)]; ok { + httpInfo.Address = v.(string) + } + + if v, ok := httpMap[string(contactHTTPFormatAttr)]; ok { + httpInfo.Format = v.(string) + } + + if v, ok := httpMap[string(contactHTTPMethodAttr)]; ok { + httpInfo.Method = v.(string) + } + + js, err := json.Marshal(httpInfo) + if err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("error marshalling %s JSON config string: {{err}}", contactHTTPAttr), err) + } + + cg.Contacts.External = append(cg.Contacts.External, api.ContactGroupContactsExternal{ + Info: string(js), + Method: circonusMethodHTTP, + }) + } + } + + if v, ok := d.GetOk(contactIRCAttr); ok { + ircListRaw := v.(*schema.Set).List() + for _, ircMapRaw := range ircListRaw { + ircMap := ircMapRaw.(map[string]interface{}) + + if v, ok := ircMap[contactUserCIDAttr]; ok && v.(string) != "" { + cg.Contacts.Users = append(cg.Contacts.Users, api.ContactGroupContactsUser{ + Method: circonusMethodIRC, + UserCID: v.(string), + }) + } + } + } + + if v, ok := d.GetOk(contactPagerDutyAttr); ok { + pagerDutyListRaw := v.(*schema.Set).List() + for _, pagerDutyMapRaw := range pagerDutyListRaw { + pagerDutyMap := pagerDutyMapRaw.(map[string]interface{}) + + pagerDutyInfo := contactPagerDutyInfo{} + + if v, ok := pagerDutyMap[contactContactGroupFallbackAttr]; ok && v.(string) != "" { + cid := v.(string) + contactGroupID, err := failoverGroupCIDToID(api.CIDType(&cid)) + if err != nil { + return nil, errwrap.Wrapf("error reading contact group CID: {{err}}", err) + } + pagerDutyInfo.FallbackGroupCID = contactGroupID + } + + if v, ok := pagerDutyMap[string(contactPagerDutyIntegrationKeyAttr)]; ok { + pagerDutyInfo.IntegrationKey = v.(string) + } + + if v, ok := pagerDutyMap[string(contactPagerDutyWebhookURLAttr)]; ok { + pagerDutyInfo.WebookURL = v.(string) + } + + js, err := json.Marshal(pagerDutyInfo) + if err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("error marshalling %s JSON config string: {{err}}", contactPagerDutyAttr), err) + } + + cg.Contacts.External = append(cg.Contacts.External, api.ContactGroupContactsExternal{ + Info: string(js), + Method: circonusMethodPagerDuty, + }) + } + } + + if v, ok := d.GetOk(contactSlackAttr); ok { + slackListRaw := v.(*schema.Set).List() + for _, slackMapRaw := range slackListRaw { + slackMap := slackMapRaw.(map[string]interface{}) + + slackInfo := contactSlackInfo{} + + var buttons int + if v, ok := slackMap[contactSlackButtonsAttr]; ok { + if v.(bool) { + buttons = 1 + } + slackInfo.Buttons = buttons + } + + if v, ok := slackMap[contactSlackChannelAttr]; ok { + slackInfo.Channel = v.(string) + } + + if v, ok := slackMap[contactContactGroupFallbackAttr]; ok && v.(string) != "" { + cid := v.(string) + contactGroupID, err := failoverGroupCIDToID(api.CIDType(&cid)) + if err != nil { + return nil, errwrap.Wrapf("error reading contact group CID: {{err}}", err) + } + slackInfo.FallbackGroupCID = contactGroupID + } + + if v, ok := slackMap[contactSlackTeamAttr]; ok { + slackInfo.Team = v.(string) + } + + if v, ok := slackMap[contactSlackUsernameAttr]; ok { + slackInfo.Username = v.(string) + } + + js, err := json.Marshal(slackInfo) + if err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("error marshalling %s JSON config string: {{err}}", contactSlackAttr), err) + } + + cg.Contacts.External = append(cg.Contacts.External, api.ContactGroupContactsExternal{ + Info: string(js), + Method: circonusMethodSlack, + }) + } + } + + if v, ok := d.GetOk(contactSMSAttr); ok { + smsListRaw := v.(*schema.Set).List() + for _, smsMapRaw := range smsListRaw { + smsMap := smsMapRaw.(map[string]interface{}) + + var requiredAttrFound bool + if v, ok := smsMap[contactSMSAddressAttr]; ok && v.(string) != "" { + requiredAttrFound = true + cg.Contacts.External = append(cg.Contacts.External, api.ContactGroupContactsExternal{ + Info: v.(string), + Method: circonusMethodSMS, + }) + } + + if v, ok := smsMap[contactUserCIDAttr]; ok && v.(string) != "" { + requiredAttrFound = true + cg.Contacts.Users = append(cg.Contacts.Users, api.ContactGroupContactsUser{ + Method: circonusMethodSMS, + UserCID: v.(string), + }) + } + + // Can't mark two attributes that are conflicting as required so we do our + // own validation check here. + if !requiredAttrFound { + return nil, fmt.Errorf("In type %s, either %s or %s must be specified", contactEmailAttr, contactEmailAddressAttr, contactUserCIDAttr) + } + } + } + + if v, ok := d.GetOk(contactVictorOpsAttr); ok { + victorOpsListRaw := v.(*schema.Set).List() + for _, victorOpsMapRaw := range victorOpsListRaw { + victorOpsMap := victorOpsMapRaw.(map[string]interface{}) + + victorOpsInfo := contactVictorOpsInfo{} + + if v, ok := victorOpsMap[contactContactGroupFallbackAttr]; ok && v.(string) != "" { + cid := v.(string) + contactGroupID, err := failoverGroupCIDToID(api.CIDType(&cid)) + if err != nil { + return nil, errwrap.Wrapf("error reading contact group CID: {{err}}", err) + } + victorOpsInfo.FallbackGroupCID = contactGroupID + } + + if v, ok := victorOpsMap[contactVictorOpsAPIKeyAttr]; ok { + victorOpsInfo.APIKey = v.(string) + } + + if v, ok := victorOpsMap[contactVictorOpsCriticalAttr]; ok { + victorOpsInfo.Critical = v.(int) + } + + if v, ok := victorOpsMap[contactVictorOpsInfoAttr]; ok { + victorOpsInfo.Info = v.(int) + } + + if v, ok := victorOpsMap[contactVictorOpsTeamAttr]; ok { + victorOpsInfo.Team = v.(string) + } + + if v, ok := victorOpsMap[contactVictorOpsWarningAttr]; ok { + victorOpsInfo.Warning = v.(int) + } + + js, err := json.Marshal(victorOpsInfo) + if err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("error marshalling %s JSON config string: {{err}}", contactVictorOpsAttr), err) + } + + cg.Contacts.External = append(cg.Contacts.External, api.ContactGroupContactsExternal{ + Info: string(js), + Method: circonusMethodVictorOps, + }) + } + } + + if v, ok := d.GetOk(contactXMPPAttr); ok { + xmppListRaw := v.(*schema.Set).List() + for _, xmppMapRaw := range xmppListRaw { + xmppMap := xmppMapRaw.(map[string]interface{}) + + if v, ok := xmppMap[contactXMPPAddressAttr]; ok && v.(string) != "" { + cg.Contacts.External = append(cg.Contacts.External, api.ContactGroupContactsExternal{ + Info: v.(string), + Method: circonusMethodXMPP, + }) + } + + if v, ok := xmppMap[contactUserCIDAttr]; ok && v.(string) != "" { + cg.Contacts.Users = append(cg.Contacts.Users, api.ContactGroupContactsUser{ + Method: circonusMethodXMPP, + UserCID: v.(string), + }) + } + } + } + + if v, ok := d.GetOk(contactLongMessageAttr); ok { + msg := v.(string) + cg.AlertFormats.LongMessage = &msg + } + + if v, ok := d.GetOk(contactLongSubjectAttr); ok { + msg := v.(string) + cg.AlertFormats.LongSubject = &msg + } + + if v, ok := d.GetOk(contactLongSummaryAttr); ok { + msg := v.(string) + cg.AlertFormats.LongSummary = &msg + } + + if v, ok := d.GetOk(contactShortMessageAttr); ok { + msg := v.(string) + cg.AlertFormats.ShortMessage = &msg + } + + if v, ok := d.GetOk(contactShortSummaryAttr); ok { + msg := v.(string) + cg.AlertFormats.ShortSummary = &msg + } + + if v, ok := d.GetOk(contactShortMessageAttr); ok { + msg := v.(string) + cg.AlertFormats.ShortMessage = &msg + } + + if v, found := d.GetOk(checkTagsAttr); found { + cg.Tags = derefStringList(flattenSet(v.(*schema.Set))) + } + + if err := validateContactGroup(cg); err != nil { + return nil, err + } + + return cg, nil +} + +func contactGroupIRCToState(cg *api.ContactGroup) []interface{} { + ircContacts := make([]interface{}, 0, len(cg.Contacts.Users)) + + for _, user := range cg.Contacts.Users { + switch user.Method { + case contactIRCAttr: + ircContacts = append(ircContacts, map[string]interface{}{ + contactUserCIDAttr: user.UserCID, + }) + } + } + + return ircContacts +} + +func contactGroupPagerDutyToState(cg *api.ContactGroup) ([]interface{}, error) { + pdContacts := make([]interface{}, 0, len(cg.Contacts.External)) + + for _, ext := range cg.Contacts.External { + switch ext.Method { + case contactPagerDutyAttr: + pdInfo := contactPagerDutyInfo{} + if err := json.Unmarshal([]byte(ext.Info), &pdInfo); err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("unable to decode external %s JSON (%q): {{err}}", contactPagerDutyAttr, ext.Info), err) + } + + pdContacts = append(pdContacts, map[string]interface{}{ + string(contactContactGroupFallbackAttr): failoverGroupIDToCID(pdInfo.FallbackGroupCID), + string(contactPagerDutyIntegrationKeyAttr): pdInfo.IntegrationKey, + string(contactPagerDutyWebhookURLAttr): pdInfo.WebookURL, + }) + } + } + + return pdContacts, nil +} + +func contactGroupSlackToState(cg *api.ContactGroup) ([]interface{}, error) { + slackContacts := make([]interface{}, 0, len(cg.Contacts.External)) + + for _, ext := range cg.Contacts.External { + switch ext.Method { + case contactSlackAttr: + slackInfo := contactSlackInfo{} + if err := json.Unmarshal([]byte(ext.Info), &slackInfo); err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("unable to decode external %s JSON (%q): {{err}}", contactSlackAttr, ext.Info), err) + } + + slackContacts = append(slackContacts, map[string]interface{}{ + contactContactGroupFallbackAttr: failoverGroupIDToCID(slackInfo.FallbackGroupCID), + contactSlackButtonsAttr: int(slackInfo.Buttons) == int(1), + contactSlackChannelAttr: slackInfo.Channel, + contactSlackTeamAttr: slackInfo.Team, + contactSlackUsernameAttr: slackInfo.Username, + }) + } + } + + return slackContacts, nil +} + +func contactGroupSMSToState(cg *api.ContactGroup) ([]interface{}, error) { + smsContacts := make([]interface{}, 0, len(cg.Contacts.Users)+len(cg.Contacts.External)) + + for _, ext := range cg.Contacts.External { + switch ext.Method { + case contactSMSAttr: + smsContacts = append(smsContacts, map[string]interface{}{ + contactSMSAddressAttr: ext.Info, + }) + } + } + + for _, user := range cg.Contacts.Users { + switch user.Method { + case contactSMSAttr: + smsContacts = append(smsContacts, map[string]interface{}{ + contactUserCIDAttr: user.UserCID, + }) + } + } + + return smsContacts, nil +} + +func contactGroupVictorOpsToState(cg *api.ContactGroup) ([]interface{}, error) { + victorOpsContacts := make([]interface{}, 0, len(cg.Contacts.External)) + + for _, ext := range cg.Contacts.External { + switch ext.Method { + case contactVictorOpsAttr: + victorOpsInfo := contactVictorOpsInfo{} + if err := json.Unmarshal([]byte(ext.Info), &victorOpsInfo); err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("unable to decode external %s JSON (%q): {{err}}", contactVictorOpsInfoAttr, ext.Info), err) + } + + victorOpsContacts = append(victorOpsContacts, map[string]interface{}{ + contactContactGroupFallbackAttr: failoverGroupIDToCID(victorOpsInfo.FallbackGroupCID), + contactVictorOpsAPIKeyAttr: victorOpsInfo.APIKey, + contactVictorOpsCriticalAttr: victorOpsInfo.Critical, + contactVictorOpsInfoAttr: victorOpsInfo.Info, + contactVictorOpsTeamAttr: victorOpsInfo.Team, + contactVictorOpsWarningAttr: victorOpsInfo.Warning, + }) + } + } + + return victorOpsContacts, nil +} + +func contactGroupXMPPToState(cg *api.ContactGroup) ([]interface{}, error) { + xmppContacts := make([]interface{}, 0, len(cg.Contacts.Users)+len(cg.Contacts.External)) + + for _, ext := range cg.Contacts.External { + switch ext.Method { + case contactXMPPAttr: + xmppContacts = append(xmppContacts, map[string]interface{}{ + contactXMPPAddressAttr: ext.Info, + }) + } + } + + for _, user := range cg.Contacts.Users { + switch user.Method { + case contactXMPPAttr: + xmppContacts = append(xmppContacts, map[string]interface{}{ + contactUserCIDAttr: user.UserCID, + }) + } + } + + return xmppContacts, nil +} + +// contactGroupAlertOptionsChecksum creates a stable hash of the normalized values +func contactGroupAlertOptionsChecksum(v interface{}) int { + m := v.(map[string]interface{}) + b := &bytes.Buffer{} + b.Grow(defaultHashBufSize) + fmt.Fprintf(b, "%x", m[contactSeverityAttr].(int)) + fmt.Fprint(b, normalizeTimeDurationStringToSeconds(m[contactEscalateAfterAttr])) + fmt.Fprint(b, m[contactEscalateToAttr]) + fmt.Fprint(b, normalizeTimeDurationStringToSeconds(m[contactReminderAttr])) + return hashcode.String(b.String()) +} diff --git a/builtin/providers/circonus/resource_circonus_contact_test.go b/builtin/providers/circonus/resource_circonus_contact_test.go new file mode 100644 index 000000000..ba5035789 --- /dev/null +++ b/builtin/providers/circonus/resource_circonus_contact_test.go @@ -0,0 +1,241 @@ +package circonus + +import ( + "fmt" + "strings" + "testing" + + "github.com/circonus-labs/circonus-gometrics/api" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccCirconusContactGroup_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDestroyCirconusContactGroup, + Steps: []resource.TestStep{ + { + Config: testAccCirconusContactGroupConfig, + Check: resource.ComposeTestCheckFunc( + // testAccContactGroupExists("circonus_contact_group.staging-sev3", "foo"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "name", "ops-staging-sev3"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "email.#", "3"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "email.1119127802.address", ""), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "email.1119127802.user", "/user/5469"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "email.1456570992.address", ""), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "email.1456570992.user", "/user/6331"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "email.343263208.address", "user@example.com"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "email.343263208.user", ""), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "http.#", "1"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "http.1287846151.address", "https://www.example.org/post/endpoint"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "http.1287846151.format", "json"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "http.1287846151.method", "POST"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "irc.#", "0"), + // resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "irc.918937268.user", "/user/6331"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "slack.#", "1"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "slack.274933206.channel", "#ops-staging"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "slack.274933206.team", "T123UT98F"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "slack.274933206.username", "Circonus"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "slack.274933206.buttons", "true"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "sms.#", "1"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "sms.1119127802.user", "/user/5469"), + + // xmpp.# will be 0 for user faux user accounts that don't have an + // XMPP address setup. + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "xmpp.#", "0"), + // resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "xmpp.1119127802.user", "/user/5469"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "victorops.#", "1"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "victorops.2029434450.api_key", "123"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "victorops.2029434450.critical", "2"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "victorops.2029434450.info", "5"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "victorops.2029434450.team", "bender"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "victorops.2029434450.warning", "3"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "aggregation_window", "60s"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.#", "5"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.689365425.severity", "1"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.689365425.reminder", "60s"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.689365425.escalate_after", "3600s"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.689365425.escalate_to", "/contact_group/2913"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.551050940.severity", "2"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.551050940.reminder", "120s"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.551050940.escalate_after", "7200s"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.551050940.escalate_to", "/contact_group/2913"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.1292974544.severity", "3"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.1292974544.reminder", "180s"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.1292974544.escalate_after", "10800s"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.1292974544.escalate_to", "/contact_group/2913"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.1183354841.severity", "4"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.1183354841.reminder", "240s"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.1183354841.escalate_after", "14400s"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.1183354841.escalate_to", "/contact_group/2913"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.2942620849.severity", "5"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.2942620849.reminder", "300s"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.2942620849.escalate_after", "18000s"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "alert_option.2942620849.escalate_to", "/contact_group/2913"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "long_message", "a long message"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "long_subject", "long subject"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "long_summary", "long summary"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "short_message", "short message"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "short_summary", "short summary"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "tags.#", "2"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "tags.2087084518", "author:terraform"), + resource.TestCheckResourceAttr("circonus_contact_group.staging-sev3", "tags.393923453", "other:foo"), + ), + }, + }, + }) +} + +func testAccCheckDestroyCirconusContactGroup(s *terraform.State) error { + c := testAccProvider.Meta().(*providerContext) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "circonus_contact_group" { + continue + } + + cid := rs.Primary.ID + exists, err := checkContactGroupExists(c, api.CIDType(&cid)) + switch { + case !exists: + // noop + case exists: + return fmt.Errorf("contact group still exists after destroy") + case err != nil: + return fmt.Errorf("Error checking contact group %s", err) + } + } + + return nil +} + +func checkContactGroupExists(c *providerContext, contactGroupCID api.CIDType) (bool, error) { + cb, err := c.client.FetchContactGroup(contactGroupCID) + if err != nil { + if strings.Contains(err.Error(), defaultCirconus404ErrorString) { + return false, nil + } + + return false, err + } + + if api.CIDType(&cb.CID) == contactGroupCID { + return true, nil + } + + return false, nil +} + +const testAccCirconusContactGroupConfig = ` +resource "circonus_contact_group" "staging-sev3" { + name = "ops-staging-sev3" + + email { + user = "/user/5469" + } + + email { + address = "user@example.com" + } + + email { + user = "/user/6331" + } + + http { + address = "https://www.example.org/post/endpoint" + format = "json" + method = "POST" + } + +/* + // Account needs to be setup with IRC before this can work. + irc { + user = "/user/6331" + } +*/ + +/* + pagerduty { + // NOTE(sean@): needs to be filled in + } +*/ + + slack { + channel = "#ops-staging" + team = "T123UT98F" + username = "Circonus" + buttons = true + } + + sms { + user = "/user/5469" + } + + victorops { + api_key = "123" + critical = 2 + info = 5 + team = "bender" + warning = 3 + } + + // Faux user accounts that don't have an XMPP address setup will not return a + // valid response in the future. + // + // xmpp { + // user = "/user/5469" + // } + + aggregation_window = "1m" + + alert_option { + severity = 1 + reminder = "60s" + escalate_after = "3600s" + escalate_to = "/contact_group/2913" + } + + alert_option { + severity = 2 + reminder = "2m" + escalate_after = "2h" + escalate_to = "/contact_group/2913" + } + + alert_option { + severity = 3 + reminder = "3m" + escalate_after = "3h" + escalate_to = "/contact_group/2913" + } + + alert_option { + severity = 4 + reminder = "4m" + escalate_after = "4h" + escalate_to = "/contact_group/2913" + } + + alert_option { + severity = 5 + reminder = "5m" + escalate_after = "5h" + escalate_to = "/contact_group/2913" + } + + // alert_formats: omit to use defaults + long_message = "a long message" + long_subject = "long subject" + long_summary = "long summary" + short_message = "short message" + short_summary = "short summary" + + tags = [ + "author:terraform", + "other:foo", + ] +} +` diff --git a/builtin/providers/circonus/resource_circonus_graph.go b/builtin/providers/circonus/resource_circonus_graph.go new file mode 100644 index 000000000..836e42263 --- /dev/null +++ b/builtin/providers/circonus/resource_circonus_graph.go @@ -0,0 +1,930 @@ +package circonus + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "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_graph.* resource attribute names + graphDescriptionAttr = "description" + graphLeftAttr = "left" + graphLineStyleAttr = "line_style" + graphMetricClusterAttr = "metric_cluster" + graphNameAttr = "name" + graphNotesAttr = "notes" + graphRightAttr = "right" + graphMetricAttr = "metric" + graphStyleAttr = "graph_style" + graphTagsAttr = "tags" + + // circonus_graph.metric.* resource attribute names + graphMetricActiveAttr = "active" + graphMetricAlphaAttr = "alpha" + graphMetricAxisAttr = "axis" + graphMetricCAQLAttr = "caql" + graphMetricCheckAttr = "check" + graphMetricColorAttr = "color" + graphMetricFormulaAttr = "formula" + graphMetricFormulaLegendAttr = "legend_formula" + graphMetricFunctionAttr = "function" + graphMetricHumanNameAttr = "name" + graphMetricMetricTypeAttr = "metric_type" + graphMetricNameAttr = "metric_name" + graphMetricStackAttr = "stack" + + // circonus_graph.metric_cluster.* resource attribute names + graphMetricClusterActiveAttr = "active" + graphMetricClusterAggregateAttr = "aggregate" + graphMetricClusterAxisAttr = "axis" + graphMetricClusterColorAttr = "color" + graphMetricClusterQueryAttr = "query" + graphMetricClusterHumanNameAttr = "name" + + // circonus_graph.{left,right}.* resource attribute names + graphAxisLogarithmicAttr = "logarithmic" + graphAxisMaxAttr = "max" + graphAxisMinAttr = "min" +) + +const ( + apiGraphStyleLine = "line" +) + +var graphDescriptions = attrDescrs{ + // circonus_graph.* resource attribute names + graphDescriptionAttr: "", + graphLeftAttr: "", + graphLineStyleAttr: "How the line should change between point. A string containing either 'stepped', 'interpolated' or null.", + graphNameAttr: "", + graphNotesAttr: "", + graphRightAttr: "", + graphMetricAttr: "", + graphMetricClusterAttr: "", + graphStyleAttr: "", + graphTagsAttr: "", +} + +var graphMetricDescriptions = attrDescrs{ + // circonus_graph.metric.* resource attribute names + graphMetricActiveAttr: "", + graphMetricAlphaAttr: "", + graphMetricAxisAttr: "", + graphMetricCAQLAttr: "", + graphMetricCheckAttr: "", + graphMetricColorAttr: "", + graphMetricFormulaAttr: "", + graphMetricFormulaLegendAttr: "", + graphMetricFunctionAttr: "", + graphMetricMetricTypeAttr: "", + graphMetricHumanNameAttr: "", + graphMetricNameAttr: "", + graphMetricStackAttr: "", +} + +var graphMetricClusterDescriptions = attrDescrs{ + // circonus_graph.metric_cluster.* resource attribute names + graphMetricClusterActiveAttr: "", + graphMetricClusterAggregateAttr: "", + graphMetricClusterAxisAttr: "", + graphMetricClusterColorAttr: "", + graphMetricClusterQueryAttr: "", + graphMetricClusterHumanNameAttr: "", +} + +// NOTE(sean@): There is no way to set a description on map inputs, but if that +// does happen: +// +// var graphMetricAxisOptionDescriptions = attrDescrs{ +// // circonus_graph.if.value.over.* resource attribute names +// graphAxisLogarithmicAttr: "", +// graphAxisMaxAttr: "", +// graphAxisMinAttr: "", +// } + +func resourceGraph() *schema.Resource { + makeConflictsWith := func(in ...schemaAttr) []string { + out := make([]string, 0, len(in)) + for _, attr := range in { + out = append(out, string(graphMetricAttr)+"."+string(attr)) + } + return out + } + + return &schema.Resource{ + Create: graphCreate, + Read: graphRead, + Update: graphUpdate, + Delete: graphDelete, + Exists: graphExists, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: convertToHelperSchema(graphDescriptions, map[schemaAttr]*schema.Schema{ + graphDescriptionAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + StateFunc: suppressWhitespace, + }, + graphLeftAttr: &schema.Schema{ + Type: schema.TypeMap, + Elem: schema.TypeString, + Optional: true, + ValidateFunc: validateGraphAxisOptions, + }, + graphLineStyleAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: defaultGraphLineStyle, + ValidateFunc: validateStringIn(graphLineStyleAttr, validGraphLineStyles), + }, + graphNameAttr: &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateRegexp(graphNameAttr, `.+`), + }, + graphNotesAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + graphRightAttr: &schema.Schema{ + Type: schema.TypeMap, + Elem: schema.TypeString, + Optional: true, + ValidateFunc: validateGraphAxisOptions, + }, + graphMetricAttr: &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MinItems: 1, + Elem: &schema.Resource{ + Schema: convertToHelperSchema(graphMetricDescriptions, map[schemaAttr]*schema.Schema{ + graphMetricActiveAttr: &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + graphMetricAlphaAttr: &schema.Schema{ + Type: schema.TypeFloat, + Optional: true, + ValidateFunc: validateFuncs( + validateFloatMin(graphMetricAlphaAttr, 0.0), + validateFloatMax(graphMetricAlphaAttr, 1.0), + ), + }, + graphMetricAxisAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "left", + ValidateFunc: validateStringIn(graphMetricAxisAttr, validAxisAttrs), + }, + graphMetricCAQLAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(graphMetricCAQLAttr, `.+`), + ConflictsWith: makeConflictsWith(graphMetricCheckAttr, graphMetricNameAttr), + }, + graphMetricCheckAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(graphMetricCheckAttr, config.CheckCIDRegex), + ConflictsWith: makeConflictsWith(graphMetricCAQLAttr), + }, + graphMetricColorAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(graphMetricColorAttr, `^#[0-9a-fA-F]{6}$`), + }, + graphMetricFormulaAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(graphMetricFormulaAttr, `^.+$`), + }, + graphMetricFormulaLegendAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(graphMetricFormulaLegendAttr, `^.+$`), + }, + graphMetricFunctionAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: defaultGraphFunction, + ValidateFunc: validateStringIn(graphMetricFunctionAttr, validGraphFunctionValues), + }, + graphMetricMetricTypeAttr: &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateStringIn(graphMetricMetricTypeAttr, validMetricTypes), + }, + graphMetricHumanNameAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(graphMetricHumanNameAttr, `.+`), + }, + graphMetricNameAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(graphMetricNameAttr, `^[\S]+$`), + }, + graphMetricStackAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(graphMetricStackAttr, `^[\d]*$`), + }, + }), + }, + }, + graphMetricClusterAttr: &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MinItems: 1, + Elem: &schema.Resource{ + Schema: convertToHelperSchema(graphMetricClusterDescriptions, map[schemaAttr]*schema.Schema{ + graphMetricClusterActiveAttr: &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + graphMetricClusterAggregateAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "none", + ValidateFunc: validateStringIn(graphMetricClusterAggregateAttr, validAggregateFuncs), + }, + graphMetricClusterAxisAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "left", + ValidateFunc: validateStringIn(graphMetricClusterAttr, validAxisAttrs), + }, + graphMetricClusterColorAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(graphMetricClusterColorAttr, `^#[0-9a-fA-F]{6}$`), + }, + graphMetricClusterQueryAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRegexp(graphMetricClusterQueryAttr, config.MetricClusterCIDRegex), + }, + graphMetricClusterHumanNameAttr: &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateRegexp(graphMetricHumanNameAttr, `.+`), + }, + }), + }, + }, + graphStyleAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: defaultGraphStyle, + ValidateFunc: validateStringIn(graphStyleAttr, validGraphStyles), + }, + graphTagsAttr: tagMakeConfigSchema(graphTagsAttr), + }), + } +} + +func graphCreate(d *schema.ResourceData, meta interface{}) error { + ctxt := meta.(*providerContext) + g := newGraph() + if err := g.ParseConfig(d); err != nil { + return errwrap.Wrapf("error parsing graph schema during create: {{err}}", err) + } + + if err := g.Create(ctxt); err != nil { + return errwrap.Wrapf("error creating graph: {{err}}", err) + } + + d.SetId(g.CID) + + return graphRead(d, meta) +} + +func graphExists(d *schema.ResourceData, meta interface{}) (bool, error) { + ctxt := meta.(*providerContext) + + cid := d.Id() + g, err := ctxt.client.FetchGraph(api.CIDType(&cid)) + if err != nil { + if strings.Contains(err.Error(), defaultCirconus404ErrorString) { + return false, nil + } + + return false, err + } + + if g.CID == "" { + return false, nil + } + + return true, nil +} + +// graphRead pulls data out of the Graph object and stores it into the +// appropriate place in the statefile. +func graphRead(d *schema.ResourceData, meta interface{}) error { + ctxt := meta.(*providerContext) + + cid := d.Id() + g, err := loadGraph(ctxt, api.CIDType(&cid)) + if err != nil { + return err + } + + d.SetId(g.CID) + + metrics := make([]interface{}, 0, len(g.Datapoints)) + for _, datapoint := range g.Datapoints { + dataPointAttrs := make(map[string]interface{}, 13) // 13 == len(members in api.GraphDatapoint) + + dataPointAttrs[string(graphMetricActiveAttr)] = !datapoint.Hidden + + if datapoint.Alpha != nil && *datapoint.Alpha != 0 { + dataPointAttrs[string(graphMetricAlphaAttr)] = *datapoint.Alpha + } + + switch datapoint.Axis { + case "l", "": + dataPointAttrs[string(graphMetricAxisAttr)] = "left" + case "r": + dataPointAttrs[string(graphMetricAxisAttr)] = "right" + default: + return fmt.Errorf("PROVIDER BUG: Unsupported axis type %q", datapoint.Axis) + } + + if datapoint.CAQL != nil { + dataPointAttrs[string(graphMetricCAQLAttr)] = *datapoint.CAQL + } + + if datapoint.CheckID != 0 { + dataPointAttrs[string(graphMetricCheckAttr)] = fmt.Sprintf("%s/%d", config.CheckPrefix, datapoint.CheckID) + } + + if datapoint.Color != nil { + dataPointAttrs[string(graphMetricColorAttr)] = *datapoint.Color + } + + if datapoint.DataFormula != nil { + dataPointAttrs[string(graphMetricFormulaAttr)] = *datapoint.DataFormula + } + + switch datapoint.Derive.(type) { + case bool: + case string: + dataPointAttrs[string(graphMetricFunctionAttr)] = datapoint.Derive.(string) + default: + return fmt.Errorf("PROVIDER BUG: Unsupported type for derive: %T", datapoint.Derive) + } + + if datapoint.LegendFormula != nil { + dataPointAttrs[string(graphMetricFormulaLegendAttr)] = *datapoint.LegendFormula + } + + if datapoint.MetricName != "" { + dataPointAttrs[string(graphMetricNameAttr)] = datapoint.MetricName + } + + if datapoint.MetricType != "" { + dataPointAttrs[string(graphMetricMetricTypeAttr)] = datapoint.MetricType + } + + if datapoint.Name != "" { + dataPointAttrs[string(graphMetricHumanNameAttr)] = datapoint.Name + } + + if datapoint.Stack != nil { + dataPointAttrs[string(graphMetricStackAttr)] = fmt.Sprintf("%d", *datapoint.Stack) + } + + metrics = append(metrics, dataPointAttrs) + } + + metricClusters := make([]interface{}, 0, len(g.MetricClusters)) + for _, metricCluster := range g.MetricClusters { + metricClusterAttrs := make(map[string]interface{}, 8) // 8 == len(num struct attrs in api.GraphMetricCluster) + + metricClusterAttrs[string(graphMetricClusterActiveAttr)] = !metricCluster.Hidden + + if metricCluster.AggregateFunc != "" { + metricClusterAttrs[string(graphMetricClusterAggregateAttr)] = metricCluster.AggregateFunc + } + + switch metricCluster.Axis { + case "l", "": + metricClusterAttrs[string(graphMetricClusterAxisAttr)] = "left" + case "r": + metricClusterAttrs[string(graphMetricClusterAxisAttr)] = "right" + default: + return fmt.Errorf("PROVIDER BUG: Unsupported axis type %q", metricCluster.Axis) + } + + if metricCluster.Color != nil { + metricClusterAttrs[string(graphMetricClusterColorAttr)] = *metricCluster.Color + } + + if metricCluster.DataFormula != nil { + metricClusterAttrs[string(graphMetricFormulaAttr)] = *metricCluster.DataFormula + } + + if metricCluster.LegendFormula != nil { + metricClusterAttrs[string(graphMetricFormulaLegendAttr)] = *metricCluster.LegendFormula + } + + if metricCluster.MetricCluster != "" { + metricClusterAttrs[string(graphMetricClusterQueryAttr)] = metricCluster.MetricCluster + } + + if metricCluster.Name != "" { + metricClusterAttrs[string(graphMetricHumanNameAttr)] = metricCluster.Name + } + + if metricCluster.Stack != nil { + metricClusterAttrs[string(graphMetricStackAttr)] = fmt.Sprintf("%d", *metricCluster.Stack) + } + + metricClusters = append(metricClusters, metricClusterAttrs) + } + + leftAxisMap := make(map[string]interface{}, 3) + if g.LogLeftY != nil { + leftAxisMap[string(graphAxisLogarithmicAttr)] = fmt.Sprintf("%d", *g.LogLeftY) + } + + if g.MaxLeftY != nil { + leftAxisMap[string(graphAxisMaxAttr)] = strconv.FormatFloat(*g.MaxLeftY, 'f', -1, 64) + } + + if g.MinLeftY != nil { + leftAxisMap[string(graphAxisMinAttr)] = strconv.FormatFloat(*g.MinLeftY, 'f', -1, 64) + } + + rightAxisMap := make(map[string]interface{}, 3) + if g.LogRightY != nil { + rightAxisMap[string(graphAxisLogarithmicAttr)] = fmt.Sprintf("%d", *g.LogRightY) + } + + if g.MaxRightY != nil { + rightAxisMap[string(graphAxisMaxAttr)] = strconv.FormatFloat(*g.MaxRightY, 'f', -1, 64) + } + + if g.MinRightY != nil { + rightAxisMap[string(graphAxisMinAttr)] = strconv.FormatFloat(*g.MinRightY, 'f', -1, 64) + } + + d.Set(graphDescriptionAttr, g.Description) + + if err := d.Set(graphLeftAttr, leftAxisMap); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to store graph %q attribute: {{err}}", graphLeftAttr), err) + } + + d.Set(graphLineStyleAttr, g.LineStyle) + d.Set(graphNameAttr, g.Title) + d.Set(graphNotesAttr, indirect(g.Notes)) + + if err := d.Set(graphRightAttr, rightAxisMap); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to store graph %q attribute: {{err}}", graphRightAttr), err) + } + + if err := d.Set(graphMetricAttr, metrics); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to store graph %q attribute: {{err}}", graphMetricAttr), err) + } + + if err := d.Set(graphMetricClusterAttr, metricClusters); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to store graph %q attribute: {{err}}", graphMetricClusterAttr), err) + } + + d.Set(graphStyleAttr, g.Style) + + if err := d.Set(graphTagsAttr, tagsToState(apiToTags(g.Tags))); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to store graph %q attribute: {{err}}", graphTagsAttr), err) + } + + return nil +} + +func graphUpdate(d *schema.ResourceData, meta interface{}) error { + ctxt := meta.(*providerContext) + g := newGraph() + if err := g.ParseConfig(d); err != nil { + return err + } + + g.CID = d.Id() + if err := g.Update(ctxt); err != nil { + return errwrap.Wrapf(fmt.Sprintf("unable to update graph %q: {{err}}", d.Id()), err) + } + + return graphRead(d, meta) +} + +func graphDelete(d *schema.ResourceData, meta interface{}) error { + ctxt := meta.(*providerContext) + + cid := d.Id() + if _, err := ctxt.client.DeleteGraphByCID(api.CIDType(&cid)); err != nil { + return errwrap.Wrapf(fmt.Sprintf("unable to delete graph %q: {{err}}", d.Id()), err) + } + + d.SetId("") + + return nil +} + +type circonusGraph struct { + api.Graph +} + +func newGraph() circonusGraph { + g := circonusGraph{ + Graph: *api.NewGraph(), + } + + return g +} + +func loadGraph(ctxt *providerContext, cid api.CIDType) (circonusGraph, error) { + var g circonusGraph + ng, err := ctxt.client.FetchGraph(cid) + if err != nil { + return circonusGraph{}, err + } + g.Graph = *ng + + return g, nil +} + +// ParseConfig reads Terraform config data and stores the information into a +// Circonus Graph object. ParseConfig and graphRead() must be kept in sync. +func (g *circonusGraph) ParseConfig(d *schema.ResourceData) error { + g.Datapoints = make([]api.GraphDatapoint, 0, defaultGraphDatapoints) + + if v, found := d.GetOk(graphLeftAttr); found { + listRaw := v.(map[string]interface{}) + leftAxisMap := make(map[string]interface{}, len(listRaw)) + for k, v := range listRaw { + leftAxisMap[k] = v + } + + if v, ok := leftAxisMap[string(graphAxisLogarithmicAttr)]; ok { + i64, _ := strconv.ParseInt(v.(string), 10, 64) + i := int(i64) + g.LogLeftY = &i + } + + if v, ok := leftAxisMap[string(graphAxisMaxAttr)]; ok && v.(string) != "" { + f, _ := strconv.ParseFloat(v.(string), 64) + g.MaxLeftY = &f + } + + if v, ok := leftAxisMap[string(graphAxisMinAttr)]; ok && v.(string) != "" { + f, _ := strconv.ParseFloat(v.(string), 64) + g.MinLeftY = &f + } + } + + if v, found := d.GetOk(graphRightAttr); found { + listRaw := v.(map[string]interface{}) + rightAxisMap := make(map[string]interface{}, len(listRaw)) + for k, v := range listRaw { + rightAxisMap[k] = v + } + + if v, ok := rightAxisMap[string(graphAxisLogarithmicAttr)]; ok { + i64, _ := strconv.ParseInt(v.(string), 10, 64) + i := int(i64) + g.LogRightY = &i + } + + if v, ok := rightAxisMap[string(graphAxisMaxAttr)]; ok && v.(string) != "" { + f, _ := strconv.ParseFloat(v.(string), 64) + g.MaxRightY = &f + } + + if v, ok := rightAxisMap[string(graphAxisMinAttr)]; ok && v.(string) != "" { + f, _ := strconv.ParseFloat(v.(string), 64) + g.MinRightY = &f + } + } + + if v, found := d.GetOk(graphDescriptionAttr); found { + g.Description = v.(string) + } + + if v, found := d.GetOk(graphLineStyleAttr); found { + switch v.(type) { + case string: + s := v.(string) + g.LineStyle = &s + case *string: + g.LineStyle = v.(*string) + default: + return fmt.Errorf("PROVIDER BUG: unsupported type for %q: %T", graphLineStyleAttr, v) + } + } + + if v, found := d.GetOk(graphNameAttr); found { + g.Title = v.(string) + } + + if v, found := d.GetOk(graphNotesAttr); found { + s := v.(string) + g.Notes = &s + } + + if listRaw, found := d.GetOk(graphMetricAttr); found { + metricList := listRaw.([]interface{}) + for _, metricListElem := range metricList { + metricAttrs := newInterfaceMap(metricListElem.(map[string]interface{})) + datapoint := api.GraphDatapoint{} + + if v, found := metricAttrs[graphMetricActiveAttr]; found { + datapoint.Hidden = !(v.(bool)) + } + + if v, found := metricAttrs[graphMetricAlphaAttr]; found { + f := v.(float64) + if f != 0 { + datapoint.Alpha = &f + } + } + + if v, found := metricAttrs[graphMetricAxisAttr]; found { + switch v.(string) { + case "left", "": + datapoint.Axis = "l" + case "right": + datapoint.Axis = "r" + default: + return fmt.Errorf("PROVIDER BUG: Unsupported axis attribute %q: %q", graphMetricAxisAttr, v.(string)) + } + } + + if v, found := metricAttrs[graphMetricCheckAttr]; found { + re := regexp.MustCompile(config.CheckCIDRegex) + matches := re.FindStringSubmatch(v.(string)) + if len(matches) == 3 { + checkID, _ := strconv.ParseUint(matches[2], 10, 64) + datapoint.CheckID = uint(checkID) + } + } + + if v, found := metricAttrs[graphMetricColorAttr]; found { + s := v.(string) + datapoint.Color = &s + } + + if v, found := metricAttrs[graphMetricFormulaAttr]; found { + switch v.(type) { + case string: + s := v.(string) + datapoint.DataFormula = &s + case *string: + datapoint.DataFormula = v.(*string) + default: + return fmt.Errorf("PROVIDER BUG: unsupported type for %q: %T", graphMetricAttr, v) + } + } + + if v, found := metricAttrs[graphMetricFunctionAttr]; found { + s := v.(string) + if s != "" { + datapoint.Derive = s + } else { + datapoint.Derive = false + } + } else { + datapoint.Derive = false + } + + if v, found := metricAttrs[graphMetricFormulaLegendAttr]; found { + switch u := v.(type) { + case string: + datapoint.LegendFormula = &u + case *string: + datapoint.LegendFormula = u + default: + return fmt.Errorf("PROVIDER BUG: unsupported type for %q: %T", graphMetricAttr, v) + } + } + + if v, found := metricAttrs[graphMetricNameAttr]; found { + s := v.(string) + if s != "" { + datapoint.MetricName = s + } + } + + if v, found := metricAttrs[graphMetricMetricTypeAttr]; found { + s := v.(string) + if s != "" { + datapoint.MetricType = s + } + } + + if v, found := metricAttrs[graphMetricHumanNameAttr]; found { + s := v.(string) + if s != "" { + datapoint.Name = s + } + } + + if v, found := metricAttrs[graphMetricStackAttr]; found { + var stackStr string + switch u := v.(type) { + case string: + stackStr = u + case *string: + if u != nil { + stackStr = *u + } + default: + return fmt.Errorf("PROVIDER BUG: unsupported type for %q: %T", graphMetricStackAttr, v) + } + + if stackStr != "" { + u64, _ := strconv.ParseUint(stackStr, 10, 64) + u := uint(u64) + datapoint.Stack = &u + } + } + + g.Datapoints = append(g.Datapoints, datapoint) + } + } + + if listRaw, found := d.GetOk(graphMetricClusterAttr); found { + metricClusterList := listRaw.([]interface{}) + + for _, metricClusterListRaw := range metricClusterList { + metricClusterAttrs := newInterfaceMap(metricClusterListRaw.(map[string]interface{})) + + metricCluster := api.GraphMetricCluster{} + + if v, found := metricClusterAttrs[graphMetricClusterActiveAttr]; found { + metricCluster.Hidden = !(v.(bool)) + } + + if v, found := metricClusterAttrs[graphMetricClusterAggregateAttr]; found { + metricCluster.AggregateFunc = v.(string) + } + + if v, found := metricClusterAttrs[graphMetricClusterAxisAttr]; found { + switch v.(string) { + case "left", "": + metricCluster.Axis = "l" + case "right": + metricCluster.Axis = "r" + default: + return fmt.Errorf("PROVIDER BUG: Unsupported axis attribute %q: %q", graphMetricClusterAxisAttr, v.(string)) + } + } + + if v, found := metricClusterAttrs[graphMetricClusterColorAttr]; found { + s := v.(string) + if s != "" { + metricCluster.Color = &s + } + } + + if v, found := metricClusterAttrs[graphMetricFormulaAttr]; found { + switch v.(type) { + case string: + s := v.(string) + metricCluster.DataFormula = &s + case *string: + metricCluster.DataFormula = v.(*string) + default: + return fmt.Errorf("PROVIDER BUG: unsupported type for %q: %T", graphMetricFormulaAttr, v) + } + } + + if v, found := metricClusterAttrs[graphMetricFormulaLegendAttr]; found { + switch v.(type) { + case string: + s := v.(string) + metricCluster.LegendFormula = &s + case *string: + metricCluster.LegendFormula = v.(*string) + default: + return fmt.Errorf("PROVIDER BUG: unsupported type for %q: %T", graphMetricFormulaLegendAttr, v) + } + } + + if v, found := metricClusterAttrs[graphMetricClusterQueryAttr]; found { + s := v.(string) + if s != "" { + metricCluster.MetricCluster = s + } + } + + if v, found := metricClusterAttrs[graphMetricHumanNameAttr]; found { + s := v.(string) + if s != "" { + metricCluster.Name = s + } + } + + if v, found := metricClusterAttrs[graphMetricStackAttr]; found { + var stackStr string + switch u := v.(type) { + case string: + stackStr = u + case *string: + if u != nil { + stackStr = *u + } + default: + return fmt.Errorf("PROVIDER BUG: unsupported type for %q: %T", graphMetricStackAttr, v) + } + + if stackStr != "" { + u64, _ := strconv.ParseUint(stackStr, 10, 64) + u := uint(u64) + metricCluster.Stack = &u + } + } + + g.MetricClusters = append(g.MetricClusters, metricCluster) + } + } + + if v, found := d.GetOk(graphStyleAttr); found { + switch v.(type) { + case string: + s := v.(string) + g.Style = &s + case *string: + g.Style = v.(*string) + default: + return fmt.Errorf("PROVIDER BUG: unsupported type for %q: %T", graphStyleAttr, v) + } + } + + if v, found := d.GetOk(graphTagsAttr); found { + g.Tags = derefStringList(flattenSet(v.(*schema.Set))) + } + + if err := g.Validate(); err != nil { + return err + } + + return nil +} + +func (g *circonusGraph) Create(ctxt *providerContext) error { + ng, err := ctxt.client.CreateGraph(&g.Graph) + if err != nil { + return err + } + + g.CID = ng.CID + + return nil +} + +func (g *circonusGraph) Update(ctxt *providerContext) error { + _, err := ctxt.client.UpdateGraph(&g.Graph) + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to update graph %s: {{err}}", g.CID), err) + } + + return nil +} + +func (g *circonusGraph) Validate() error { + for i, datapoint := range g.Datapoints { + if *g.Style == apiGraphStyleLine && datapoint.Alpha != nil && *datapoint.Alpha != 0 { + return fmt.Errorf("%s can not be set on graphs with style %s", graphMetricAlphaAttr, apiGraphStyleLine) + } + + if datapoint.CheckID != 0 && datapoint.MetricName == "" { + return fmt.Errorf("Error with %s[%d] name=%q: %s is set, missing attribute %s must also be set", graphMetricAttr, i, datapoint.Name, graphMetricCheckAttr, graphMetricNameAttr) + } + + if datapoint.CheckID == 0 && datapoint.MetricName != "" { + return fmt.Errorf("Error with %s[%d] name=%q: %s is set, missing attribute %s must also be set", graphMetricAttr, i, datapoint.Name, graphMetricNameAttr, graphMetricCheckAttr) + } + + if datapoint.CAQL != nil && (datapoint.CheckID != 0 || datapoint.MetricName != "") { + return fmt.Errorf("Error with %s[%d] name=%q: %q attribute is mutually exclusive with attributes %s or %s", graphMetricAttr, i, datapoint.Name, graphMetricCAQLAttr, graphMetricNameAttr, graphMetricCheckAttr) + } + } + + for i, mc := range g.MetricClusters { + if mc.AggregateFunc != "" && (mc.Color == nil || *mc.Color == "") { + return fmt.Errorf("Error with %s[%d] name=%q: %s is a required attribute for graphs with %s set", graphMetricClusterAttr, i, mc.Name, graphMetricClusterColorAttr, graphMetricClusterAggregateAttr) + } + } + + return nil +} diff --git a/builtin/providers/circonus/resource_circonus_graph_test.go b/builtin/providers/circonus/resource_circonus_graph_test.go new file mode 100644 index 000000000..d51d00fc8 --- /dev/null +++ b/builtin/providers/circonus/resource_circonus_graph_test.go @@ -0,0 +1,199 @@ +package circonus + +import ( + "fmt" + "strings" + "testing" + + "github.com/circonus-labs/circonus-gometrics/api" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccCirconusGraph_basic(t *testing.T) { + graphName := fmt.Sprintf("Test Graph - %s", acctest.RandString(5)) + checkName := fmt.Sprintf("ICMP Ping check - %s", acctest.RandString(5)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDestroyCirconusGraph, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccCirconusGraphConfigFmt, checkName, graphName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "name", graphName), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "description", "Terraform Test: mixed graph"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "notes", "test notes"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "graph_style", "line"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "left.%", "1"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "left.max", "11"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "right.%", "3"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "right.logarithmic", "10"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "right.max", "20"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "right.min", "-1"), + + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "line_style", "stepped"), + + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.#", "2"), + + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.0.caql", ""), + resource.TestCheckResourceAttrSet("circonus_graph.mixed-points", "metric.0.check"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.0.metric_name", "maximum"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.0.metric_type", "numeric"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.0.name", "Maximum Latency"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.0.axis", "left"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.0.color", "#657aa6"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.0.function", "gauge"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.0.active", "true"), + + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.1.caql", ""), + resource.TestCheckResourceAttrSet("circonus_graph.mixed-points", "metric.1.check"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.1.metric_name", "minimum"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.1.metric_type", "numeric"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.1.name", "Minimum Latency"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.1.axis", "right"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.1.color", "#657aa6"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.1.function", "gauge"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "metric.1.active", "true"), + + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "tags.#", "2"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "tags.2087084518", "author:terraform"), + resource.TestCheckResourceAttr("circonus_graph.mixed-points", "tags.1401442048", "lifecycle:unittest"), + ), + }, + }, + }) +} + +func testAccCheckDestroyCirconusGraph(s *terraform.State) error { + ctxt := testAccProvider.Meta().(*providerContext) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "circonus_graph" { + continue + } + + cid := rs.Primary.ID + exists, err := checkGraphExists(ctxt, api.CIDType(&cid)) + switch { + case !exists: + // noop + case exists: + return fmt.Errorf("graph still exists after destroy") + case err != nil: + return fmt.Errorf("Error checking graph %s", err) + } + } + + return nil +} + +func checkGraphExists(c *providerContext, graphID api.CIDType) (bool, error) { + g, err := c.client.FetchGraph(graphID) + if err != nil { + if strings.Contains(err.Error(), defaultCirconus404ErrorString) { + return false, nil + } + + return false, err + } + + if api.CIDType(&g.CID) == graphID { + return true, nil + } + + return false, nil +} + +const testAccCirconusGraphConfigFmt = ` +variable "test_tags" { + type = "list" + default = [ "author:terraform", "lifecycle:unittest" ] +} + +resource "circonus_check" "api_latency" { + active = true + name = "%s" + period = "60s" + + collector { + id = "/broker/1" + } + + icmp_ping { + count = 5 + } + + metric { + name = "maximum" + tags = [ "${var.test_tags}" ] + type = "numeric" + unit = "seconds" + } + + metric { + name = "minimum" + tags = [ "${var.test_tags}" ] + type = "numeric" + unit = "seconds" + } + + tags = [ "${var.test_tags}" ] + target = "api.circonus.com" +} + +resource "circonus_graph" "mixed-points" { + name = "%s" + description = "Terraform Test: mixed graph" + notes = "test notes" + graph_style = "line" + line_style = "stepped" + + metric { + # caql = "" # conflicts with metric_name/check + check = "${circonus_check.api_latency.checks[0]}" + metric_name = "maximum" + metric_type = "numeric" + name = "Maximum Latency" + axis = "left" # right + color = "#657aa6" + function = "gauge" + active = true + } + + metric { + # caql = "" # conflicts with metric_name/check + check = "${circonus_check.api_latency.checks[0]}" + metric_name = "minimum" + metric_type = "numeric" + name = "Minimum Latency" + axis = "right" # left + color = "#657aa6" + function = "gauge" + active = true + } + + // metric_cluster { + // active = true + // aggregate = "average" + // axis = "left" # right + // color = "#657aa6" + // group = "${circonus_check.api_latency.checks[0]}" + // name = "Metrics Used" + // } + + left { + max = 11 + } + + right { + logarithmic = 10 + max = 20 + min = -1 + } + + tags = [ "${var.test_tags}" ] +} +` diff --git a/builtin/providers/circonus/resource_circonus_metric.go b/builtin/providers/circonus/resource_circonus_metric.go new file mode 100644 index 000000000..0b9bed1f2 --- /dev/null +++ b/builtin/providers/circonus/resource_circonus_metric.go @@ -0,0 +1,138 @@ +package circonus + +// The `circonus_metric` type is a synthetic, top-level resource that doesn't +// actually exist within Circonus. The `circonus_check` resource uses +// `circonus_metric` as input to its `metric` attribute. The `circonus_check` +// resource can, if configured, override various parameters in the +// `circonus_metric` resource if no value was set (e.g. the `icmp_ping` will +// implicitly set the `unit` metric to `seconds`). + +import ( + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/helper/schema" +) + +const ( + // circonus_metric.* resource attribute names + metricActiveAttr = "active" + metricIDAttr = "id" + metricNameAttr = "name" + metricTypeAttr = "type" + metricTagsAttr = "tags" + metricUnitAttr = "unit" + + // CheckBundle.Metric.Status can be one of these values + metricStatusActive = "active" + metricStatusAvailable = "available" +) + +var metricDescriptions = attrDescrs{ + metricActiveAttr: "Enables or disables the metric", + metricNameAttr: "Name of the metric", + metricTypeAttr: "Type of metric (e.g. numeric, histogram, text)", + metricTagsAttr: "Tags assigned to the metric", + metricUnitAttr: "The unit of measurement for a metric", +} + +func resourceMetric() *schema.Resource { + return &schema.Resource{ + Create: metricCreate, + Read: metricRead, + Update: metricUpdate, + Delete: metricDelete, + Exists: metricExists, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: convertToHelperSchema(metricDescriptions, 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]+`), + }, + metricTypeAttr: &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateStringIn(metricTypeAttr, validMetricTypes), + }, + metricTagsAttr: tagMakeConfigSchema(metricTagsAttr), + metricUnitAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: metricUnit, + ValidateFunc: validateRegexp(metricUnitAttr, metricUnitRegexp), + }, + }), + } +} + +func metricCreate(d *schema.ResourceData, meta interface{}) error { + m := newMetric() + + id := d.Id() + if id == "" { + var err error + id, err = newMetricID() + if err != nil { + return errwrap.Wrapf("metric ID creation failed: {{err}}", err) + } + } + + if err := m.ParseConfig(id, d); err != nil { + return errwrap.Wrapf("error parsing metric schema during create: {{err}}", err) + } + + if err := m.Create(d); err != nil { + return errwrap.Wrapf("error creating metric: {{err}}", err) + } + + return metricRead(d, meta) +} + +func metricRead(d *schema.ResourceData, meta interface{}) error { + m := newMetric() + + if err := m.ParseConfig(d.Id(), d); err != nil { + return errwrap.Wrapf("error parsing metric schema during read: {{err}}", err) + } + + if err := m.SaveState(d); err != nil { + return errwrap.Wrapf("error saving metric during read: {{err}}", err) + } + + return nil +} + +func metricUpdate(d *schema.ResourceData, meta interface{}) error { + m := newMetric() + + if err := m.ParseConfig(d.Id(), d); err != nil { + return errwrap.Wrapf("error parsing metric schema during update: {{err}}", err) + } + + if err := m.Update(d); err != nil { + return errwrap.Wrapf("error updating metric: {{err}}", err) + } + + return nil +} + +func metricDelete(d *schema.ResourceData, meta interface{}) error { + d.SetId("") + + return nil +} + +func metricExists(d *schema.ResourceData, meta interface{}) (bool, error) { + if id := d.Id(); id != "" { + return true, nil + } + + return false, nil +} diff --git a/builtin/providers/circonus/resource_circonus_metric_cluster.go b/builtin/providers/circonus/resource_circonus_metric_cluster.go new file mode 100644 index 000000000..f8776099b --- /dev/null +++ b/builtin/providers/circonus/resource_circonus_metric_cluster.go @@ -0,0 +1,262 @@ +package circonus + +import ( + "bytes" + "fmt" + "strings" + + "github.com/circonus-labs/circonus-gometrics/api" + "github.com/circonus-labs/circonus-gometrics/api/config" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +const ( + // circonus_metric_cluster.* resource attribute names + metricClusterDescriptionAttr = "description" + metricClusterNameAttr = "name" + metricClusterQueryAttr = "query" + metricClusterTagsAttr = "tags" + + // circonus_metric_cluster.* out parameters + metricClusterIDAttr = "id" + + // circonus_metric_cluster.query.* resource attribute names + metricClusterDefinitionAttr = "definition" + metricClusterTypeAttr = "type" +) + +var metricClusterDescriptions = attrDescrs{ + metricClusterDescriptionAttr: "A description of the metric cluster", + metricClusterIDAttr: "The ID of this metric cluster", + metricClusterNameAttr: "The name of the metric cluster", + metricClusterQueryAttr: "A metric cluster query definition", + metricClusterTagsAttr: "A list of tags assigned to the metric cluster", +} + +var metricClusterQueryDescriptions = attrDescrs{ + metricClusterDefinitionAttr: "A query to select a collection of metric streams", + metricClusterTypeAttr: "The operation to perform on the matching metric streams", +} + +func resourceMetricCluster() *schema.Resource { + return &schema.Resource{ + Create: metricClusterCreate, + Read: metricClusterRead, + Update: metricClusterUpdate, + Delete: metricClusterDelete, + Exists: metricClusterExists, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: convertToHelperSchema(metricClusterDescriptions, map[schemaAttr]*schema.Schema{ + metricClusterDescriptionAttr: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + StateFunc: suppressWhitespace, + }, + metricClusterNameAttr: &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + metricClusterQueryAttr: &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + MinItems: 1, + Elem: &schema.Resource{ + Schema: convertToHelperSchema(metricClusterQueryDescriptions, map[schemaAttr]*schema.Schema{ + metricClusterDefinitionAttr: &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateRegexp(metricClusterDefinitionAttr, `.+`), + }, + metricClusterTypeAttr: &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateStringIn(metricClusterTypeAttr, supportedMetricClusterTypes), + }, + }), + }, + }, + metricClusterTagsAttr: tagMakeConfigSchema(metricClusterTagsAttr), + + // Out parameters + metricClusterIDAttr: &schema.Schema{ + Computed: true, + Type: schema.TypeString, + ValidateFunc: validateRegexp(metricClusterIDAttr, config.MetricClusterCIDRegex), + }, + }), + } +} + +func metricClusterCreate(d *schema.ResourceData, meta interface{}) error { + ctxt := meta.(*providerContext) + mc := newMetricCluster() + + if err := mc.ParseConfig(d); err != nil { + return errwrap.Wrapf("error parsing metric cluster schema during create: {{err}}", err) + } + + if err := mc.Create(ctxt); err != nil { + return errwrap.Wrapf("error creating metric cluster: {{err}}", err) + } + + d.SetId(mc.CID) + + return metricClusterRead(d, meta) +} + +func metricClusterExists(d *schema.ResourceData, meta interface{}) (bool, error) { + ctxt := meta.(*providerContext) + + cid := d.Id() + mc, err := ctxt.client.FetchMetricCluster(api.CIDType(&cid), "") + if err != nil { + if strings.Contains(err.Error(), defaultCirconus404ErrorString) { + return false, nil + } + + return false, err + } + + if mc.CID == "" { + return false, nil + } + + return true, nil +} + +// metricClusterRead pulls data out of the MetricCluster object and stores it +// into the appropriate place in the statefile. +func metricClusterRead(d *schema.ResourceData, meta interface{}) error { + ctxt := meta.(*providerContext) + + cid := d.Id() + mc, err := loadMetricCluster(ctxt, api.CIDType(&cid)) + if err != nil { + return err + } + + d.SetId(mc.CID) + + queries := schema.NewSet(metricClusterQueryChecksum, nil) + for _, query := range mc.Queries { + queryAttrs := map[string]interface{}{ + string(metricClusterDefinitionAttr): query.Query, + string(metricClusterTypeAttr): query.Type, + } + + queries.Add(queryAttrs) + } + + d.Set(metricClusterDescriptionAttr, mc.Description) + d.Set(metricClusterNameAttr, mc.Name) + + if err := d.Set(metricClusterTagsAttr, tagsToState(apiToTags(mc.Tags))); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to store metric cluster %q attribute: {{err}}", metricClusterTagsAttr), err) + } + + d.Set(metricClusterIDAttr, mc.CID) + + return nil +} + +func metricClusterUpdate(d *schema.ResourceData, meta interface{}) error { + ctxt := meta.(*providerContext) + mc := newMetricCluster() + + if err := mc.ParseConfig(d); err != nil { + return err + } + + mc.CID = d.Id() + if err := mc.Update(ctxt); err != nil { + return errwrap.Wrapf(fmt.Sprintf("unable to update metric cluster %q: {{err}}", d.Id()), err) + } + + return metricClusterRead(d, meta) +} + +func metricClusterDelete(d *schema.ResourceData, meta interface{}) error { + ctxt := meta.(*providerContext) + + cid := d.Id() + if _, err := ctxt.client.DeleteMetricClusterByCID(api.CIDType(&cid)); err != nil { + return errwrap.Wrapf(fmt.Sprintf("unable to delete metric cluster %q: {{err}}", d.Id()), err) + } + + d.SetId("") + + return nil +} + +func metricClusterQueryChecksum(v interface{}) int { + m := v.(map[string]interface{}) + + b := &bytes.Buffer{} + b.Grow(defaultHashBufSize) + + // Order writes to the buffer using lexically sorted list for easy visual + // reconciliation with other lists. + if v, found := m[metricClusterDefinitionAttr]; found { + fmt.Fprint(b, v.(string)) + } + + if v, found := m[metricClusterTypeAttr]; found { + fmt.Fprint(b, v.(string)) + } + + s := b.String() + return hashcode.String(s) +} + +// ParseConfig reads Terraform config data and stores the information into a +// Circonus MetricCluster object. +func (mc *circonusMetricCluster) ParseConfig(d *schema.ResourceData) error { + if v, found := d.GetOk(metricClusterDescriptionAttr); found { + mc.Description = v.(string) + } + + if v, found := d.GetOk(metricClusterNameAttr); found { + mc.Name = v.(string) + } + + if queryListRaw, found := d.GetOk(metricClusterQueryAttr); found { + queryList := queryListRaw.(*schema.Set).List() + + mc.Queries = make([]api.MetricQuery, 0, len(queryList)) + + for _, queryRaw := range queryList { + queryAttrs := newInterfaceMap(queryRaw) + + var query string + if v, found := queryAttrs[metricClusterDefinitionAttr]; found { + query = v.(string) + } + + var queryType string + if v, found := queryAttrs[metricClusterTypeAttr]; found { + queryType = v.(string) + } + + mc.Queries = append(mc.Queries, api.MetricQuery{ + Query: query, + Type: queryType, + }) + } + } + + if v, found := d.GetOk(metricClusterTagsAttr); found { + mc.Tags = derefStringList(flattenSet(v.(*schema.Set))) + } + + if err := mc.Validate(); err != nil { + return err + } + + return nil +} diff --git a/builtin/providers/circonus/resource_circonus_metric_cluster_test.go b/builtin/providers/circonus/resource_circonus_metric_cluster_test.go new file mode 100644 index 000000000..8c501041d --- /dev/null +++ b/builtin/providers/circonus/resource_circonus_metric_cluster_test.go @@ -0,0 +1,95 @@ +package circonus + +import ( + "fmt" + "strings" + "testing" + + "github.com/circonus-labs/circonus-gometrics/api" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccCirconusMetricCluster_basic(t *testing.T) { + metricClusterName := fmt.Sprintf("job1-stream-agg - %s", acctest.RandString(5)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDestroyCirconusMetricCluster, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccCirconusMetricClusterConfigFmt, metricClusterName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("circonus_metric_cluster.nomad-job1", "description", `Metric Cluster Description`), + resource.TestCheckResourceAttrSet("circonus_metric_cluster.nomad-job1", "id"), + resource.TestCheckResourceAttr("circonus_metric_cluster.nomad-job1", "name", metricClusterName), + resource.TestCheckResourceAttr("circonus_metric_cluster.nomad-job1", "query.236803225.definition", "*`nomad-jobname`memory`rss"), + resource.TestCheckResourceAttr("circonus_metric_cluster.nomad-job1", "query.236803225.type", "average"), + resource.TestCheckResourceAttr("circonus_metric_cluster.nomad-job1", "tags.2087084518", "author:terraform"), + resource.TestCheckResourceAttr("circonus_metric_cluster.nomad-job1", "tags.3354173695", "source:nomad"), + ), + }, + }, + }) +} + +func testAccCheckDestroyCirconusMetricCluster(s *terraform.State) error { + ctxt := testAccProvider.Meta().(*providerContext) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "circonus_metric_cluster" { + continue + } + + cid := rs.Primary.ID + exists, err := checkMetricClusterExists(ctxt, api.CIDType(&cid)) + switch { + case !exists: + // noop + case exists: + return fmt.Errorf("metric cluster still exists after destroy") + case err != nil: + return fmt.Errorf("Error checking metric cluster: %v", err) + } + } + + return nil +} + +func checkMetricClusterExists(c *providerContext, metricClusterCID api.CIDType) (bool, error) { + cmc, err := c.client.FetchMetricCluster(metricClusterCID, "") + if err != nil { + if strings.Contains(err.Error(), defaultCirconus404ErrorString) { + return false, nil + } + + return false, err + } + + if api.CIDType(&cmc.CID) == metricClusterCID { + return true, nil + } + + return false, nil +} + +const testAccCirconusMetricClusterConfigFmt = ` +resource "circonus_metric_cluster" "nomad-job1" { + description = < 0 { + thenAttrs[string(ruleSetAfterAttr)] = fmt.Sprintf("%ds", 60*rule.Wait) + } + thenAttrs[string(ruleSetSeverityAttr)] = int(rule.Severity) + + if rule.WindowingFunction != nil { + valueOverAttrs[string(ruleSetUsingAttr)] = *rule.WindowingFunction + + // NOTE: Only save the window duration if a function was specified + valueOverAttrs[string(ruleSetLastAttr)] = fmt.Sprintf("%ds", rule.WindowingDuration) + } + valueOverSet := schema.NewSet(ruleSetValueOverChecksum, nil) + valueOverSet.Add(valueOverAttrs) + valueAttrs[string(ruleSetOverAttr)] = valueOverSet + + if contactGroups, ok := rs.ContactGroups[uint8(rule.Severity)]; ok { + sort.Strings(contactGroups) + thenAttrs[string(ruleSetNotifyAttr)] = contactGroups + } + thenSet := schema.NewSet(ruleSetThenChecksum, nil) + thenSet.Add(thenAttrs) + + valueSet := schema.NewSet(ruleSetValueChecksum, nil) + valueSet.Add(valueAttrs) + ifAttrs[string(ruleSetThenAttr)] = thenSet + ifAttrs[string(ruleSetValueAttr)] = valueSet + + ifRules = append(ifRules, ifAttrs) + } + + d.Set(ruleSetCheckAttr, rs.CheckCID) + + if err := d.Set(ruleSetIfAttr, ifRules); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to store rule set %q attribute: {{err}}", ruleSetIfAttr), err) + } + + d.Set(ruleSetLinkAttr, indirect(rs.Link)) + d.Set(ruleSetMetricNameAttr, rs.MetricName) + d.Set(ruleSetMetricTypeAttr, rs.MetricType) + d.Set(ruleSetNotesAttr, indirect(rs.Notes)) + d.Set(ruleSetParentAttr, indirect(rs.Parent)) + + if err := d.Set(ruleSetTagsAttr, tagsToState(apiToTags(rs.Tags))); err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to store rule set %q attribute: {{err}}", ruleSetTagsAttr), err) + } + + return nil +} + +func ruleSetUpdate(d *schema.ResourceData, meta interface{}) error { + ctxt := meta.(*providerContext) + rs := newRuleSet() + + if err := rs.ParseConfig(d); err != nil { + return err + } + + rs.CID = d.Id() + if err := rs.Update(ctxt); err != nil { + return errwrap.Wrapf(fmt.Sprintf("unable to update rule set %q: {{err}}", d.Id()), err) + } + + return ruleSetRead(d, meta) +} + +func ruleSetDelete(d *schema.ResourceData, meta interface{}) error { + ctxt := meta.(*providerContext) + + cid := d.Id() + if _, err := ctxt.client.DeleteRuleSetByCID(api.CIDType(&cid)); err != nil { + return errwrap.Wrapf(fmt.Sprintf("unable to delete rule set %q: {{err}}", d.Id()), err) + } + + d.SetId("") + + return nil +} + +type circonusRuleSet struct { + api.RuleSet +} + +func newRuleSet() circonusRuleSet { + rs := circonusRuleSet{ + RuleSet: *api.NewRuleSet(), + } + + rs.ContactGroups = make(map[uint8][]string, config.NumSeverityLevels) + for i := uint8(0); i < config.NumSeverityLevels; i++ { + rs.ContactGroups[i+1] = make([]string, 0, 1) + } + + rs.Rules = make([]api.RuleSetRule, 0, 1) + + return rs +} + +func loadRuleSet(ctxt *providerContext, cid api.CIDType) (circonusRuleSet, error) { + var rs circonusRuleSet + crs, err := ctxt.client.FetchRuleSet(cid) + if err != nil { + return circonusRuleSet{}, err + } + rs.RuleSet = *crs + + return rs, nil +} + +func ruleSetThenChecksum(v interface{}) int { + b := &bytes.Buffer{} + b.Grow(defaultHashBufSize) + + writeInt := func(m map[string]interface{}, attrName string) { + if v, found := m[attrName]; found { + i := v.(int) + if i != 0 { + fmt.Fprintf(b, "%x", i) + } + } + } + + writeString := func(m map[string]interface{}, attrName string) { + if v, found := m[attrName]; found { + s := strings.TrimSpace(v.(string)) + if s != "" { + fmt.Fprint(b, s) + } + } + } + + writeStringArray := func(m map[string]interface{}, attrName string) { + if v, found := m[attrName]; found { + a := v.([]string) + if a != nil { + sort.Strings(a) + for _, s := range a { + fmt.Fprint(b, strings.TrimSpace(s)) + } + } + } + } + + m := v.(map[string]interface{}) + + writeString(m, ruleSetAfterAttr) + writeStringArray(m, ruleSetNotifyAttr) + writeInt(m, ruleSetSeverityAttr) + + s := b.String() + return hashcode.String(s) +} + +func ruleSetValueChecksum(v interface{}) int { + b := &bytes.Buffer{} + b.Grow(defaultHashBufSize) + + writeBool := func(m map[string]interface{}, attrName string) { + if v, found := m[attrName]; found { + fmt.Fprintf(b, "%t", v.(bool)) + } + } + + writeDuration := func(m map[string]interface{}, attrName string) { + if v, found := m[attrName]; found { + s := v.(string) + if s != "" { + d, _ := time.ParseDuration(s) + fmt.Fprint(b, d.String()) + } + } + } + + writeString := func(m map[string]interface{}, attrName string) { + if v, found := m[attrName]; found { + s := strings.TrimSpace(v.(string)) + if s != "" { + fmt.Fprint(b, s) + } + } + } + + m := v.(map[string]interface{}) + + if v, found := m[ruleSetValueAttr]; found { + valueMap := v.(map[string]interface{}) + if valueMap != nil { + writeDuration(valueMap, ruleSetAbsentAttr) + writeBool(valueMap, ruleSetChangedAttr) + writeString(valueMap, ruleSetContainsAttr) + writeString(valueMap, ruleSetMatchAttr) + writeString(valueMap, ruleSetNotMatchAttr) + writeString(valueMap, ruleSetMinValueAttr) + writeString(valueMap, ruleSetNotContainAttr) + writeString(valueMap, ruleSetMaxValueAttr) + + if v, found := valueMap[ruleSetOverAttr]; found { + overMap := v.(map[string]interface{}) + writeDuration(overMap, ruleSetLastAttr) + writeString(overMap, ruleSetUsingAttr) + } + } + } + + s := b.String() + return hashcode.String(s) +} + +func ruleSetValueOverChecksum(v interface{}) int { + b := &bytes.Buffer{} + b.Grow(defaultHashBufSize) + + writeString := func(m map[string]interface{}, attrName string) { + if v, found := m[attrName]; found { + s := strings.TrimSpace(v.(string)) + if s != "" { + fmt.Fprint(b, s) + } + } + } + + m := v.(map[string]interface{}) + + writeString(m, ruleSetLastAttr) + writeString(m, ruleSetUsingAttr) + + s := b.String() + return hashcode.String(s) +} + +// ParseConfig reads Terraform config data and stores the information into a +// Circonus RuleSet object. ParseConfig, ruleSetRead(), and ruleSetChecksum +// must be kept in sync. +func (rs *circonusRuleSet) ParseConfig(d *schema.ResourceData) error { + if v, found := d.GetOk(ruleSetCheckAttr); found { + rs.CheckCID = v.(string) + } + + if v, found := d.GetOk(ruleSetLinkAttr); found { + s := v.(string) + rs.Link = &s + } + + if v, found := d.GetOk(ruleSetMetricTypeAttr); found { + rs.MetricType = v.(string) + } + + if v, found := d.GetOk(ruleSetNotesAttr); found { + s := v.(string) + rs.Notes = &s + } + + if v, found := d.GetOk(ruleSetParentAttr); found { + s := v.(string) + rs.Parent = &s + } + + if v, found := d.GetOk(ruleSetMetricNameAttr); found { + rs.MetricName = v.(string) + } + + rs.Rules = make([]api.RuleSetRule, 0, defaultRuleSetRuleLen) + if ifListRaw, found := d.GetOk(ruleSetIfAttr); found { + ifList := ifListRaw.([]interface{}) + for _, ifListElem := range ifList { + ifAttrs := newInterfaceMap(ifListElem.(map[string]interface{})) + + rule := api.RuleSetRule{} + + if thenListRaw, found := ifAttrs[ruleSetThenAttr]; found { + thenList := thenListRaw.(*schema.Set).List() + + for _, thenListRaw := range thenList { + thenAttrs := newInterfaceMap(thenListRaw) + + if v, found := thenAttrs[ruleSetAfterAttr]; found { + s := v.(string) + if s != "" { + d, err := time.ParseDuration(v.(string)) + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("unable to parse %q duration %q: {{err}}", ruleSetAfterAttr, v.(string)), err) + } + rule.Wait = uint(d.Minutes()) + } + } + + // NOTE: break from convention of alpha sorting attributes and handle Notify after Severity + + if i, found := thenAttrs[ruleSetSeverityAttr]; found { + rule.Severity = uint(i.(int)) + } + + if notifyListRaw, found := thenAttrs[ruleSetNotifyAttr]; found { + notifyList := interfaceList(notifyListRaw.([]interface{})) + + sev := uint8(rule.Severity) + for _, contactGroupCID := range notifyList.List() { + var found bool + if contactGroups, ok := rs.ContactGroups[sev]; ok { + for _, contactGroup := range contactGroups { + if contactGroup == contactGroupCID { + found = true + break + } + } + } + if !found { + rs.ContactGroups[sev] = append(rs.ContactGroups[sev], contactGroupCID) + } + } + } + } + } + + if ruleSetValueListRaw, found := ifAttrs[ruleSetValueAttr]; found { + ruleSetValueList := ruleSetValueListRaw.(*schema.Set).List() + + for _, valueListRaw := range ruleSetValueList { + valueAttrs := newInterfaceMap(valueListRaw) + + METRIC_TYPE: + switch rs.MetricType { + case ruleSetMetricTypeNumeric: + if v, found := valueAttrs[ruleSetAbsentAttr]; found { + s := v.(string) + if s != "" { + d, _ := time.ParseDuration(s) + rule.Criteria = apiRuleSetAbsent + rule.Value = float64(d.Seconds()) + break METRIC_TYPE + } + } + + if v, found := valueAttrs[ruleSetChangedAttr]; found { + b := v.(bool) + if b { + rule.Criteria = apiRuleSetChanged + break METRIC_TYPE + } + } + + if v, found := valueAttrs[ruleSetMinValueAttr]; found { + s := v.(string) + if s != "" { + rule.Criteria = apiRuleSetMinValue + rule.Value = s + break METRIC_TYPE + } + } + + if v, found := valueAttrs[ruleSetMaxValueAttr]; found { + s := v.(string) + if s != "" { + rule.Criteria = apiRuleSetMaxValue + rule.Value = s + break METRIC_TYPE + } + } + case ruleSetMetricTypeText: + if v, found := valueAttrs[ruleSetAbsentAttr]; found { + s := v.(string) + if s != "" { + d, _ := time.ParseDuration(s) + rule.Criteria = apiRuleSetAbsent + rule.Value = float64(d.Seconds()) + break METRIC_TYPE + } + } + + if v, found := valueAttrs[ruleSetChangedAttr]; found { + b := v.(bool) + if b { + rule.Criteria = apiRuleSetChanged + break METRIC_TYPE + } + } + + if v, found := valueAttrs[ruleSetContainsAttr]; found { + s := v.(string) + if s != "" { + rule.Criteria = apiRuleSetContains + rule.Value = s + break METRIC_TYPE + } + } + + if v, found := valueAttrs[ruleSetMatchAttr]; found { + s := v.(string) + if s != "" { + rule.Criteria = apiRuleSetMatch + rule.Value = s + break METRIC_TYPE + } + } + + if v, found := valueAttrs[ruleSetNotMatchAttr]; found { + s := v.(string) + if s != "" { + rule.Criteria = apiRuleSetNotMatch + rule.Value = s + break METRIC_TYPE + } + } + + if v, found := valueAttrs[ruleSetNotContainAttr]; found { + s := v.(string) + if s != "" { + rule.Criteria = apiRuleSetNotContains + rule.Value = s + break METRIC_TYPE + } + } + default: + return fmt.Errorf("PROVIDER BUG: unsupported rule set metric type: %q", rs.MetricType) + } + + if ruleSetOverListRaw, found := valueAttrs[ruleSetOverAttr]; found { + overList := ruleSetOverListRaw.(*schema.Set).List() + + for _, overListRaw := range overList { + overAttrs := newInterfaceMap(overListRaw) + + if v, found := overAttrs[ruleSetLastAttr]; found { + last, err := time.ParseDuration(v.(string)) + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("unable to parse duration %s attribute", ruleSetLastAttr), err) + } + rule.WindowingDuration = uint(last.Seconds()) + } + + if v, found := overAttrs[ruleSetUsingAttr]; found { + s := v.(string) + rule.WindowingFunction = &s + } + } + } + } + } + rs.Rules = append(rs.Rules, rule) + } + } + + if v, found := d.GetOk(ruleSetTagsAttr); found { + rs.Tags = derefStringList(flattenSet(v.(*schema.Set))) + } + + if err := rs.Validate(); err != nil { + return err + } + + return nil +} + +func (rs *circonusRuleSet) Create(ctxt *providerContext) error { + crs, err := ctxt.client.CreateRuleSet(&rs.RuleSet) + if err != nil { + return err + } + + rs.CID = crs.CID + + return nil +} + +func (rs *circonusRuleSet) Update(ctxt *providerContext) error { + _, err := ctxt.client.UpdateRuleSet(&rs.RuleSet) + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Unable to update rule set %s: {{err}}", rs.CID), err) + } + + return nil +} + +func (rs *circonusRuleSet) Validate() error { + // TODO(sean@): From https://login.circonus.com/resources/api/calls/rule_set + // under `value`: + // + // For an 'on absence' rule this is the number of seconds the metric must not + // have been collected for, and should not be lower than either the period or + // timeout of the metric being collected. + + for i, rule := range rs.Rules { + if rule.Criteria == "" { + return fmt.Errorf("rule %d for check ID %s has an empty criteria", i, rs.CheckCID) + } + } + + return nil +} diff --git a/builtin/providers/circonus/resource_circonus_rule_set_test.go b/builtin/providers/circonus/resource_circonus_rule_set_test.go new file mode 100644 index 000000000..71cf94ceb --- /dev/null +++ b/builtin/providers/circonus/resource_circonus_rule_set_test.go @@ -0,0 +1,226 @@ +package circonus + +import ( + "fmt" + "strings" + "testing" + + "github.com/circonus-labs/circonus-gometrics/api" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccCirconusRuleSet_basic(t *testing.T) { + checkName := fmt.Sprintf("ICMP Ping check - %s", acctest.RandString(5)) + contactGroupName := fmt.Sprintf("ops-staging-sev3 - %s", acctest.RandString(5)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDestroyCirconusRuleSet, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccCirconusRuleSetConfigFmt, contactGroupName, checkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("circonus_rule_set.icmp-latency-alarm", "check"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "metric_name", "maximum"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "metric_type", "numeric"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "notes", "Simple check to create notifications based on ICMP performance."), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "link", "https://wiki.example.org/playbook/what-to-do-when-high-latency-strikes"), + // resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "parent", "some check ID"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.#", "4"), + + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.0.value.#", "1"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.0.value.360613670.absent", "70s"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.0.value.360613670.over.#", "0"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.0.then.#", "1"), + // Computed: + // resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.0.then..notify.#", "1"), + // resource.TestCheckResourceAttrSet("circonus_rule_set.icmp-latency-alarm", "if.0.then..notify.0"), + // resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.0.then..severity", "1"), + + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.1.value.#", "1"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.1.value.2300199732.over.#", "1"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.1.value.2300199732.over.689776960.last", "120s"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.1.value.2300199732.over.689776960.using", "average"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.1.value.2300199732.min_value", "2"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.1.then.#", "1"), + // Computed: + // resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.1.then..notify.#", "1"), + // resource.TestCheckResourceAttrSet("circonus_rule_set.icmp-latency-alarm", "if.1.then..notify.0"), + // resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.1.then..severity", "2"), + + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.2.value.#", "1"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.2.value.2842654150.over.#", "1"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.2.value.2842654150.over.999877839.last", "180s"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.2.value.2842654150.over.999877839.using", "average"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.2.value.2842654150.max_value", "300"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.2.then.#", "1"), + // Computed: + // resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.2.then..notify.#", "1"), + // resource.TestCheckResourceAttrSet("circonus_rule_set.icmp-latency-alarm", "if.2.then..notify.0"), + // resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.2.then..severity", "3"), + + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.3.value.#", "1"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.3.value.803690187.over.#", "0"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.3.value.803690187.max_value", "400"), + // Computed: + // resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.3.then..notify.#", "1"), + // resource.TestCheckResourceAttrSet("circonus_rule_set.icmp-latency-alarm", "if.3.then..notify.0"), + // resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.3.then..after", "2400s"), + // resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "if.3.then..severity", "4"), + + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "tags.#", "2"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "tags.2087084518", "author:terraform"), + resource.TestCheckResourceAttr("circonus_rule_set.icmp-latency-alarm", "tags.1401442048", "lifecycle:unittest"), + ), + }, + }, + }) +} + +func testAccCheckDestroyCirconusRuleSet(s *terraform.State) error { + ctxt := testAccProvider.Meta().(*providerContext) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "circonus_rule_set" { + continue + } + + cid := rs.Primary.ID + exists, err := checkRuleSetExists(ctxt, api.CIDType(&cid)) + switch { + case !exists: + // noop + case exists: + return fmt.Errorf("rule set still exists after destroy") + case err != nil: + return fmt.Errorf("Error checking rule set: %v", err) + } + } + + return nil +} + +func checkRuleSetExists(c *providerContext, ruleSetCID api.CIDType) (bool, error) { + rs, err := c.client.FetchRuleSet(ruleSetCID) + if err != nil { + if strings.Contains(err.Error(), defaultCirconus404ErrorString) { + return false, nil + } + + return false, err + } + + if api.CIDType(&rs.CID) == ruleSetCID { + return true, nil + } + + return false, nil +} + +const testAccCirconusRuleSetConfigFmt = ` +variable "test_tags" { + type = "list" + default = [ "author:terraform", "lifecycle:unittest" ] +} + +resource "circonus_contact_group" "test-trigger" { + name = "%s" + tags = [ "${var.test_tags}" ] +} + +resource "circonus_check" "api_latency" { + active = true + name = "%s" + period = "60s" + + collector { + id = "/broker/1" + } + + icmp_ping { + count = 1 + } + + metric { + name = "maximum" + tags = [ "${var.test_tags}" ] + type = "numeric" + unit = "seconds" + } + + tags = [ "${var.test_tags}" ] + target = "api.circonus.com" +} + +resource "circonus_rule_set" "icmp-latency-alarm" { + check = "${circonus_check.api_latency.checks[0]}" + metric_name = "maximum" + // metric_name = "${circonus_check.api_latency.metric["maximum"].name}" + // metric_type = "${circonus_check.api_latency.metric["maximum"].type}" + notes = <", v.(string), err) + } + + return fmt.Sprintf("%ds", int(d.Seconds())) + default: + return fmt.Sprintf("", v) + } +} + +func indirect(v interface{}) interface{} { + switch v.(type) { + case string: + return v + case *string: + p := v.(*string) + if p == nil { + return nil + } + return *p + default: + return v + } +} + +func suppressEquivalentTimeDurations(k, old, new string, d *schema.ResourceData) bool { + d1, err := time.ParseDuration(old) + if err != nil { + return false + } + + d2, err := time.ParseDuration(new) + if err != nil { + return false + } + + return d1 == d2 +} + +func suppressWhitespace(v interface{}) string { + return strings.TrimSpace(v.(string)) +} diff --git a/builtin/providers/circonus/validators.go b/builtin/providers/circonus/validators.go new file mode 100644 index 000000000..7fd5e85a3 --- /dev/null +++ b/builtin/providers/circonus/validators.go @@ -0,0 +1,370 @@ +package circonus + +import ( + "fmt" + "net/url" + "regexp" + "strings" + "time" + + "github.com/circonus-labs/circonus-gometrics/api" + "github.com/circonus-labs/circonus-gometrics/api/config" + "github.com/hashicorp/errwrap" +) + +var knownCheckTypes map[circonusCheckType]struct{} +var knownContactMethods map[contactMethods]struct{} + +var userContactMethods map[contactMethods]struct{} +var externalContactMethods map[contactMethods]struct{} +var supportedHTTPVersions = validStringValues{"0.9", "1.0", "1.1", "2.0"} +var supportedMetricClusterTypes = validStringValues{ + "average", "count", "counter", "counter2", "counter2_stddev", + "counter_stddev", "derive", "derive2", "derive2_stddev", "derive_stddev", + "histogram", "stddev", "text", +} + +func init() { + checkTypes := []circonusCheckType{ + "caql", "cim", "circonuswindowsagent", "circonuswindowsagent,nad", + "collectd", "composite", "dcm", "dhcp", "dns", "elasticsearch", + "external", "ganglia", "googleanalytics", "haproxy", "http", + "http,apache", "httptrap", "imap", "jmx", "json", "json,couchdb", + "json,mongodb", "json,nad", "json,riak", "ldap", "memcached", + "munin", "mysql", "newrelic_rpm", "nginx", "nrpe", "ntp", + "oracle", "ping_icmp", "pop3", "postgres", "redis", "resmon", + "smtp", "snmp", "snmp,momentum", "sqlserver", "ssh2", "statsd", + "tcp", "varnish", "keynote", "keynote_pulse", "cloudwatch", + "ec_console", "mongodb", + } + + knownCheckTypes = make(map[circonusCheckType]struct{}, len(checkTypes)) + for _, k := range checkTypes { + knownCheckTypes[k] = struct{}{} + } + + userMethods := []contactMethods{"email", "sms", "xmpp"} + externalMethods := []contactMethods{"slack"} + + knownContactMethods = make(map[contactMethods]struct{}, len(externalContactMethods)+len(userContactMethods)) + + externalContactMethods = make(map[contactMethods]struct{}, len(externalMethods)) + for _, k := range externalMethods { + knownContactMethods[k] = struct{}{} + externalContactMethods[k] = struct{}{} + } + + userContactMethods = make(map[contactMethods]struct{}, len(userMethods)) + for _, k := range userMethods { + knownContactMethods[k] = struct{}{} + userContactMethods[k] = struct{}{} + } +} + +func validateCheckType(v interface{}, key string) (warnings []string, errors []error) { + if _, ok := knownCheckTypes[circonusCheckType(v.(string))]; !ok { + warnings = append(warnings, fmt.Sprintf("Possibly unsupported check type: %s", v.(string))) + } + + return warnings, errors +} + +func validateCheckCloudWatchDimmensions(v interface{}, key string) (warnings []string, errors []error) { + validDimmensionName := regexp.MustCompile(`^[\S]+$`) + validDimmensionValue := regexp.MustCompile(`^[\S]+$`) + + dimmensions := v.(map[string]interface{}) + for k, vRaw := range dimmensions { + if !validDimmensionName.MatchString(k) { + errors = append(errors, fmt.Errorf("Invalid CloudWatch Dimmension Name specified: %q", k)) + continue + } + + v := vRaw.(string) + if !validDimmensionValue.MatchString(v) { + errors = append(errors, fmt.Errorf("Invalid value for CloudWatch Dimmension %q specified: %q", k, v)) + } + } + + return warnings, errors +} + +func validateContactGroup(cg *api.ContactGroup) error { + for i := range cg.Reminders { + if cg.Reminders[i] != 0 && cg.AggregationWindow > cg.Reminders[i] { + return fmt.Errorf("severity %d reminder (%ds) is shorter than the aggregation window (%ds)", i+1, cg.Reminders[i], cg.AggregationWindow) + } + } + + for severityIndex := range cg.Escalations { + switch { + case cg.Escalations[severityIndex] == nil: + continue + case cg.Escalations[severityIndex].After > 0 && cg.Escalations[severityIndex].ContactGroupCID == "", + cg.Escalations[severityIndex].After == 0 && cg.Escalations[severityIndex].ContactGroupCID != "": + return fmt.Errorf("severity %d escalation requires both and %s and %s be set", severityIndex+1, contactEscalateToAttr, contactEscalateAfterAttr) + } + } + + return nil +} + +func validateContactGroupCID(attrName schemaAttr) func(v interface{}, key string) (warnings []string, errors []error) { + return func(v interface{}, key string) (warnings []string, errors []error) { + validContactGroupCID := regexp.MustCompile(config.ContactGroupCIDRegex) + + if !validContactGroupCID.MatchString(v.(string)) { + errors = append(errors, fmt.Errorf("Invalid %s specified (%q)", attrName, v.(string))) + } + + return warnings, errors + } +} + +func validateDurationMin(attrName schemaAttr, minDuration string) func(v interface{}, key string) (warnings []string, errors []error) { + var min time.Duration + { + var err error + min, err = time.ParseDuration(minDuration) + if err != nil { + return func(interface{}, string) (warnings []string, errors []error) { + errors = []error{errwrap.Wrapf(fmt.Sprintf("Invalid time +%q: {{err}}", minDuration), err)} + return warnings, errors + } + } + } + + return func(v interface{}, key string) (warnings []string, errors []error) { + d, err := time.ParseDuration(v.(string)) + switch { + case err != nil: + errors = append(errors, errwrap.Wrapf(fmt.Sprintf("Invalid %s specified (%q): {{err}}", attrName, v.(string)), err)) + case d < min: + errors = append(errors, fmt.Errorf("Invalid %s specified (%q): minimum value must be %s", attrName, v.(string), min)) + } + + return warnings, errors + } +} + +func validateDurationMax(attrName schemaAttr, maxDuration string) func(v interface{}, key string) (warnings []string, errors []error) { + var max time.Duration + { + var err error + max, err = time.ParseDuration(maxDuration) + if err != nil { + return func(interface{}, string) (warnings []string, errors []error) { + errors = []error{errwrap.Wrapf(fmt.Sprintf("Invalid time +%q: {{err}}", maxDuration), err)} + return warnings, errors + } + } + } + + return func(v interface{}, key string) (warnings []string, errors []error) { + d, err := time.ParseDuration(v.(string)) + switch { + case err != nil: + errors = append(errors, errwrap.Wrapf(fmt.Sprintf("Invalid %s specified (%q): {{err}}", attrName, v.(string)), err)) + case d > max: + errors = append(errors, fmt.Errorf("Invalid %s specified (%q): maximum value must be less than or equal to %s", attrName, v.(string), max)) + } + + return warnings, errors + } +} + +func validateFloatMin(attrName schemaAttr, min float64) func(v interface{}, key string) (warnings []string, errors []error) { + return func(v interface{}, key string) (warnings []string, errors []error) { + if v.(float64) < min { + errors = append(errors, fmt.Errorf("Invalid %s specified (%f): minimum value must be %f", attrName, v.(float64), min)) + } + + return warnings, errors + } +} + +func validateFloatMax(attrName schemaAttr, max float64) func(v interface{}, key string) (warnings []string, errors []error) { + return func(v interface{}, key string) (warnings []string, errors []error) { + if v.(float64) > max { + errors = append(errors, fmt.Errorf("Invalid %s specified (%f): maximum value must be %f", attrName, v.(float64), max)) + } + + return warnings, errors + } +} + +// validateFuncs takes a list of functions and runs them in serial until either +// a warning or error is returned from the first validation function argument. +func validateFuncs(fns ...func(v interface{}, key string) (warnings []string, errors []error)) func(v interface{}, key string) (warnings []string, errors []error) { + 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 validateHTTPHeaders(v interface{}, key string) (warnings []string, errors []error) { + validHTTPHeader := regexp.MustCompile(`.+`) + validHTTPValue := regexp.MustCompile(`.+`) + + headers := v.(map[string]interface{}) + for k, vRaw := range headers { + if !validHTTPHeader.MatchString(k) { + errors = append(errors, fmt.Errorf("Invalid HTTP Header specified: %q", k)) + continue + } + + v := vRaw.(string) + if !validHTTPValue.MatchString(v) { + errors = append(errors, fmt.Errorf("Invalid value for HTTP Header %q specified: %q", k, v)) + } + } + + return warnings, errors +} + +func validateGraphAxisOptions(v interface{}, key string) (warnings []string, errors []error) { + axisOptionsMap := v.(map[string]interface{}) + validOpts := map[schemaAttr]struct{}{ + graphAxisLogarithmicAttr: struct{}{}, + graphAxisMaxAttr: struct{}{}, + graphAxisMinAttr: struct{}{}, + } + + for k := range axisOptionsMap { + if _, ok := validOpts[schemaAttr(k)]; !ok { + errors = append(errors, fmt.Errorf("Invalid axis option specified: %q", k)) + continue + } + } + + return warnings, errors +} + +func validateIntMin(attrName schemaAttr, 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): minimum value must be %d", attrName, v.(int), min)) + } + + return warnings, errors + } +} + +func validateIntMax(attrName schemaAttr, max int) func(v interface{}, key string) (warnings []string, errors []error) { + return func(v interface{}, key string) (warnings []string, errors []error) { + if v.(int) > max { + errors = append(errors, fmt.Errorf("Invalid %s specified (%d): maximum value must be %d", attrName, v.(int), max)) + } + + return warnings, errors + } +} + +func validateMetricType(v interface{}, key string) (warnings []string, errors []error) { + value := v.(string) + switch value { + case "caql", "composite", "histogram", "numeric", "text": + default: + errors = append(errors, fmt.Errorf("unsupported metric type %s", value)) + } + + return warnings, errors +} + +func validateRegexp(attrName schemaAttr, 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", attrName, v.(string))) + } + + return warnings, errors + } +} + +func validateTag(v interface{}, key string) (warnings []string, errors []error) { + tag := v.(string) + if !strings.ContainsRune(tag, ':') { + errors = append(errors, fmt.Errorf("tag %q is missing a category", tag)) + } + + return warnings, errors +} + +func validateUserCID(attrName string) func(v interface{}, key string) (warnings []string, errors []error) { + return func(v interface{}, key string) (warnings []string, errors []error) { + valid := regexp.MustCompile(config.UserCIDRegex) + + if !valid.MatchString(v.(string)) { + errors = append(errors, fmt.Errorf("Invalid %s specified (%q)", attrName, v.(string))) + } + + return warnings, errors + } +} + +type urlParseFlags int + +const ( + urlIsAbs urlParseFlags = 1 << iota + urlWithoutSchema + urlWithoutPort +) + +const urlBasicCheck urlParseFlags = 0 + +func validateHTTPURL(attrName schemaAttr, checkFlags urlParseFlags) func(v interface{}, key string) (warnings []string, errors []error) { + return func(v interface{}, key string) (warnings []string, errors []error) { + u, err := url.Parse(v.(string)) + switch { + case err != nil: + errors = append(errors, errwrap.Wrapf(fmt.Sprintf("Invalid %s specified (%q): {{err}}", attrName, v.(string)), err)) + case u.Host == "": + errors = append(errors, fmt.Errorf("Invalid %s specified: host can not be empty", attrName)) + case !(u.Scheme == "http" || u.Scheme == "https"): + errors = append(errors, fmt.Errorf("Invalid %s specified: scheme unsupported (only support http and https)", attrName)) + } + + if checkFlags&urlIsAbs != 0 && !u.IsAbs() { + errors = append(errors, fmt.Errorf("Schema is missing from URL %q (HINT: https://%s)", v.(string), v.(string))) + } + + if checkFlags&urlWithoutSchema != 0 && u.IsAbs() { + errors = append(errors, fmt.Errorf("Schema is present on URL %q (HINT: drop the https://%s)", v.(string), v.(string))) + } + + if checkFlags&urlWithoutPort != 0 { + hostParts := strings.SplitN(u.Host, ":", 2) + if len(hostParts) != 1 { + errors = append(errors, fmt.Errorf("Port is present on URL %q (HINT: drop the :%s)", v.(string), hostParts[1])) + } + } + + return warnings, errors + } +} + +func validateStringIn(attrName schemaAttr, valid validStringValues) func(v interface{}, key string) (warnings []string, errors []error) { + return func(v interface{}, key string) (warnings []string, errors []error) { + s := v.(string) + var found bool + for i := range valid { + if s == string(valid[i]) { + found = true + break + } + } + + if !found { + errors = append(errors, fmt.Errorf("Invalid %q specified: %q not found in list %#v", string(attrName), s, valid)) + } + + return warnings, errors + } +} diff --git a/command/internal_plugin_list.go b/command/internal_plugin_list.go index 82e2f1df5..5032a756d 100644 --- a/command/internal_plugin_list.go +++ b/command/internal_plugin_list.go @@ -15,6 +15,7 @@ import ( azurermprovider "github.com/hashicorp/terraform/builtin/providers/azurerm" bitbucketprovider "github.com/hashicorp/terraform/builtin/providers/bitbucket" chefprovider "github.com/hashicorp/terraform/builtin/providers/chef" + circonusprovider "github.com/hashicorp/terraform/builtin/providers/circonus" clcprovider "github.com/hashicorp/terraform/builtin/providers/clc" cloudflareprovider "github.com/hashicorp/terraform/builtin/providers/cloudflare" cloudstackprovider "github.com/hashicorp/terraform/builtin/providers/cloudstack" @@ -89,6 +90,7 @@ var InternalProviders = map[string]plugin.ProviderFunc{ "azurerm": azurermprovider.Provider, "bitbucket": bitbucketprovider.Provider, "chef": chefprovider.Provider, + "circonus": circonusprovider.Provider, "clc": clcprovider.Provider, "cloudflare": cloudflareprovider.Provider, "cloudstack": cloudstackprovider.Provider, diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/LICENSE b/vendor/github.com/circonus-labs/circonus-gometrics/LICENSE new file mode 100644 index 000000000..761798c3b --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2016, Circonus, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name Circonus, Inc. nor the names + of its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/README.md b/vendor/github.com/circonus-labs/circonus-gometrics/api/README.md new file mode 100644 index 000000000..8f286b79f --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/README.md @@ -0,0 +1,163 @@ +## Circonus API package + +Full api documentation (for using *this* package) is available at [godoc.org](https://godoc.org/github.com/circonus-labs/circonus-gometrics/api). Links in the lists below go directly to the generic Circonus API documentation for the endpoint. + +### Straight [raw] API access + +* Get +* Post (for creates) +* Put (for updates) +* Delete + +### Helpers for currently supported API endpoints + +> Note, these interfaces are still being actively developed. For example, many of the `New*` methods only return an empty struct; sensible defaults will be added going forward. Other, common helper methods for the various endpoints may be added as use cases emerge. The organization +of the API may change if common use contexts would benefit significantly. + +* [Account](https://login.circonus.com/resources/api/calls/account) + * FetchAccount + * FetchAccounts + * UpdateAccount + * SearchAccounts +* [Acknowledgement](https://login.circonus.com/resources/api/calls/acknowledgement) + * NewAcknowledgement + * FetchAcknowledgement + * FetchAcknowledgements + * UpdateAcknowledgement + * CreateAcknowledgement + * DeleteAcknowledgement + * DeleteAcknowledgementByCID + * SearchAcknowledgements +* [Alert](https://login.circonus.com/resources/api/calls/alert) + * FetchAlert + * FetchAlerts + * SearchAlerts +* [Annotation](https://login.circonus.com/resources/api/calls/annotation) + * NewAnnotation + * FetchAnnotation + * FetchAnnotations + * UpdateAnnotation + * CreateAnnotation + * DeleteAnnotation + * DeleteAnnotationByCID + * SearchAnnotations +* [Broker](https://login.circonus.com/resources/api/calls/broker) + * FetchBroker + * FetchBrokers + * SearchBrokers +* [Check Bundle](https://login.circonus.com/resources/api/calls/check_bundle) + * NewCheckBundle + * FetchCheckBundle + * FetchCheckBundles + * UpdateCheckBundle + * CreateCheckBundle + * DeleteCheckBundle + * DeleteCheckBundleByCID + * SearchCheckBundles +* [Check Bundle Metrics](https://login.circonus.com/resources/api/calls/check_bundle_metrics) + * FetchCheckBundleMetrics + * UpdateCheckBundleMetrics +* [Check](https://login.circonus.com/resources/api/calls/check) + * FetchCheck + * FetchChecks + * SearchChecks +* [Contact Group](https://login.circonus.com/resources/api/calls/contact_group) + * NewContactGroup + * FetchContactGroup + * FetchContactGroups + * UpdateContactGroup + * CreateContactGroup + * DeleteContactGroup + * DeleteContactGroupByCID + * SearchContactGroups +* [Dashboard](https://login.circonus.com/resources/api/calls/dashboard) -- note, this is a work in progress, the methods/types may still change + * NewDashboard + * FetchDashboard + * FetchDashboards + * UpdateDashboard + * CreateDashboard + * DeleteDashboard + * DeleteDashboardByCID + * SearchDashboards +* [Graph](https://login.circonus.com/resources/api/calls/graph) + * NewGraph + * FetchGraph + * FetchGraphs + * UpdateGraph + * CreateGraph + * DeleteGraph + * DeleteGraphByCID + * SearchGraphs +* [Metric Cluster](https://login.circonus.com/resources/api/calls/metric_cluster) + * NewMetricCluster + * FetchMetricCluster + * FetchMetricClusters + * UpdateMetricCluster + * CreateMetricCluster + * DeleteMetricCluster + * DeleteMetricClusterByCID + * SearchMetricClusters +* [Metric](https://login.circonus.com/resources/api/calls/metric) + * FetchMetric + * FetchMetrics + * UpdateMetric + * SearchMetrics +* [Maintenance window](https://login.circonus.com/resources/api/calls/maintenance) + * NewMaintenanceWindow + * FetchMaintenanceWindow + * FetchMaintenanceWindows + * UpdateMaintenanceWindow + * CreateMaintenanceWindow + * DeleteMaintenanceWindow + * DeleteMaintenanceWindowByCID + * SearchMaintenanceWindows +* [Outlier Report](https://login.circonus.com/resources/api/calls/outlier_report) + * NewOutlierReport + * FetchOutlierReport + * FetchOutlierReports + * UpdateOutlierReport + * CreateOutlierReport + * DeleteOutlierReport + * DeleteOutlierReportByCID + * SearchOutlierReports +* [Provision Broker](https://login.circonus.com/resources/api/calls/provision_broker) + * NewProvisionBroker + * FetchProvisionBroker + * UpdateProvisionBroker + * CreateProvisionBroker +* [Rule Set](https://login.circonus.com/resources/api/calls/rule_set) + * NewRuleset + * FetchRuleset + * FetchRulesets + * UpdateRuleset + * CreateRuleset + * DeleteRuleset + * DeleteRulesetByCID + * SearchRulesets +* [Rule Set Group](https://login.circonus.com/resources/api/calls/rule_set_group) + * NewRulesetGroup + * FetchRulesetGroup + * FetchRulesetGroups + * UpdateRulesetGroup + * CreateRulesetGroup + * DeleteRulesetGroup + * DeleteRulesetGroupByCID + * SearchRulesetGroups +* [User](https://login.circonus.com/resources/api/calls/user) + * FetchUser + * FetchUsers + * UpdateUser + * SearchUsers +* [Worksheet](https://login.circonus.com/resources/api/calls/worksheet) + * NewWorksheet + * FetchWorksheet + * FetchWorksheets + * UpdateWorksheet + * CreateWorksheet + * DeleteWorksheet + * DeleteWorksheetByCID + * SearchWorksheets + +--- + +Unless otherwise noted, the source files are distributed under the BSD-style license found in the LICENSE file. diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/account.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/account.go new file mode 100644 index 000000000..dd8ff577d --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/account.go @@ -0,0 +1,181 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Account API support - Fetch and Update +// See: https://login.circonus.com/resources/api/calls/account +// Note: Create and Delete are not supported for Accounts via the API + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// AccountLimit defines a usage limit imposed on account +type AccountLimit struct { + Limit uint `json:"_limit,omitempty"` // uint >=0 + Type string `json:"_type,omitempty"` // string + Used uint `json:"_used,omitempty"` // uint >=0 +} + +// AccountInvite defines outstanding invites +type AccountInvite struct { + Email string `json:"email"` // string + Role string `json:"role"` // string +} + +// AccountUser defines current users +type AccountUser struct { + Role string `json:"role"` // string + UserCID string `json:"user"` // string +} + +// Account defines an account. See https://login.circonus.com/resources/api/calls/account for more information. +type Account struct { + Address1 *string `json:"address1,omitempty"` // string or null + Address2 *string `json:"address2,omitempty"` // string or null + CCEmail *string `json:"cc_email,omitempty"` // string or null + CID string `json:"_cid,omitempty"` // string + City *string `json:"city,omitempty"` // string or null + ContactGroups []string `json:"_contact_groups,omitempty"` // [] len >= 0 + Country string `json:"country_code,omitempty"` // string + Description *string `json:"description,omitempty"` // string or null + Invites []AccountInvite `json:"invites,omitempty"` // [] len >= 0 + Name string `json:"name,omitempty"` // string + OwnerCID string `json:"_owner,omitempty"` // string + StateProv *string `json:"state_prov,omitempty"` // string or null + Timezone string `json:"timezone,omitempty"` // string + UIBaseURL string `json:"_ui_base_url,omitempty"` // string + Usage []AccountLimit `json:"_usage,omitempty"` // [] len >= 0 + Users []AccountUser `json:"users,omitempty"` // [] len >= 0 +} + +// FetchAccount retrieves account with passed cid. Pass nil for '/account/current'. +func (a *API) FetchAccount(cid CIDType) (*Account, error) { + var accountCID string + + if cid == nil || *cid == "" { + accountCID = config.AccountPrefix + "/current" + } else { + accountCID = string(*cid) + } + + matched, err := regexp.MatchString(config.AccountCIDRegex, accountCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid account CID [%s]", accountCID) + } + + result, err := a.Get(accountCID) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] account fetch, received JSON: %s", string(result)) + } + + account := new(Account) + if err := json.Unmarshal(result, account); err != nil { + return nil, err + } + + return account, nil +} + +// FetchAccounts retrieves all accounts available to the API Token. +func (a *API) FetchAccounts() (*[]Account, error) { + result, err := a.Get(config.AccountPrefix) + if err != nil { + return nil, err + } + + var accounts []Account + if err := json.Unmarshal(result, &accounts); err != nil { + return nil, err + } + + return &accounts, nil +} + +// UpdateAccount updates passed account. +func (a *API) UpdateAccount(cfg *Account) (*Account, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid account config [nil]") + } + + accountCID := string(cfg.CID) + + matched, err := regexp.MatchString(config.AccountCIDRegex, accountCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid account CID [%s]", accountCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] account update, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(accountCID, jsonCfg) + if err != nil { + return nil, err + } + + account := &Account{} + if err := json.Unmarshal(result, account); err != nil { + return nil, err + } + + return account, nil +} + +// SearchAccounts returns accounts matching a filter (search queries are not +// suppoted by the account endpoint). Pass nil as filter for all accounts the +// API Token can access. +func (a *API) SearchAccounts(filterCriteria *SearchFilterType) (*[]Account, error) { + q := url.Values{} + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchAccounts() + } + + reqURL := url.URL{ + Path: config.AccountPrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var accounts []Account + if err := json.Unmarshal(result, &accounts); err != nil { + return nil, err + } + + return &accounts, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/acknowledgement.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/acknowledgement.go new file mode 100644 index 000000000..f6da51d4d --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/acknowledgement.go @@ -0,0 +1,190 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Acknowledgement API support - Fetch, Create, Update, Delete*, and Search +// See: https://login.circonus.com/resources/api/calls/acknowledgement +// * : delete (cancel) by updating with AcknowledgedUntil set to 0 + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// Acknowledgement defines a acknowledgement. See https://login.circonus.com/resources/api/calls/acknowledgement for more information. +type Acknowledgement struct { + AcknowledgedBy string `json:"_acknowledged_by,omitempty"` // string + AcknowledgedOn uint `json:"_acknowledged_on,omitempty"` // uint + AcknowledgedUntil interface{} `json:"acknowledged_until,omitempty"` // NOTE received as uint; can be set using string or uint + Active bool `json:"_active,omitempty"` // bool + AlertCID string `json:"alert,omitempty"` // string + CID string `json:"_cid,omitempty"` // string + LastModified uint `json:"_last_modified,omitempty"` // uint + LastModifiedBy string `json:"_last_modified_by,omitempty"` // string + Notes string `json:"notes,omitempty"` // string +} + +// NewAcknowledgement returns new Acknowledgement (with defaults, if applicable). +func NewAcknowledgement() *Acknowledgement { + return &Acknowledgement{} +} + +// FetchAcknowledgement retrieves acknowledgement with passed cid. +func (a *API) FetchAcknowledgement(cid CIDType) (*Acknowledgement, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid acknowledgement CID [none]") + } + + acknowledgementCID := string(*cid) + + matched, err := regexp.MatchString(config.AcknowledgementCIDRegex, acknowledgementCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid acknowledgement CID [%s]", acknowledgementCID) + } + + result, err := a.Get(acknowledgementCID) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] acknowledgement fetch, received JSON: %s", string(result)) + } + + acknowledgement := &Acknowledgement{} + if err := json.Unmarshal(result, acknowledgement); err != nil { + return nil, err + } + + return acknowledgement, nil +} + +// FetchAcknowledgements retrieves all acknowledgements available to the API Token. +func (a *API) FetchAcknowledgements() (*[]Acknowledgement, error) { + result, err := a.Get(config.AcknowledgementPrefix) + if err != nil { + return nil, err + } + + var acknowledgements []Acknowledgement + if err := json.Unmarshal(result, &acknowledgements); err != nil { + return nil, err + } + + return &acknowledgements, nil +} + +// UpdateAcknowledgement updates passed acknowledgement. +func (a *API) UpdateAcknowledgement(cfg *Acknowledgement) (*Acknowledgement, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid acknowledgement config [nil]") + } + + acknowledgementCID := string(cfg.CID) + + matched, err := regexp.MatchString(config.AcknowledgementCIDRegex, acknowledgementCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid acknowledgement CID [%s]", acknowledgementCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] acknowledgement update, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(acknowledgementCID, jsonCfg) + if err != nil { + return nil, err + } + + acknowledgement := &Acknowledgement{} + if err := json.Unmarshal(result, acknowledgement); err != nil { + return nil, err + } + + return acknowledgement, nil +} + +// CreateAcknowledgement creates a new acknowledgement. +func (a *API) CreateAcknowledgement(cfg *Acknowledgement) (*Acknowledgement, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid acknowledgement config [nil]") + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + result, err := a.Post(config.AcknowledgementPrefix, jsonCfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] acknowledgement create, sending JSON: %s", string(jsonCfg)) + } + + acknowledgement := &Acknowledgement{} + if err := json.Unmarshal(result, acknowledgement); err != nil { + return nil, err + } + + return acknowledgement, nil +} + +// SearchAcknowledgements returns acknowledgements matching +// the specified search query and/or filter. If nil is passed for +// both parameters all acknowledgements will be returned. +func (a *API) SearchAcknowledgements(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]Acknowledgement, error) { + q := url.Values{} + + if searchCriteria != nil && *searchCriteria != "" { + q.Set("search", string(*searchCriteria)) + } + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchAcknowledgements() + } + + reqURL := url.URL{ + Path: config.AcknowledgementPrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var acknowledgements []Acknowledgement + if err := json.Unmarshal(result, &acknowledgements); err != nil { + return nil, err + } + + return &acknowledgements, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/alert.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/alert.go new file mode 100644 index 000000000..a242d3d85 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/alert.go @@ -0,0 +1,131 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Alert API support - Fetch and Search +// See: https://login.circonus.com/resources/api/calls/alert + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// Alert defines a alert. See https://login.circonus.com/resources/api/calls/alert for more information. +type Alert struct { + AcknowledgementCID *string `json:"_acknowledgement,omitempty"` // string or null + AlertURL string `json:"_alert_url,omitempty"` // string + BrokerCID string `json:"_broker,omitempty"` // string + CheckCID string `json:"_check,omitempty"` // string + CheckName string `json:"_check_name,omitempty"` // string + CID string `json:"_cid,omitempty"` // string + ClearedOn *uint `json:"_cleared_on,omitempty"` // uint or null + ClearedValue *string `json:"_cleared_value,omitempty"` // string or null + Maintenance []string `json:"_maintenance,omitempty"` // [] len >= 0 + MetricLinkURL *string `json:"_metric_link,omitempty"` // string or null + MetricName string `json:"_metric_name,omitempty"` // string + MetricNotes *string `json:"_metric_notes,omitempty"` // string or null + OccurredOn uint `json:"_occurred_on,omitempty"` // uint + RuleSetCID string `json:"_rule_set,omitempty"` // string + Severity uint `json:"_severity,omitempty"` // uint + Tags []string `json:"_tags,omitempty"` // [] len >= 0 + Value string `json:"_value,omitempty"` // string +} + +// NewAlert returns a new alert (with defaults, if applicable) +func NewAlert() *Alert { + return &Alert{} +} + +// FetchAlert retrieves alert with passed cid. +func (a *API) FetchAlert(cid CIDType) (*Alert, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid alert CID [none]") + } + + alertCID := string(*cid) + + matched, err := regexp.MatchString(config.AlertCIDRegex, alertCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid alert CID [%s]", alertCID) + } + + result, err := a.Get(alertCID) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch alert, received JSON: %s", string(result)) + } + + alert := &Alert{} + if err := json.Unmarshal(result, alert); err != nil { + return nil, err + } + + return alert, nil +} + +// FetchAlerts retrieves all alerts available to the API Token. +func (a *API) FetchAlerts() (*[]Alert, error) { + result, err := a.Get(config.AlertPrefix) + if err != nil { + return nil, err + } + + var alerts []Alert + if err := json.Unmarshal(result, &alerts); err != nil { + return nil, err + } + + return &alerts, nil +} + +// SearchAlerts returns alerts matching the specified search query +// and/or filter. If nil is passed for both parameters all alerts +// will be returned. +func (a *API) SearchAlerts(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]Alert, error) { + q := url.Values{} + + if searchCriteria != nil && *searchCriteria != "" { + q.Set("search", string(*searchCriteria)) + } + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchAlerts() + } + + reqURL := url.URL{ + Path: config.AlertPrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var alerts []Alert + if err := json.Unmarshal(result, &alerts); err != nil { + return nil, err + } + + return &alerts, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/annotation.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/annotation.go new file mode 100644 index 000000000..589ec6da9 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/annotation.go @@ -0,0 +1,223 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Annotation API support - Fetch, Create, Update, Delete, and Search +// See: https://login.circonus.com/resources/api/calls/annotation + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// Annotation defines a annotation. See https://login.circonus.com/resources/api/calls/annotation for more information. +type Annotation struct { + Category string `json:"category"` // string + CID string `json:"_cid,omitempty"` // string + Created uint `json:"_created,omitempty"` // uint + Description string `json:"description"` // string + LastModified uint `json:"_last_modified,omitempty"` // uint + LastModifiedBy string `json:"_last_modified_by,omitempty"` // string + RelatedMetrics []string `json:"rel_metrics"` // [] len >= 0 + Start uint `json:"start"` // uint + Stop uint `json:"stop"` // uint + Title string `json:"title"` // string +} + +// NewAnnotation returns a new Annotation (with defaults, if applicable) +func NewAnnotation() *Annotation { + return &Annotation{} +} + +// FetchAnnotation retrieves annotation with passed cid. +func (a *API) FetchAnnotation(cid CIDType) (*Annotation, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid annotation CID [none]") + } + + annotationCID := string(*cid) + + matched, err := regexp.MatchString(config.AnnotationCIDRegex, annotationCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid annotation CID [%s]", annotationCID) + } + + result, err := a.Get(annotationCID) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch annotation, received JSON: %s", string(result)) + } + + annotation := &Annotation{} + if err := json.Unmarshal(result, annotation); err != nil { + return nil, err + } + + return annotation, nil +} + +// FetchAnnotations retrieves all annotations available to the API Token. +func (a *API) FetchAnnotations() (*[]Annotation, error) { + result, err := a.Get(config.AnnotationPrefix) + if err != nil { + return nil, err + } + + var annotations []Annotation + if err := json.Unmarshal(result, &annotations); err != nil { + return nil, err + } + + return &annotations, nil +} + +// UpdateAnnotation updates passed annotation. +func (a *API) UpdateAnnotation(cfg *Annotation) (*Annotation, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid annotation config [nil]") + } + + annotationCID := string(cfg.CID) + + matched, err := regexp.MatchString(config.AnnotationCIDRegex, annotationCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid annotation CID [%s]", annotationCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] update annotation, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(annotationCID, jsonCfg) + if err != nil { + return nil, err + } + + annotation := &Annotation{} + if err := json.Unmarshal(result, annotation); err != nil { + return nil, err + } + + return annotation, nil +} + +// CreateAnnotation creates a new annotation. +func (a *API) CreateAnnotation(cfg *Annotation) (*Annotation, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid annotation config [nil]") + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] create annotation, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Post(config.AnnotationPrefix, jsonCfg) + if err != nil { + return nil, err + } + + annotation := &Annotation{} + if err := json.Unmarshal(result, annotation); err != nil { + return nil, err + } + + return annotation, nil +} + +// DeleteAnnotation deletes passed annotation. +func (a *API) DeleteAnnotation(cfg *Annotation) (bool, error) { + if cfg == nil { + return false, fmt.Errorf("Invalid annotation config [nil]") + } + + return a.DeleteAnnotationByCID(CIDType(&cfg.CID)) +} + +// DeleteAnnotationByCID deletes annotation with passed cid. +func (a *API) DeleteAnnotationByCID(cid CIDType) (bool, error) { + if cid == nil || *cid == "" { + return false, fmt.Errorf("Invalid annotation CID [none]") + } + + annotationCID := string(*cid) + + matched, err := regexp.MatchString(config.AnnotationCIDRegex, annotationCID) + if err != nil { + return false, err + } + if !matched { + return false, fmt.Errorf("Invalid annotation CID [%s]", annotationCID) + } + + _, err = a.Delete(annotationCID) + if err != nil { + return false, err + } + + return true, nil +} + +// SearchAnnotations returns annotations matching the specified +// search query and/or filter. If nil is passed for both parameters +// all annotations will be returned. +func (a *API) SearchAnnotations(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]Annotation, error) { + q := url.Values{} + + if searchCriteria != nil && *searchCriteria != "" { + q.Set("search", string(*searchCriteria)) + } + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchAnnotations() + } + + reqURL := url.URL{ + Path: config.AnnotationPrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var annotations []Annotation + if err := json.Unmarshal(result, &annotations); err != nil { + return nil, err + } + + return &annotations, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/api.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/api.go new file mode 100644 index 000000000..b9265aa7e --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/api.go @@ -0,0 +1,371 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package api + +import ( + "bytes" + crand "crypto/rand" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io/ioutil" + "log" + "math" + "math/big" + "math/rand" + "net" + "net/http" + "net/url" + "os" + "regexp" + "strings" + "sync" + "time" + + "github.com/hashicorp/go-retryablehttp" +) + +func init() { + n, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + rand.Seed(time.Now().UTC().UnixNano()) + return + } + rand.Seed(n.Int64()) +} + +const ( + // a few sensible defaults + defaultAPIURL = "https://api.circonus.com/v2" + defaultAPIApp = "circonus-gometrics" + minRetryWait = 1 * time.Second + maxRetryWait = 15 * time.Second + maxRetries = 4 // equating to 1 + maxRetries total attempts +) + +// TokenKeyType - Circonus API Token key +type TokenKeyType string + +// TokenAppType - Circonus API Token app name +type TokenAppType string + +// CIDType Circonus object cid +type CIDType *string + +// IDType Circonus object id +type IDType int + +// URLType submission url type +type URLType string + +// SearchQueryType search query (see: https://login.circonus.com/resources/api#searching) +type SearchQueryType string + +// SearchFilterType search filter (see: https://login.circonus.com/resources/api#filtering) +type SearchFilterType map[string][]string + +// TagType search/select/custom tag(s) type +type TagType []string + +// Config options for Circonus API +type Config struct { + URL string + TokenKey string + TokenApp string + CACert *x509.CertPool + Log *log.Logger + Debug bool +} + +// API Circonus API +type API struct { + apiURL *url.URL + key TokenKeyType + app TokenAppType + caCert *x509.CertPool + Debug bool + Log *log.Logger + useExponentialBackoff bool + useExponentialBackoffmu sync.Mutex +} + +// NewClient returns a new Circonus API (alias for New) +func NewClient(ac *Config) (*API, error) { + return New(ac) +} + +// NewAPI returns a new Circonus API (alias for New) +func NewAPI(ac *Config) (*API, error) { + return New(ac) +} + +// New returns a new Circonus API +func New(ac *Config) (*API, error) { + + if ac == nil { + return nil, errors.New("Invalid API configuration (nil)") + } + + key := TokenKeyType(ac.TokenKey) + if key == "" { + return nil, errors.New("API Token is required") + } + + app := TokenAppType(ac.TokenApp) + if app == "" { + app = defaultAPIApp + } + + au := string(ac.URL) + if au == "" { + au = defaultAPIURL + } + if !strings.Contains(au, "/") { + // if just a hostname is passed, ASSume "https" and a path prefix of "/v2" + au = fmt.Sprintf("https://%s/v2", ac.URL) + } + if last := len(au) - 1; last >= 0 && au[last] == '/' { + // strip off trailing '/' + au = au[:last] + } + apiURL, err := url.Parse(au) + if err != nil { + return nil, err + } + + a := &API{ + apiURL: apiURL, + key: key, + app: app, + caCert: ac.CACert, + Debug: ac.Debug, + Log: ac.Log, + useExponentialBackoff: false, + } + + a.Debug = ac.Debug + a.Log = ac.Log + if a.Debug && a.Log == nil { + a.Log = log.New(os.Stderr, "", log.LstdFlags) + } + if a.Log == nil { + a.Log = log.New(ioutil.Discard, "", log.LstdFlags) + } + + return a, nil +} + +// EnableExponentialBackoff enables use of exponential backoff for next API call(s) +// and use exponential backoff for all API calls until exponential backoff is disabled. +func (a *API) EnableExponentialBackoff() { + a.useExponentialBackoffmu.Lock() + a.useExponentialBackoff = true + a.useExponentialBackoffmu.Unlock() +} + +// DisableExponentialBackoff disables use of exponential backoff. If a request using +// exponential backoff is currently running, it will stop using exponential backoff +// on its next iteration (if needed). +func (a *API) DisableExponentialBackoff() { + a.useExponentialBackoffmu.Lock() + a.useExponentialBackoff = false + a.useExponentialBackoffmu.Unlock() +} + +// Get API request +func (a *API) Get(reqPath string) ([]byte, error) { + return a.apiRequest("GET", reqPath, nil) +} + +// Delete API request +func (a *API) Delete(reqPath string) ([]byte, error) { + return a.apiRequest("DELETE", reqPath, nil) +} + +// Post API request +func (a *API) Post(reqPath string, data []byte) ([]byte, error) { + return a.apiRequest("POST", reqPath, data) +} + +// Put API request +func (a *API) Put(reqPath string, data []byte) ([]byte, error) { + return a.apiRequest("PUT", reqPath, data) +} + +func backoff(interval uint) float64 { + return math.Floor(((float64(interval) * (1 + rand.Float64())) / 2) + .5) +} + +// apiRequest manages retry strategy for exponential backoffs +func (a *API) apiRequest(reqMethod string, reqPath string, data []byte) ([]byte, error) { + backoffs := []uint{2, 4, 8, 16, 32} + attempts := 0 + success := false + + var result []byte + var err error + + for !success { + result, err = a.apiCall(reqMethod, reqPath, data) + if err == nil { + success = true + } + + // break and return error if not using exponential backoff + if err != nil { + if !a.useExponentialBackoff { + break + } + if matched, _ := regexp.MatchString("code 403", err.Error()); matched { + break + } + } + + if !success { + var wait float64 + if attempts >= len(backoffs) { + wait = backoff(backoffs[len(backoffs)-1]) + } else { + wait = backoff(backoffs[attempts]) + } + attempts++ + a.Log.Printf("[WARN] API call failed %s, retrying in %d seconds.\n", err.Error(), uint(wait)) + time.Sleep(time.Duration(wait) * time.Second) + } + } + + return result, err +} + +// apiCall call Circonus API +func (a *API) apiCall(reqMethod string, reqPath string, data []byte) ([]byte, error) { + reqURL := a.apiURL.String() + + if reqPath == "" { + return nil, errors.New("Invalid URL path") + } + if reqPath[:1] != "/" { + reqURL += "/" + } + if len(reqPath) >= 3 && reqPath[:3] == "/v2" { + reqURL += reqPath[3:] + } else { + reqURL += reqPath + } + + // keep last HTTP error in the event of retry failure + var lastHTTPError error + retryPolicy := func(resp *http.Response, err error) (bool, error) { + if err != nil { + lastHTTPError = err + return true, err + } + // Check the response code. We retry on 500-range responses to allow + // the server time to recover, as 500's are typically not permanent + // errors and may relate to outages on the server side. This will catch + // invalid response codes as well, like 0 and 999. + // Retry on 429 (rate limit) as well. + if resp.StatusCode == 0 || // wtf?! + resp.StatusCode >= 500 || // rutroh + resp.StatusCode == 429 { // rate limit + body, readErr := ioutil.ReadAll(resp.Body) + if readErr != nil { + lastHTTPError = fmt.Errorf("- response: %d %s", resp.StatusCode, readErr.Error()) + } else { + lastHTTPError = fmt.Errorf("- response: %d %s", resp.StatusCode, strings.TrimSpace(string(body))) + } + return true, nil + } + return false, nil + } + + dataReader := bytes.NewReader(data) + + req, err := retryablehttp.NewRequest(reqMethod, reqURL, dataReader) + if err != nil { + return nil, fmt.Errorf("[ERROR] creating API request: %s %+v", reqURL, err) + } + req.Header.Add("Accept", "application/json") + req.Header.Add("X-Circonus-Auth-Token", string(a.key)) + req.Header.Add("X-Circonus-App-Name", string(a.app)) + + client := retryablehttp.NewClient() + if a.apiURL.Scheme == "https" && a.caCert != nil { + client.HTTPClient.Transport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: &tls.Config{RootCAs: a.caCert}, + DisableKeepAlives: true, + MaxIdleConnsPerHost: -1, + DisableCompression: true, + } + } else { + client.HTTPClient.Transport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + DisableKeepAlives: true, + MaxIdleConnsPerHost: -1, + DisableCompression: true, + } + } + + a.useExponentialBackoffmu.Lock() + eb := a.useExponentialBackoff + a.useExponentialBackoffmu.Unlock() + + if eb { + // limit to one request if using exponential backoff + client.RetryWaitMin = 1 + client.RetryWaitMax = 2 + client.RetryMax = 0 + } else { + client.RetryWaitMin = minRetryWait + client.RetryWaitMax = maxRetryWait + client.RetryMax = maxRetries + } + + // retryablehttp only groks log or no log + if a.Debug { + client.Logger = a.Log + } else { + client.Logger = log.New(ioutil.Discard, "", log.LstdFlags) + } + + client.CheckRetry = retryPolicy + + resp, err := client.Do(req) + if err != nil { + if lastHTTPError != nil { + return nil, lastHTTPError + } + return nil, fmt.Errorf("[ERROR] %s: %+v", reqURL, err) + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("[ERROR] reading response %+v", err) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + msg := fmt.Sprintf("API response code %d: %s", resp.StatusCode, string(body)) + if a.Debug { + a.Log.Printf("[DEBUG] %s\n", msg) + } + + return nil, fmt.Errorf("[ERROR] %s", msg) + } + + return body, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/broker.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/broker.go new file mode 100644 index 000000000..459fda6df --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/broker.go @@ -0,0 +1,131 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Broker API support - Fetch and Search +// See: https://login.circonus.com/resources/api/calls/broker + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// BrokerDetail defines instance attributes +type BrokerDetail struct { + CN string `json:"cn"` // string + ExternalHost *string `json:"external_host"` // string or null + ExternalPort uint16 `json:"external_port"` // uint16 + IP *string `json:"ipaddress"` // string or null + MinVer uint `json:"minimum_version_required"` // uint + Modules []string `json:"modules"` // [] len >= 0 + Port *uint16 `json:"port"` // uint16 or null + Skew *string `json:"skew"` // BUG doc: floating point number, api object: string or null + Status string `json:"status"` // string + Version *uint `json:"version"` // uint or null +} + +// Broker defines a broker. See https://login.circonus.com/resources/api/calls/broker for more information. +type Broker struct { + CID string `json:"_cid"` // string + Details []BrokerDetail `json:"_details"` // [] len >= 1 + Latitude *string `json:"_latitude"` // string or null + Longitude *string `json:"_longitude"` // string or null + Name string `json:"_name"` // string + Tags []string `json:"_tags"` // [] len >= 0 + Type string `json:"_type"` // string +} + +// FetchBroker retrieves broker with passed cid. +func (a *API) FetchBroker(cid CIDType) (*Broker, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid broker CID [none]") + } + + brokerCID := string(*cid) + + matched, err := regexp.MatchString(config.BrokerCIDRegex, brokerCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid broker CID [%s]", brokerCID) + } + + result, err := a.Get(brokerCID) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch broker, received JSON: %s", string(result)) + } + + response := new(Broker) + if err := json.Unmarshal(result, &response); err != nil { + return nil, err + } + + return response, nil + +} + +// FetchBrokers returns all brokers available to the API Token. +func (a *API) FetchBrokers() (*[]Broker, error) { + result, err := a.Get(config.BrokerPrefix) + if err != nil { + return nil, err + } + + var response []Broker + if err := json.Unmarshal(result, &response); err != nil { + return nil, err + } + + return &response, nil +} + +// SearchBrokers returns brokers matching the specified search +// query and/or filter. If nil is passed for both parameters +// all brokers will be returned. +func (a *API) SearchBrokers(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]Broker, error) { + q := url.Values{} + + if searchCriteria != nil && *searchCriteria != "" { + q.Set("search", string(*searchCriteria)) + } + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchBrokers() + } + + reqURL := url.URL{ + Path: config.BrokerPrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var brokers []Broker + if err := json.Unmarshal(result, &brokers); err != nil { + return nil, err + } + + return &brokers, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/check.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/check.go new file mode 100644 index 000000000..047d71935 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/check.go @@ -0,0 +1,119 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Check API support - Fetch and Search +// See: https://login.circonus.com/resources/api/calls/check +// Notes: checks do not directly support create, update, and delete - see check bundle. + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// CheckDetails contains [undocumented] check type specific information +type CheckDetails map[config.Key]string + +// Check defines a check. See https://login.circonus.com/resources/api/calls/check for more information. +type Check struct { + Active bool `json:"_active"` // bool + BrokerCID string `json:"_broker"` // string + CheckBundleCID string `json:"_check_bundle"` // string + CheckUUID string `json:"_check_uuid"` // string + CID string `json:"_cid"` // string + Details CheckDetails `json:"_details"` // NOTE contents of details are check type specific, map len >= 0 +} + +// FetchCheck retrieves check with passed cid. +func (a *API) FetchCheck(cid CIDType) (*Check, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid check CID [none]") + } + + checkCID := string(*cid) + + matched, err := regexp.MatchString(config.CheckCIDRegex, checkCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid check CID [%s]", checkCID) + } + + result, err := a.Get(checkCID) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch check, received JSON: %s", string(result)) + } + + check := new(Check) + if err := json.Unmarshal(result, check); err != nil { + return nil, err + } + + return check, nil +} + +// FetchChecks retrieves all checks available to the API Token. +func (a *API) FetchChecks() (*[]Check, error) { + result, err := a.Get(config.CheckPrefix) + if err != nil { + return nil, err + } + + var checks []Check + if err := json.Unmarshal(result, &checks); err != nil { + return nil, err + } + + return &checks, nil +} + +// SearchChecks returns checks matching the specified search query +// and/or filter. If nil is passed for both parameters all checks +// will be returned. +func (a *API) SearchChecks(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]Check, error) { + q := url.Values{} + + if searchCriteria != nil && *searchCriteria != "" { + q.Set("search", string(*searchCriteria)) + } + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchChecks() + } + + reqURL := url.URL{ + Path: config.CheckPrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, err + } + + var checks []Check + if err := json.Unmarshal(result, &checks); err != nil { + return nil, err + } + + return &checks, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/check_bundle.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/check_bundle.go new file mode 100644 index 000000000..8ab851e0c --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/check_bundle.go @@ -0,0 +1,255 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Check bundle API support - Fetch, Create, Update, Delete, and Search +// See: https://login.circonus.com/resources/api/calls/check_bundle + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// CheckBundleMetric individual metric configuration +type CheckBundleMetric struct { + Name string `json:"name"` // string + Result *string `json:"result,omitempty"` // string or null, NOTE not settable - return/information value only + Status string `json:"status,omitempty"` // string + Tags []string `json:"tags"` // [] len >= 0 + Type string `json:"type"` // string + Units *string `json:"units,omitempty"` // string or null + +} + +// CheckBundleConfig contains the check type specific configuration settings +// as k/v pairs (see https://login.circonus.com/resources/api/calls/check_bundle +// for the specific settings available for each distinct check type) +type CheckBundleConfig map[config.Key]string + +// CheckBundle defines a check bundle. See https://login.circonus.com/resources/api/calls/check_bundle for more information. +type CheckBundle struct { + Brokers []string `json:"brokers"` // [] len >= 0 + Checks []string `json:"_checks,omitempty"` // [] len >= 0 + CheckUUIDs []string `json:"_check_uuids,omitempty"` // [] len >= 0 + CID string `json:"_cid,omitempty"` // string + Config CheckBundleConfig `json:"config,omitempty"` // NOTE contents of config are check type specific, map len >= 0 + Created uint `json:"_created,omitempty"` // uint + DisplayName string `json:"display_name"` // string + LastModifedBy string `json:"_last_modifed_by,omitempty"` // string + LastModified uint `json:"_last_modified,omitempty"` // uint + MetricLimit int `json:"metric_limit,omitempty"` // int + Metrics []CheckBundleMetric `json:"metrics"` // [] >= 0 + Notes *string `json:"notes,omitempty"` // string or null + Period uint `json:"period,omitempty"` // uint + ReverseConnectURLs []string `json:"_reverse_connection_urls,omitempty"` // [] len >= 0 + Status string `json:"status,omitempty"` // string + Tags []string `json:"tags,omitempty"` // [] len >= 0 + Target string `json:"target"` // string + Timeout float32 `json:"timeout,omitempty"` // float32 + Type string `json:"type"` // string +} + +// NewCheckBundle returns new CheckBundle (with defaults, if applicable) +func NewCheckBundle() *CheckBundle { + return &CheckBundle{ + Config: make(CheckBundleConfig, config.DefaultConfigOptionsSize), + MetricLimit: config.DefaultCheckBundleMetricLimit, + Period: config.DefaultCheckBundlePeriod, + Timeout: config.DefaultCheckBundleTimeout, + Status: config.DefaultCheckBundleStatus, + } +} + +// FetchCheckBundle retrieves check bundle with passed cid. +func (a *API) FetchCheckBundle(cid CIDType) (*CheckBundle, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid check bundle CID [none]") + } + + bundleCID := string(*cid) + + matched, err := regexp.MatchString(config.CheckBundleCIDRegex, bundleCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid check bundle CID [%v]", bundleCID) + } + + result, err := a.Get(bundleCID) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch check bundle, received JSON: %s", string(result)) + } + + checkBundle := &CheckBundle{} + if err := json.Unmarshal(result, checkBundle); err != nil { + return nil, err + } + + return checkBundle, nil +} + +// FetchCheckBundles retrieves all check bundles available to the API Token. +func (a *API) FetchCheckBundles() (*[]CheckBundle, error) { + result, err := a.Get(config.CheckBundlePrefix) + if err != nil { + return nil, err + } + + var checkBundles []CheckBundle + if err := json.Unmarshal(result, &checkBundles); err != nil { + return nil, err + } + + return &checkBundles, nil +} + +// UpdateCheckBundle updates passed check bundle. +func (a *API) UpdateCheckBundle(cfg *CheckBundle) (*CheckBundle, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid check bundle config [nil]") + } + + bundleCID := string(cfg.CID) + + matched, err := regexp.MatchString(config.CheckBundleCIDRegex, bundleCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid check bundle CID [%s]", bundleCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] update check bundle, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(bundleCID, jsonCfg) + if err != nil { + return nil, err + } + + checkBundle := &CheckBundle{} + if err := json.Unmarshal(result, checkBundle); err != nil { + return nil, err + } + + return checkBundle, nil +} + +// CreateCheckBundle creates a new check bundle (check). +func (a *API) CreateCheckBundle(cfg *CheckBundle) (*CheckBundle, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid check bundle config [nil]") + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] create check bundle, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Post(config.CheckBundlePrefix, jsonCfg) + if err != nil { + return nil, err + } + + checkBundle := &CheckBundle{} + if err := json.Unmarshal(result, checkBundle); err != nil { + return nil, err + } + + return checkBundle, nil +} + +// DeleteCheckBundle deletes passed check bundle. +func (a *API) DeleteCheckBundle(cfg *CheckBundle) (bool, error) { + if cfg == nil { + return false, fmt.Errorf("Invalid check bundle config [nil]") + } + return a.DeleteCheckBundleByCID(CIDType(&cfg.CID)) +} + +// DeleteCheckBundleByCID deletes check bundle with passed cid. +func (a *API) DeleteCheckBundleByCID(cid CIDType) (bool, error) { + + if cid == nil || *cid == "" { + return false, fmt.Errorf("Invalid check bundle CID [none]") + } + + bundleCID := string(*cid) + + matched, err := regexp.MatchString(config.CheckBundleCIDRegex, bundleCID) + if err != nil { + return false, err + } + if !matched { + return false, fmt.Errorf("Invalid check bundle CID [%v]", bundleCID) + } + + _, err = a.Delete(bundleCID) + if err != nil { + return false, err + } + + return true, nil +} + +// SearchCheckBundles returns check bundles matching the specified +// search query and/or filter. If nil is passed for both parameters +// all check bundles will be returned. +func (a *API) SearchCheckBundles(searchCriteria *SearchQueryType, filterCriteria *map[string][]string) (*[]CheckBundle, error) { + + q := url.Values{} + + if searchCriteria != nil && *searchCriteria != "" { + q.Set("search", string(*searchCriteria)) + } + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchCheckBundles() + } + + reqURL := url.URL{ + Path: config.CheckBundlePrefix, + RawQuery: q.Encode(), + } + + resp, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var results []CheckBundle + if err := json.Unmarshal(resp, &results); err != nil { + return nil, err + } + + return &results, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/check_bundle_metrics.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/check_bundle_metrics.go new file mode 100644 index 000000000..817c7b891 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/check_bundle_metrics.go @@ -0,0 +1,95 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// CheckBundleMetrics API support - Fetch, Create*, Update, and Delete** +// See: https://login.circonus.com/resources/api/calls/check_bundle_metrics +// * : create metrics by adding to array with a status of 'active' +// ** : delete (distable collection of) metrics by changing status from 'active' to 'available' + +package api + +import ( + "encoding/json" + "fmt" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// CheckBundleMetrics defines metrics for a specific check bundle. See https://login.circonus.com/resources/api/calls/check_bundle_metrics for more information. +type CheckBundleMetrics struct { + CID string `json:"_cid,omitempty"` // string + Metrics []CheckBundleMetric `json:"metrics"` // See check_bundle.go for CheckBundleMetric definition +} + +// FetchCheckBundleMetrics retrieves metrics for the check bundle with passed cid. +func (a *API) FetchCheckBundleMetrics(cid CIDType) (*CheckBundleMetrics, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid check bundle metrics CID [none]") + } + + metricsCID := string(*cid) + + matched, err := regexp.MatchString(config.CheckBundleMetricsCIDRegex, metricsCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid check bundle metrics CID [%s]", metricsCID) + } + + result, err := a.Get(metricsCID) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch check bundle metrics, received JSON: %s", string(result)) + } + + metrics := &CheckBundleMetrics{} + if err := json.Unmarshal(result, metrics); err != nil { + return nil, err + } + + return metrics, nil +} + +// UpdateCheckBundleMetrics updates passed metrics. +func (a *API) UpdateCheckBundleMetrics(cfg *CheckBundleMetrics) (*CheckBundleMetrics, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid check bundle metrics config [nil]") + } + + metricsCID := string(cfg.CID) + + matched, err := regexp.MatchString(config.CheckBundleMetricsCIDRegex, metricsCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid check bundle metrics CID [%s]", metricsCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] update check bundle metrics, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(metricsCID, jsonCfg) + if err != nil { + return nil, err + } + + metrics := &CheckBundleMetrics{} + if err := json.Unmarshal(result, metrics); err != nil { + return nil, err + } + + return metrics, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/config/consts.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/config/consts.go new file mode 100644 index 000000000..bbca43d03 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/config/consts.go @@ -0,0 +1,538 @@ +package config + +// Key for CheckBundleConfig options and CheckDetails info +type Key string + +// Constants per type as defined in +// https://login.circonus.com/resources/api/calls/check_bundle +const ( + // + // default settings for api.NewCheckBundle() + // + DefaultCheckBundleMetricLimit = -1 // unlimited + DefaultCheckBundleStatus = "active" + DefaultCheckBundlePeriod = 60 + DefaultCheckBundleTimeout = 10 + DefaultConfigOptionsSize = 20 + + // + // common (apply to more than one check type) + // + AsyncMetrics = Key("asynch_metrics") + AuthMethod = Key("auth_method") + AuthPassword = Key("auth_password") + AuthUser = Key("auth_user") + BaseURL = Key("base_url") + CAChain = Key("ca_chain") + CertFile = Key("certificate_file") + Ciphers = Key("ciphers") + Command = Key("command") + DSN = Key("dsn") + HeaderPrefix = Key("header_") + HTTPVersion = Key("http_version") + KeyFile = Key("key_file") + Method = Key("method") + Password = Key("password") + Payload = Key("payload") + Port = Key("port") + Query = Key("query") + ReadLimit = Key("read_limit") + Secret = Key("secret") + SQL = Key("sql") + URI = Key("uri") + URL = Key("url") + Username = Key("username") + UseSSL = Key("use_ssl") + User = Key("user") + SASLAuthentication = Key("sasl_authentication") + SASLUser = Key("sasl_user") + SecurityLevel = Key("security_level") + Version = Key("version") + AppendColumnName = Key("append_column_name") + Database = Key("database") + JDBCPrefix = Key("jdbc_") + + // + // CAQL check + // + // Common items: + // Query + + // + // Circonus Windows Agent + // + // Common items: + // AuthPassword + // AuthUser + // Port + // URL + Calculated = Key("calculated") + Category = Key("category") + + // + // Cloudwatch + // + // Notes: + // DimPrefix is special because the actual key is dynamic and matches: `dim_(.+)` + // Common items: + // URL + // Version + APIKey = Key("api_key") + APISecret = Key("api_secret") + CloudwatchMetrics = Key("cloudwatch_metrics") + DimPrefix = Key("dim_") + Granularity = Key("granularity") + Namespace = Key("namespace") + Statistics = Key("statistics") + + // + // Collectd + // + // Common items: + // AsyncMetrics + // Username + // Secret + // SecurityLevel + + // + // Composite + // + CompositeMetricName = Key("composite_metric_name") + Formula = Key("formula") + + // + // DHCP + // + HardwareAddress = Key("hardware_addr") + HostIP = Key("host_ip") + RequestType = Key("request_type") + SendPort = Key("send_port") + + // + // DNS + // + // Common items: + // Query + CType = Key("ctype") + Nameserver = Key("nameserver") + RType = Key("rtype") + + // + // EC Console + // + // Common items: + // Command + // Port + // SASLAuthentication + // SASLUser + Objects = Key("objects") + XPath = Key("xpath") + + // + // Elastic Search + // + // Common items: + // Port + // URL + + // + // Ganglia + // + // Common items: + // AsyncMetrics + + // + // Google Analytics + // + // Common items: + // Password + // Username + OAuthToken = Key("oauth_token") + OAuthTokenSecret = Key("oauth_token_secret") + OAuthVersion = Key("oauth_version") + TableID = Key("table_id") + UseOAuth = Key("use_oauth") + + // + // HA Proxy + // + // Common items: + // AuthPassword + // AuthUser + // Port + // UseSSL + Host = Key("host") + Select = Key("select") + + // + // HTTP + // + // Notes: + // HeaderPrefix is special because the actual key is dynamic and matches: `header_(\S+)` + // Common items: + // AuthMethod + // AuthPassword + // AuthUser + // CAChain + // CertFile + // Ciphers + // KeyFile + // URL + // HeaderPrefix + // HTTPVersion + // Method + // Payload + // ReadLimit + Body = Key("body") + Code = Key("code") + Extract = Key("extract") + Redirects = Key("redirects") + + // + // HTTPTRAP + // + // Common items: + // AsyncMetrics + // Secret + + // + // IMAP + // + // Common items: + // AuthPassword + // AuthUser + // CAChain + // CertFile + // Ciphers + // KeyFile + // Port + // UseSSL + Fetch = Key("fetch") + Folder = Key("folder") + HeaderHost = Key("header_Host") + Search = Key("search") + + // + // JMX + // + // Common items: + // Password + // Port + // URI + // Username + MbeanDomains = Key("mbean_domains") + + // + // JSON + // + // Common items: + // AuthMethod + // AuthPassword + // AuthUser + // CAChain + // CertFile + // Ciphers + // HeaderPrefix + // HTTPVersion + // KeyFile + // Method + // Payload + // Port + // ReadLimit + // URL + + // + // Keynote + // + // Notes: + // SlotAliasPrefix is special because the actual key is dynamic and matches: `slot_alias_(\d+)` + // Common items: + // APIKey + // BaseURL + PageComponent = Key("pagecomponent") + SlotAliasPrefix = Key("slot_alias_") + SlotIDList = Key("slot_id_list") + TransPageList = Key("transpagelist") + + // + // Keynote Pulse + // + // Common items: + // BaseURL + // Password + // User + AgreementID = Key("agreement_id") + + // + // LDAP + // + // Common items: + // Password + // Port + AuthType = Key("authtype") + DN = Key("dn") + SecurityPrincipal = Key("security_principal") + + // + // Memcached + // + // Common items: + // Port + + // + // MongoDB + // + // Common items: + // Command + // Password + // Port + // Username + DBName = Key("dbname") + + // + // Munin + // + // Note: no configuration options + + // + // MySQL + // + // Common items: + // DSN + // SQL + + // + // Newrelic rpm + // + // Common items: + // APIKey + AccountID = Key("acct_id") + ApplicationID = Key("application_id") + LicenseKey = Key("license_key") + + // + // Nginx + // + // Common items: + // CAChain + // CertFile + // Ciphers + // KeyFile + // URL + + // + // NRPE + // + // Common items: + // Command + // Port + // UseSSL + AppendUnits = Key("append_uom") + + // + // NTP + // + // Common items: + // Port + Control = Key("control") + + // + // Oracle + // + // Notes: + // JDBCPrefix is special because the actual key is dynamic and matches: `jdbc_(\S+)` + // Common items: + // AppendColumnName + // Database + // JDBCPrefix + // Password + // Port + // SQL + // User + + // + // Ping ICMP + // + AvailNeeded = Key("avail_needed") + Count = Key("count") + Interval = Key("interval") + + // + // PostgreSQL + // + // Common items: + // DSN + // SQL + + // + // Redis + // + // Common items: + // Command + // Password + // Port + DBIndex = Key("dbindex") + + // + // Resmon + // + // Notes: + // HeaderPrefix is special because the actual key is dynamic and matches: `header_(\S+)` + // Common items: + // AuthMethod + // AuthPassword + // AuthUser + // CAChain + // CertFile + // Ciphers + // HeaderPrefix + // HTTPVersion + // KeyFile + // Method + // Payload + // Port + // ReadLimit + // URL + + // + // SMTP + // + // Common items: + // Payload + // Port + // SASLAuthentication + // SASLUser + EHLO = Key("ehlo") + From = Key("from") + SASLAuthID = Key("sasl_auth_id") + SASLPassword = Key("sasl_password") + StartTLS = Key("starttls") + To = Key("to") + + // + // SNMP + // + // Notes: + // OIDPrefix is special because the actual key is dynamic and matches: `oid_(.+)` + // TypePrefix is special because the actual key is dynamic and matches: `type_(.+)` + // Common items: + // Port + // SecurityLevel + // Version + AuthPassphrase = Key("auth_passphrase") + AuthProtocol = Key("auth_protocol") + Community = Key("community") + ContextEngine = Key("context_engine") + ContextName = Key("context_name") + OIDPrefix = Key("oid_") + PrivacyPassphrase = Key("privacy_passphrase") + PrivacyProtocol = Key("privacy_protocol") + SecurityEngine = Key("security_engine") + SecurityName = Key("security_name") + SeparateQueries = Key("separate_queries") + TypePrefix = Key("type_") + + // + // SQLServer + // + // Notes: + // JDBCPrefix is special because the actual key is dynamic and matches: `jdbc_(\S+)` + // Common items: + // AppendColumnName + // Database + // JDBCPrefix + // Password + // Port + // SQL + // User + + // + // SSH v2 + // + // Common items: + // Port + MethodCompCS = Key("method_comp_cs") + MethodCompSC = Key("method_comp_sc") + MethodCryptCS = Key("method_crypt_cs") + MethodCryptSC = Key("method_crypt_sc") + MethodHostKey = Key("method_hostkey") + MethodKeyExchange = Key("method_kex") + MethodMacCS = Key("method_mac_cs") + MethodMacSC = Key("method_mac_sc") + + // + // StatsD + // + // Note: no configuration options + + // + // TCP + // + // Common items: + // CAChain + // CertFile + // Ciphers + // KeyFile + // Port + // UseSSL + BannerMatch = Key("banner_match") + + // + // Varnish + // + // Note: no configuration options + + // + // reserved - config option(s) can't actually be set - here for r/o access + // + ReverseSecretKey = Key("reverse:secret_key") + SubmissionURL = Key("submission_url") + + // + // Endpoint prefix & cid regex + // + DefaultCIDRegex = "[0-9]+" + DefaultUUIDRegex = "[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}" + AccountPrefix = "/account" + AccountCIDRegex = "^(" + AccountPrefix + "/(" + DefaultCIDRegex + "|current))$" + AcknowledgementPrefix = "/acknowledgement" + AcknowledgementCIDRegex = "^(" + AcknowledgementPrefix + "/(" + DefaultCIDRegex + "))$" + AlertPrefix = "/alert" + AlertCIDRegex = "^(" + AlertPrefix + "/(" + DefaultCIDRegex + "))$" + AnnotationPrefix = "/annotation" + AnnotationCIDRegex = "^(" + AnnotationPrefix + "/(" + DefaultCIDRegex + "))$" + BrokerPrefix = "/broker" + BrokerCIDRegex = "^(" + BrokerPrefix + "/(" + DefaultCIDRegex + "))$" + CheckBundleMetricsPrefix = "/check_bundle_metrics" + CheckBundleMetricsCIDRegex = "^(" + CheckBundleMetricsPrefix + "/(" + DefaultCIDRegex + "))$" + CheckBundlePrefix = "/check_bundle" + CheckBundleCIDRegex = "^(" + CheckBundlePrefix + "/(" + DefaultCIDRegex + "))$" + CheckPrefix = "/check" + CheckCIDRegex = "^(" + CheckPrefix + "/(" + DefaultCIDRegex + "))$" + ContactGroupPrefix = "/contact_group" + ContactGroupCIDRegex = "^(" + ContactGroupPrefix + "/(" + DefaultCIDRegex + "))$" + DashboardPrefix = "/dashboard" + DashboardCIDRegex = "^(" + DashboardPrefix + "/(" + DefaultCIDRegex + "))$" + GraphPrefix = "/graph" + GraphCIDRegex = "^(" + GraphPrefix + "/(" + DefaultUUIDRegex + "))$" + MaintenancePrefix = "/maintenance" + MaintenanceCIDRegex = "^(" + MaintenancePrefix + "/(" + DefaultCIDRegex + "))$" + MetricClusterPrefix = "/metric_cluster" + MetricClusterCIDRegex = "^(" + MetricClusterPrefix + "/(" + DefaultCIDRegex + "))$" + MetricPrefix = "/metric" + MetricCIDRegex = "^(" + MetricPrefix + "/((" + DefaultCIDRegex + ")_([^[:space:]]+)))$" + OutlierReportPrefix = "/outlier_report" + OutlierReportCIDRegex = "^(" + OutlierReportPrefix + "/(" + DefaultCIDRegex + "))$" + ProvisionBrokerPrefix = "/provision_broker" + ProvisionBrokerCIDRegex = "^(" + ProvisionBrokerPrefix + "/([a-z0-9]+-[a-z0-9]+))$" + RuleSetGroupPrefix = "/rule_set_group" + RuleSetGroupCIDRegex = "^(" + RuleSetGroupPrefix + "/(" + DefaultCIDRegex + "))$" + RuleSetPrefix = "/rule_set" + RuleSetCIDRegex = "^(" + RuleSetPrefix + "/((" + DefaultCIDRegex + ")_([^[:space:]]+)))$" + UserPrefix = "/user" + UserCIDRegex = "^(" + UserPrefix + "/(" + DefaultCIDRegex + "|current))$" + WorksheetPrefix = "/worksheet" + WorksheetCIDRegex = "^(" + WorksheetPrefix + "/(" + DefaultUUIDRegex + "))$" + // contact group serverity levels + NumSeverityLevels = 5 +) diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/contact_group.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/contact_group.go new file mode 100644 index 000000000..578a2e898 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/contact_group.go @@ -0,0 +1,263 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Contact Group API support - Fetch, Create, Update, Delete, and Search +// See: https://login.circonus.com/resources/api/calls/contact_group + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// ContactGroupAlertFormats define alert formats +type ContactGroupAlertFormats struct { + LongMessage *string `json:"long_message"` // string or null + LongSubject *string `json:"long_subject"` // string or null + LongSummary *string `json:"long_summary"` // string or null + ShortMessage *string `json:"short_message"` // string or null + ShortSummary *string `json:"short_summary"` // string or null +} + +// ContactGroupContactsExternal external contacts +type ContactGroupContactsExternal struct { + Info string `json:"contact_info"` // string + Method string `json:"method"` // string +} + +// ContactGroupContactsUser user contacts +type ContactGroupContactsUser struct { + Info string `json:"_contact_info,omitempty"` // string + Method string `json:"method"` // string + UserCID string `json:"user"` // string +} + +// ContactGroupContacts list of contacts +type ContactGroupContacts struct { + External []ContactGroupContactsExternal `json:"external"` // [] len >= 0 + Users []ContactGroupContactsUser `json:"users"` // [] len >= 0 +} + +// ContactGroupEscalation defines escalations for severity levels +type ContactGroupEscalation struct { + After uint `json:"after"` // uint + ContactGroupCID string `json:"contact_group"` // string +} + +// ContactGroup defines a contact group. See https://login.circonus.com/resources/api/calls/contact_group for more information. +type ContactGroup struct { + AggregationWindow uint `json:"aggregation_window,omitempty"` // uint + AlertFormats ContactGroupAlertFormats `json:"alert_formats,omitempty"` // ContactGroupAlertFormats + CID string `json:"_cid,omitempty"` // string + Contacts ContactGroupContacts `json:"contacts,omitempty"` // ContactGroupContacts + Escalations []*ContactGroupEscalation `json:"escalations,omitempty"` // [] len == 5, elements: ContactGroupEscalation or null + LastModified uint `json:"_last_modified,omitempty"` // uint + LastModifiedBy string `json:"_last_modified_by,omitempty"` // string + Name string `json:"name,omitempty"` // string + Reminders []uint `json:"reminders,omitempty"` // [] len == 5 + Tags []string `json:"tags,omitempty"` // [] len >= 0 +} + +// NewContactGroup returns a ContactGroup (with defaults, if applicable) +func NewContactGroup() *ContactGroup { + return &ContactGroup{ + Escalations: make([]*ContactGroupEscalation, config.NumSeverityLevels), + Reminders: make([]uint, config.NumSeverityLevels), + Contacts: ContactGroupContacts{ + External: []ContactGroupContactsExternal{}, + Users: []ContactGroupContactsUser{}, + }, + } +} + +// FetchContactGroup retrieves contact group with passed cid. +func (a *API) FetchContactGroup(cid CIDType) (*ContactGroup, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid contact group CID [none]") + } + + groupCID := string(*cid) + + matched, err := regexp.MatchString(config.ContactGroupCIDRegex, groupCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid contact group CID [%s]", groupCID) + } + + result, err := a.Get(groupCID) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch contact group, received JSON: %s", string(result)) + } + + group := new(ContactGroup) + if err := json.Unmarshal(result, group); err != nil { + return nil, err + } + + return group, nil +} + +// FetchContactGroups retrieves all contact groups available to the API Token. +func (a *API) FetchContactGroups() (*[]ContactGroup, error) { + result, err := a.Get(config.ContactGroupPrefix) + if err != nil { + return nil, err + } + + var groups []ContactGroup + if err := json.Unmarshal(result, &groups); err != nil { + return nil, err + } + + return &groups, nil +} + +// UpdateContactGroup updates passed contact group. +func (a *API) UpdateContactGroup(cfg *ContactGroup) (*ContactGroup, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid contact group config [nil]") + } + + groupCID := string(cfg.CID) + + matched, err := regexp.MatchString(config.ContactGroupCIDRegex, groupCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid contact group CID [%s]", groupCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] update contact group, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(groupCID, jsonCfg) + if err != nil { + return nil, err + } + + group := &ContactGroup{} + if err := json.Unmarshal(result, group); err != nil { + return nil, err + } + + return group, nil +} + +// CreateContactGroup creates a new contact group. +func (a *API) CreateContactGroup(cfg *ContactGroup) (*ContactGroup, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid contact group config [nil]") + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] create contact group, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Post(config.ContactGroupPrefix, jsonCfg) + if err != nil { + return nil, err + } + + group := &ContactGroup{} + if err := json.Unmarshal(result, group); err != nil { + return nil, err + } + + return group, nil +} + +// DeleteContactGroup deletes passed contact group. +func (a *API) DeleteContactGroup(cfg *ContactGroup) (bool, error) { + if cfg == nil { + return false, fmt.Errorf("Invalid contact group config [nil]") + } + return a.DeleteContactGroupByCID(CIDType(&cfg.CID)) +} + +// DeleteContactGroupByCID deletes contact group with passed cid. +func (a *API) DeleteContactGroupByCID(cid CIDType) (bool, error) { + if cid == nil || *cid == "" { + return false, fmt.Errorf("Invalid contact group CID [none]") + } + + groupCID := string(*cid) + + matched, err := regexp.MatchString(config.ContactGroupCIDRegex, groupCID) + if err != nil { + return false, err + } + if !matched { + return false, fmt.Errorf("Invalid contact group CID [%s]", groupCID) + } + + _, err = a.Delete(groupCID) + if err != nil { + return false, err + } + + return true, nil +} + +// SearchContactGroups returns contact groups matching the specified +// search query and/or filter. If nil is passed for both parameters +// all contact groups will be returned. +func (a *API) SearchContactGroups(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]ContactGroup, error) { + q := url.Values{} + + if searchCriteria != nil && *searchCriteria != "" { + q.Set("search", string(*searchCriteria)) + } + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchContactGroups() + } + + reqURL := url.URL{ + Path: config.ContactGroupPrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var groups []ContactGroup + if err := json.Unmarshal(result, &groups); err != nil { + return nil, err + } + + return &groups, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/dashboard.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/dashboard.go new file mode 100644 index 000000000..b21987387 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/dashboard.go @@ -0,0 +1,399 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Dashboard API support - Fetch, Create, Update, Delete, and Search +// See: https://login.circonus.com/resources/api/calls/dashboard + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// DashboardGridLayout defines layout +type DashboardGridLayout struct { + Height uint `json:"height"` + Width uint `json:"width"` +} + +// DashboardAccessConfig defines access config +type DashboardAccessConfig struct { + BlackDash bool `json:"black_dash,omitempty"` + Enabled bool `json:"enabled,omitempty"` + Fullscreen bool `json:"fullscreen,omitempty"` + FullscreenHideTitle bool `json:"fullscreen_hide_title,omitempty"` + Nickname string `json:"nickname,omitempty"` + ScaleText bool `json:"scale_text,omitempty"` + SharedID string `json:"shared_id,omitempty"` + TextSize uint `json:"text_size,omitempty"` +} + +// DashboardOptions defines options +type DashboardOptions struct { + AccessConfigs []DashboardAccessConfig `json:"access_configs,omitempty"` + FullscreenHideTitle bool `json:"fullscreen_hide_title,omitempty"` + HideGrid bool `json:"hide_grid,omitempty"` + Linkages [][]string `json:"linkages,omitempty"` + ScaleText bool `json:"scale_text,omitempty"` + TextSize uint `json:"text_size,omitempty"` +} + +// ChartTextWidgetDatapoint defines datapoints for charts +type ChartTextWidgetDatapoint struct { + AccountID string `json:"account_id,omitempty"` // metric cluster, metric + CheckID uint `json:"_check_id,omitempty"` // metric + ClusterID uint `json:"cluster_id,omitempty"` // metric cluster + ClusterTitle string `json:"_cluster_title,omitempty"` // metric cluster + Label string `json:"label,omitempty"` // metric + Label2 string `json:"_label,omitempty"` // metric cluster + Metric string `json:"metric,omitempty"` // metric + MetricType string `json:"_metric_type,omitempty"` // metric + NumericOnly bool `json:"numeric_only,omitempty"` // metric cluster +} + +// ChartWidgetDefinitionLegend defines chart widget definition legend +type ChartWidgetDefinitionLegend struct { + Show bool `json:"show,omitempty"` + Type string `json:"type,omitempty"` +} + +// ChartWidgetWedgeLabels defines chart widget wedge labels +type ChartWidgetWedgeLabels struct { + OnChart bool `json:"on_chart,omitempty"` + ToolTips bool `json:"tooltips,omitempty"` +} + +// ChartWidgetWedgeValues defines chart widget wedge values +type ChartWidgetWedgeValues struct { + Angle string `json:"angle,omitempty"` + Color string `json:"color,omitempty"` + Show bool `json:"show,omitempty"` +} + +// ChartWidgtDefinition defines chart widget definition +type ChartWidgtDefinition struct { + Datasource string `json:"datasource,omitempty"` + Derive string `json:"derive,omitempty"` + DisableAutoformat bool `json:"disable_autoformat,omitempty"` + Formula string `json:"formula,omitempty"` + Legend ChartWidgetDefinitionLegend `json:"legend,omitempty"` + Period uint `json:"period,omitempty"` + PopOnHover bool `json:"pop_onhover,omitempty"` + WedgeLabels ChartWidgetWedgeLabels `json:"wedge_labels,omitempty"` + WedgeValues ChartWidgetWedgeValues `json:"wedge_values,omitempty"` +} + +// ForecastGaugeWidgetThresholds defines forecast widget thresholds +type ForecastGaugeWidgetThresholds struct { + Colors []string `json:"colors,omitempty"` // forecasts, gauges + Flip bool `json:"flip,omitempty"` // gauges + Values []string `json:"values,omitempty"` // forecasts, gauges +} + +// StatusWidgetAgentStatusSettings defines agent status settings +type StatusWidgetAgentStatusSettings struct { + Search string `json:"search,omitempty"` + ShowAgentTypes string `json:"show_agent_types,omitempty"` + ShowContact bool `json:"show_contact,omitempty"` + ShowFeeds bool `json:"show_feeds,omitempty"` + ShowSetup bool `json:"show_setup,omitempty"` + ShowSkew bool `json:"show_skew,omitempty"` + ShowUpdates bool `json:"show_updates,omitempty"` +} + +// StatusWidgetHostStatusSettings defines host status settings +type StatusWidgetHostStatusSettings struct { + LayoutStyle string `json:"layout_style,omitempty"` + Search string `json:"search,omitempty"` + SortBy string `json:"sort_by,omitempty"` + TagFilterSet []string `json:"tag_filter_set,omitempty"` +} + +// DashboardWidgetSettings defines settings specific to widget +type DashboardWidgetSettings struct { + AccountID string `json:"account_id,omitempty"` // alerts, clusters, gauges, graphs, lists, status + Acknowledged string `json:"acknowledged,omitempty"` // alerts + AgentStatusSettings StatusWidgetAgentStatusSettings `json:"agent_status_settings,omitempty"` // status + Algorithm string `json:"algorithm,omitempty"` // clusters + Autoformat bool `json:"autoformat,omitempty"` // text + BodyFormat string `json:"body_format,omitempty"` // text + ChartType string `json:"chart_type,omitempty"` // charts + CheckUUID string `json:"check_uuid,omitempty"` // gauges + Cleared string `json:"cleared,omitempty"` // alerts + ClusterID uint `json:"cluster_id,omitempty"` // clusters + ClusterName string `json:"cluster_name,omitempty"` // clusters + ContactGroups []uint `json:"contact_groups,omitempty"` // alerts + ContentType string `json:"content_type,omitempty"` // status + Datapoints []ChartTextWidgetDatapoint `json:"datapoints,omitempty"` // charts, text + DateWindow string `json:"date_window,omitempty"` // graphs + Definition ChartWidgtDefinition `json:"definition,omitempty"` // charts + Dependents string `json:"dependents,omitempty"` // alerts + DisableAutoformat bool `json:"disable_autoformat,omitempty"` // gauges + Display string `json:"display,omitempty"` // alerts + Format string `json:"format,omitempty"` // forecasts + Formula string `json:"formula,omitempty"` // gauges + GraphUUID string `json:"graph_id,omitempty"` // graphs + HideXAxis bool `json:"hide_xaxis,omitempty"` // graphs + HideYAxis bool `json:"hide_yaxis,omitempty"` // graphs + HostStatusSettings StatusWidgetHostStatusSettings `json:"host_status_settings,omitempty"` // status + KeyInline bool `json:"key_inline,omitempty"` // graphs + KeyLoc string `json:"key_loc,omitempty"` // graphs + KeySize uint `json:"key_size,omitempty"` // graphs + KeyWrap bool `json:"key_wrap,omitempty"` // graphs + Label string `json:"label,omitempty"` // graphs + Layout string `json:"layout,omitempty"` // clusters + Limit uint `json:"limit,omitempty"` // lists + Maintenance string `json:"maintenance,omitempty"` // alerts + Markup string `json:"markup,omitempty"` // html + MetricDisplayName string `json:"metric_display_name,omitempty"` // gauges + MetricName string `json:"metric_name,omitempty"` // gauges + MinAge string `json:"min_age,omitempty"` // alerts + OffHours []uint `json:"off_hours,omitempty"` // alerts + OverlaySetID string `json:"overlay_set_id,omitempty"` // graphs + Period uint `json:"period,omitempty"` // gauges, text, graphs + RangeHigh int `json:"range_high,omitempty"` // gauges + RangeLow int `json:"range_low,omitempty"` // gauges + Realtime bool `json:"realtime,omitempty"` // graphs + ResourceLimit string `json:"resource_limit,omitempty"` // forecasts + ResourceUsage string `json:"resource_usage,omitempty"` // forecasts + Search string `json:"search,omitempty"` // alerts, lists + Severity string `json:"severity,omitempty"` // alerts + ShowFlags bool `json:"show_flags,omitempty"` // graphs + Size string `json:"size,omitempty"` // clusters + TagFilterSet []string `json:"tag_filter_set,omitempty"` // alerts + Threshold float32 `json:"threshold,omitempty"` // clusters + Thresholds ForecastGaugeWidgetThresholds `json:"thresholds,omitempty"` // forecasts, gauges + TimeWindow string `json:"time_window,omitempty"` // alerts + Title string `json:"title,omitempty"` // alerts, charts, forecasts, gauges, html + TitleFormat string `json:"title_format,omitempty"` // text + Trend string `json:"trend,omitempty"` // forecasts + Type string `json:"type,omitempty"` // gauges, lists + UseDefault bool `json:"use_default,omitempty"` // text + ValueType string `json:"value_type,omitempty"` // gauges, text + WeekDays []string `json:"weekdays,omitempty"` // alerts +} + +// DashboardWidget defines widget +type DashboardWidget struct { + Active bool `json:"active"` + Height uint `json:"height"` + Name string `json:"name"` + Origin string `json:"origin"` + Settings DashboardWidgetSettings `json:"settings"` + Type string `json:"type"` + WidgetID string `json:"widget_id"` + Width uint `json:"width"` +} + +// Dashboard defines a dashboard. See https://login.circonus.com/resources/api/calls/dashboard for more information. +type Dashboard struct { + AccountDefault bool `json:"account_default"` + Active bool `json:"_active,omitempty"` + CID string `json:"_cid,omitempty"` + Created uint `json:"_created,omitempty"` + CreatedBy string `json:"_created_by,omitempty"` + GridLayout DashboardGridLayout `json:"grid_layout"` + LastModified uint `json:"_last_modified,omitempty"` + Options DashboardOptions `json:"options"` + Shared bool `json:"shared"` + Title string `json:"title"` + UUID string `json:"_dashboard_uuid,omitempty"` + Widgets []DashboardWidget `json:"widgets"` +} + +// NewDashboard returns a new Dashboard (with defaults, if applicable) +func NewDashboard() *Dashboard { + return &Dashboard{} +} + +// FetchDashboard retrieves dashboard with passed cid. +func (a *API) FetchDashboard(cid CIDType) (*Dashboard, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid dashboard CID [none]") + } + + dashboardCID := string(*cid) + + matched, err := regexp.MatchString(config.DashboardCIDRegex, dashboardCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid dashboard CID [%s]", dashboardCID) + } + + result, err := a.Get(string(*cid)) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch dashboard, received JSON: %s", string(result)) + } + + dashboard := new(Dashboard) + if err := json.Unmarshal(result, dashboard); err != nil { + return nil, err + } + + return dashboard, nil +} + +// FetchDashboards retrieves all dashboards available to the API Token. +func (a *API) FetchDashboards() (*[]Dashboard, error) { + result, err := a.Get(config.DashboardPrefix) + if err != nil { + return nil, err + } + + var dashboards []Dashboard + if err := json.Unmarshal(result, &dashboards); err != nil { + return nil, err + } + + return &dashboards, nil +} + +// UpdateDashboard updates passed dashboard. +func (a *API) UpdateDashboard(cfg *Dashboard) (*Dashboard, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid dashboard config [nil]") + } + + dashboardCID := string(cfg.CID) + + matched, err := regexp.MatchString(config.DashboardCIDRegex, dashboardCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid dashboard CID [%s]", dashboardCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] update dashboard, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(dashboardCID, jsonCfg) + if err != nil { + return nil, err + } + + dashboard := &Dashboard{} + if err := json.Unmarshal(result, dashboard); err != nil { + return nil, err + } + + return dashboard, nil +} + +// CreateDashboard creates a new dashboard. +func (a *API) CreateDashboard(cfg *Dashboard) (*Dashboard, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid dashboard config [nil]") + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] create dashboard, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Post(config.DashboardPrefix, jsonCfg) + if err != nil { + return nil, err + } + + dashboard := &Dashboard{} + if err := json.Unmarshal(result, dashboard); err != nil { + return nil, err + } + + return dashboard, nil +} + +// DeleteDashboard deletes passed dashboard. +func (a *API) DeleteDashboard(cfg *Dashboard) (bool, error) { + if cfg == nil { + return false, fmt.Errorf("Invalid dashboard config [nil]") + } + return a.DeleteDashboardByCID(CIDType(&cfg.CID)) +} + +// DeleteDashboardByCID deletes dashboard with passed cid. +func (a *API) DeleteDashboardByCID(cid CIDType) (bool, error) { + if cid == nil || *cid == "" { + return false, fmt.Errorf("Invalid dashboard CID [none]") + } + + dashboardCID := string(*cid) + + matched, err := regexp.MatchString(config.DashboardCIDRegex, dashboardCID) + if err != nil { + return false, err + } + if !matched { + return false, fmt.Errorf("Invalid dashboard CID [%s]", dashboardCID) + } + + _, err = a.Delete(dashboardCID) + if err != nil { + return false, err + } + + return true, nil +} + +// SearchDashboards returns dashboards matching the specified +// search query and/or filter. If nil is passed for both parameters +// all dashboards will be returned. +func (a *API) SearchDashboards(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]Dashboard, error) { + q := url.Values{} + + if searchCriteria != nil && *searchCriteria != "" { + q.Set("search", string(*searchCriteria)) + } + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchDashboards() + } + + reqURL := url.URL{ + Path: config.DashboardPrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var dashboards []Dashboard + if err := json.Unmarshal(result, &dashboards); err != nil { + return nil, err + } + + return &dashboards, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/doc.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/doc.go new file mode 100644 index 000000000..63904d784 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/doc.go @@ -0,0 +1,63 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package api provides methods for interacting with the Circonus API. See the full Circonus API +Documentation at https://login.circonus.com/resources/api for more information. + +Raw REST methods + + Get - retrieve existing item(s) + Put - update an existing item + Post - create a new item + Delete - remove an existing item + +Endpoints (supported) + + Account https://login.circonus.com/resources/api/calls/account + Acknowledgement https://login.circonus.com/resources/api/calls/acknowledgement + Alert https://login.circonus.com/resources/api/calls/alert + Annotation https://login.circonus.com/resources/api/calls/annotation + Broker https://login.circonus.com/resources/api/calls/broker + Check https://login.circonus.com/resources/api/calls/check + Check Bundle https://login.circonus.com/resources/api/calls/check_bundle + Check Bundle Metrics https://login.circonus.com/resources/api/calls/check_bundle_metrics + Contact Group https://login.circonus.com/resources/api/calls/contact_group + Dashboard https://login.circonus.com/resources/api/calls/dashboard + Graph https://login.circonus.com/resources/api/calls/graph + Maintenance [window] https://login.circonus.com/resources/api/calls/maintenance + Metric https://login.circonus.com/resources/api/calls/metric + Metric Cluster https://login.circonus.com/resources/api/calls/metric_cluster + Outlier Report https://login.circonus.com/resources/api/calls/outlier_report + Provision Broker https://login.circonus.com/resources/api/calls/provision_broker + Rule Set https://login.circonus.com/resources/api/calls/rule_set + Rule Set Group https://login.circonus.com/resources/api/calls/rule_set_group + User https://login.circonus.com/resources/api/calls/user + Worksheet https://login.circonus.com/resources/api/calls/worksheet + +Endpoints (not supported) + + Support may be added for these endpoints in the future. These endpoints may currently be used + directly with the Raw REST methods above. + + CAQL https://login.circonus.com/resources/api/calls/caql + Check Move https://login.circonus.com/resources/api/calls/check_move + Data https://login.circonus.com/resources/api/calls/data + Snapshot https://login.circonus.com/resources/api/calls/snapshot + Tag https://login.circonus.com/resources/api/calls/tag + Template https://login.circonus.com/resources/api/calls/template + +Verbs + + Fetch singular/plural item(s) - e.g. FetchAnnotation, FetchAnnotations + Create create new item - e.g. CreateAnnotation + Update update an item - e.g. UpdateAnnotation + Delete remove an item - e.g. DeleteAnnotation, DeleteAnnotationByCID + Search search for item(s) - e.g. SearchAnnotations + New new item config - e.g. NewAnnotation (returns an empty item, + any applicable defautls defined) + + Not all endpoints support all verbs. +*/ +package api diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/graph.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/graph.go new file mode 100644 index 000000000..2d2865327 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/graph.go @@ -0,0 +1,349 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Graph API support - Fetch, Create, Update, Delete, and Search +// See: https://login.circonus.com/resources/api/calls/graph + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// GraphAccessKey defines an access key for a graph +type GraphAccessKey struct { + Active bool `json:"active,omitempty"` // boolean + Height uint `json:"height,omitempty"` // uint + Key string `json:"key,omitempty"` // string + Legend bool `json:"legend,omitempty"` // boolean + LockDate bool `json:"lock_date,omitempty"` // boolean + LockMode string `json:"lock_mode,omitempty"` // string + LockRangeEnd uint `json:"lock_range_end,omitempty"` // uint + LockRangeStart uint `json:"lock_range_start,omitempty"` // uint + LockShowTimes bool `json:"lock_show_times,omitempty"` // boolean + LockZoom string `json:"lock_zoom,omitempty"` // string + Nickname string `json:"nickname,omitempty"` // string + Title bool `json:"title,omitempty"` // boolean + Width uint `json:"width,omitempty"` // uint + XLabels bool `json:"x_labels,omitempty"` // boolean + YLabels bool `json:"y_labels,omitempty"` // boolean +} + +// GraphComposite defines a composite +type GraphComposite struct { + Axis string `json:"axis,omitempty"` // string + Color string `json:"color,omitempty"` // string + DataFormula *string `json:"data_formula,omitempty"` // string or null + Hidden bool `json:"hidden,omitempty"` // boolean + LegendFormula *string `json:"legend_formula,omitempty"` // string or null + Name string `json:"name,omitempty"` // string + Stack *uint `json:"stack,omitempty"` // uint or null +} + +// GraphDatapoint defines a datapoint +type GraphDatapoint struct { + Alpha *float64 `json:"alpha,string,omitempty"` // float64 + Axis string `json:"axis,omitempty"` // string + CAQL *string `json:"caql,omitempty"` // string or null + CheckID uint `json:"check_id,omitempty"` // uint + Color *string `json:"color,omitempty"` // string + DataFormula *string `json:"data_formula"` // string or null + Derive interface{} `json:"derive,omitempty"` // BUG doc: string, api: string or boolean(for caql statements) + Hidden bool `json:"hidden"` // boolean + LegendFormula *string `json:"legend_formula"` // string or null + MetricName string `json:"metric_name,omitempty"` // string + MetricType string `json:"metric_type,omitempty"` // string + Name string `json:"name"` // string + Stack *uint `json:"stack"` // uint or null +} + +// GraphGuide defines a guide +type GraphGuide struct { + Color string `json:"color,omitempty"` // string + DataFormula *string `json:"data_formula,omitempty"` // string or null + Hidden bool `json:"hidden,omitempty"` // boolean + LegendFormula *string `json:"legend_formula,omitempty"` // string or null + Name string `json:"name,omitempty"` // string +} + +// GraphMetricCluster defines a metric cluster +type GraphMetricCluster struct { + AggregateFunc string `json:"aggregate_function,omitempty"` // string + Axis string `json:"axis,omitempty"` // string + Color *string `json:"color,omitempty"` // string + DataFormula *string `json:"data_formula"` // string or null + Hidden bool `json:"hidden"` // boolean + LegendFormula *string `json:"legend_formula"` // string or null + MetricCluster string `json:"metric_cluster,omitempty"` // string + Name string `json:"name,omitempty"` // string + Stack *uint `json:"stack"` // uint or null +} + +// OverlayDataOptions defines overlay options for data. Note, each overlay type requires +// a _subset_ of the options. See Graph API documentation (URL above) for details. +type OverlayDataOptions struct { + Alerts *int `json:"alerts,string,omitempty"` // int encoded as string BUG doc: numeric, api: string + ArrayOutput *int `json:"array_output,string,omitempty"` // int encoded as string BUG doc: numeric, api: string + BasePeriod *int `json:"base_period,string,omitempty"` // int encoded as string BUG doc: numeric, api: string + Delay *int `json:"delay,string,omitempty"` // int encoded as string BUG doc: numeric, api: string + Extension string `json:"extension,omitempty"` // string + GraphTitle string `json:"graph_title,omitempty"` // string + GraphUUID string `json:"graph_id,omitempty"` // string + InPercent *bool `json:"in_percent,string,omitempty"` // boolean encoded as string BUG doc: boolean, api: string + Inverse *int `json:"inverse,string,omitempty"` // int encoded as string BUG doc: numeric, api: string + Method string `json:"method,omitempty"` // string + Model string `json:"model,omitempty"` // string + ModelEnd string `json:"model_end,omitempty"` // string + ModelPeriod string `json:"model_period,omitempty"` // string + ModelRelative *int `json:"model_relative,string,omitempty"` // int encoded as string BUG doc: numeric, api: string + Out string `json:"out,omitempty"` // string + Prequel string `json:"prequel,omitempty"` // string + Presets string `json:"presets,omitempty"` // string + Quantiles string `json:"quantiles,omitempty"` // string + SeasonLength *int `json:"season_length,string,omitempty"` // int encoded as string BUG doc: numeric, api: string + Sensitivity *int `json:"sensitivity,string,omitempty"` // int encoded as string BUG doc: numeric, api: string + SingleValue *int `json:"single_value,string,omitempty"` // int encoded as string BUG doc: numeric, api: string + TargetPeriod string `json:"target_period,omitempty"` // string + TimeOffset string `json:"time_offset,omitempty"` // string + TimeShift *int `json:"time_shift,string,omitempty"` // int encoded as string BUG doc: numeric, api: string + Transform string `json:"transform,omitempty"` // string + Version *int `json:"version,string,omitempty"` // int encoded as string BUG doc: numeric, api: string + Window *int `json:"window,string,omitempty"` // int encoded as string BUG doc: numeric, api: string + XShift string `json:"x_shift,omitempty"` // string +} + +// OverlayUISpecs defines UI specs for overlay +type OverlayUISpecs struct { + Decouple bool `json:"decouple,omitempty"` // boolean + ID string `json:"id,omitempty"` // string + Label string `json:"label,omitempty"` // string + Type string `json:"type,omitempty"` // string + Z *int `json:"z,string,omitempty"` // int encoded as string BUG doc: numeric, api: string +} + +// GraphOverlaySet defines overlays for graph +type GraphOverlaySet struct { + DataOpts OverlayDataOptions `json:"data_opts,omitempty"` // OverlayDataOptions + ID string `json:"id,omitempty"` // string + Title string `json:"title,omitempty"` // string + UISpecs OverlayUISpecs `json:"ui_specs,omitempty"` // OverlayUISpecs +} + +// Graph defines a graph. See https://login.circonus.com/resources/api/calls/graph for more information. +type Graph struct { + AccessKeys []GraphAccessKey `json:"access_keys,omitempty"` // [] len >= 0 + CID string `json:"_cid,omitempty"` // string + Composites []GraphComposite `json:"composites,omitempty"` // [] len >= 0 + Datapoints []GraphDatapoint `json:"datapoints,omitempt"` // [] len >= 0 + Description string `json:"description,omitempty"` // string + Guides []GraphGuide `json:"guides,omitempty"` // [] len >= 0 + LineStyle *string `json:"line_style"` // string or null + LogLeftY *int `json:"logarithmic_left_y,string,omitempty"` // int encoded as string or null BUG doc: number (not string) + LogRightY *int `json:"logarithmic_right_y,string,omitempty"` // int encoded as string or null BUG doc: number (not string) + MaxLeftY *float64 `json:"max_left_y,string,omitempty"` // float64 encoded as string or null BUG doc: number (not string) + MaxRightY *float64 `json:"max_right_y,string,omitempty"` // float64 encoded as string or null BUG doc: number (not string) + MetricClusters []GraphMetricCluster `json:"metric_clusters,omitempty"` // [] len >= 0 + MinLeftY *float64 `json:"min_left_y,string,omitempty"` // float64 encoded as string or null BUG doc: number (not string) + MinRightY *float64 `json:"min_right_y,string,omitempty"` // float64 encoded as string or null BUG doc: number (not string) + Notes *string `json:"notes,omitempty"` // string or null + OverlaySets *map[string]GraphOverlaySet `json:"overlay_sets,omitempty"` // GroupOverLaySets or null + Style *string `json:"style"` // string or null + Tags []string `json:"tags,omitempty"` // [] len >= 0 + Title string `json:"title,omitempty"` // string +} + +// NewGraph returns a Graph (with defaults, if applicable) +func NewGraph() *Graph { + return &Graph{} +} + +// FetchGraph retrieves graph with passed cid. +func (a *API) FetchGraph(cid CIDType) (*Graph, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid graph CID [none]") + } + + graphCID := string(*cid) + + matched, err := regexp.MatchString(config.GraphCIDRegex, graphCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid graph CID [%s]", graphCID) + } + + result, err := a.Get(graphCID) + if err != nil { + return nil, err + } + if a.Debug { + a.Log.Printf("[DEBUG] fetch graph, received JSON: %s", string(result)) + } + + graph := new(Graph) + if err := json.Unmarshal(result, graph); err != nil { + return nil, err + } + + return graph, nil +} + +// FetchGraphs retrieves all graphs available to the API Token. +func (a *API) FetchGraphs() (*[]Graph, error) { + result, err := a.Get(config.GraphPrefix) + if err != nil { + return nil, err + } + + var graphs []Graph + if err := json.Unmarshal(result, &graphs); err != nil { + return nil, err + } + + return &graphs, nil +} + +// UpdateGraph updates passed graph. +func (a *API) UpdateGraph(cfg *Graph) (*Graph, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid graph config [nil]") + } + + graphCID := string(cfg.CID) + + matched, err := regexp.MatchString(config.GraphCIDRegex, graphCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid graph CID [%s]", graphCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] update graph, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(graphCID, jsonCfg) + if err != nil { + return nil, err + } + + graph := &Graph{} + if err := json.Unmarshal(result, graph); err != nil { + return nil, err + } + + return graph, nil +} + +// CreateGraph creates a new graph. +func (a *API) CreateGraph(cfg *Graph) (*Graph, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid graph config [nil]") + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] update graph, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Post(config.GraphPrefix, jsonCfg) + if err != nil { + return nil, err + } + + graph := &Graph{} + if err := json.Unmarshal(result, graph); err != nil { + return nil, err + } + + return graph, nil +} + +// DeleteGraph deletes passed graph. +func (a *API) DeleteGraph(cfg *Graph) (bool, error) { + if cfg == nil { + return false, fmt.Errorf("Invalid graph config [nil]") + } + return a.DeleteGraphByCID(CIDType(&cfg.CID)) +} + +// DeleteGraphByCID deletes graph with passed cid. +func (a *API) DeleteGraphByCID(cid CIDType) (bool, error) { + if cid == nil || *cid == "" { + return false, fmt.Errorf("Invalid graph CID [none]") + } + + graphCID := string(*cid) + + matched, err := regexp.MatchString(config.GraphCIDRegex, graphCID) + if err != nil { + return false, err + } + if !matched { + return false, fmt.Errorf("Invalid graph CID [%s]", graphCID) + } + + _, err = a.Delete(graphCID) + if err != nil { + return false, err + } + + return true, nil +} + +// SearchGraphs returns graphs matching the specified search query +// and/or filter. If nil is passed for both parameters all graphs +// will be returned. +func (a *API) SearchGraphs(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]Graph, error) { + q := url.Values{} + + if searchCriteria != nil && *searchCriteria != "" { + q.Set("search", string(*searchCriteria)) + } + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchGraphs() + } + + reqURL := url.URL{ + Path: config.GraphPrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var graphs []Graph + if err := json.Unmarshal(result, &graphs); err != nil { + return nil, err + } + + return &graphs, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/maintenance.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/maintenance.go new file mode 100644 index 000000000..0e5e04729 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/maintenance.go @@ -0,0 +1,220 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Maintenance window API support - Fetch, Create, Update, Delete, and Search +// See: https://login.circonus.com/resources/api/calls/maintenance + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// Maintenance defines a maintenance window. See https://login.circonus.com/resources/api/calls/maintenance for more information. +type Maintenance struct { + CID string `json:"_cid,omitempty"` // string + Item string `json:"item,omitempty"` // string + Notes string `json:"notes,omitempty"` // string + Severities interface{} `json:"severities,omitempty"` // []string NOTE can be set with CSV string or []string + Start uint `json:"start,omitempty"` // uint + Stop uint `json:"stop,omitempty"` // uint + Tags []string `json:"tags,omitempty"` // [] len >= 0 + Type string `json:"type,omitempty"` // string +} + +// NewMaintenanceWindow returns a new Maintenance window (with defaults, if applicable) +func NewMaintenanceWindow() *Maintenance { + return &Maintenance{} +} + +// FetchMaintenanceWindow retrieves maintenance [window] with passed cid. +func (a *API) FetchMaintenanceWindow(cid CIDType) (*Maintenance, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid maintenance window CID [none]") + } + + maintenanceCID := string(*cid) + + matched, err := regexp.MatchString(config.MaintenanceCIDRegex, maintenanceCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid maintenance window CID [%s]", maintenanceCID) + } + + result, err := a.Get(maintenanceCID) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch maintenance window, received JSON: %s", string(result)) + } + + window := &Maintenance{} + if err := json.Unmarshal(result, window); err != nil { + return nil, err + } + + return window, nil +} + +// FetchMaintenanceWindows retrieves all maintenance [windows] available to API Token. +func (a *API) FetchMaintenanceWindows() (*[]Maintenance, error) { + result, err := a.Get(config.MaintenancePrefix) + if err != nil { + return nil, err + } + + var windows []Maintenance + if err := json.Unmarshal(result, &windows); err != nil { + return nil, err + } + + return &windows, nil +} + +// UpdateMaintenanceWindow updates passed maintenance [window]. +func (a *API) UpdateMaintenanceWindow(cfg *Maintenance) (*Maintenance, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid maintenance window config [nil]") + } + + maintenanceCID := string(cfg.CID) + + matched, err := regexp.MatchString(config.MaintenanceCIDRegex, maintenanceCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid maintenance window CID [%s]", maintenanceCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] update maintenance window, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(maintenanceCID, jsonCfg) + if err != nil { + return nil, err + } + + window := &Maintenance{} + if err := json.Unmarshal(result, window); err != nil { + return nil, err + } + + return window, nil +} + +// CreateMaintenanceWindow creates a new maintenance [window]. +func (a *API) CreateMaintenanceWindow(cfg *Maintenance) (*Maintenance, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid maintenance window config [nil]") + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] create maintenance window, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Post(config.MaintenancePrefix, jsonCfg) + if err != nil { + return nil, err + } + + window := &Maintenance{} + if err := json.Unmarshal(result, window); err != nil { + return nil, err + } + + return window, nil +} + +// DeleteMaintenanceWindow deletes passed maintenance [window]. +func (a *API) DeleteMaintenanceWindow(cfg *Maintenance) (bool, error) { + if cfg == nil { + return false, fmt.Errorf("Invalid maintenance window config [nil]") + } + return a.DeleteMaintenanceWindowByCID(CIDType(&cfg.CID)) +} + +// DeleteMaintenanceWindowByCID deletes maintenance [window] with passed cid. +func (a *API) DeleteMaintenanceWindowByCID(cid CIDType) (bool, error) { + if cid == nil || *cid == "" { + return false, fmt.Errorf("Invalid maintenance window CID [none]") + } + + maintenanceCID := string(*cid) + + matched, err := regexp.MatchString(config.MaintenanceCIDRegex, maintenanceCID) + if err != nil { + return false, err + } + if !matched { + return false, fmt.Errorf("Invalid maintenance window CID [%s]", maintenanceCID) + } + + _, err = a.Delete(maintenanceCID) + if err != nil { + return false, err + } + + return true, nil +} + +// SearchMaintenanceWindows returns maintenance [windows] matching +// the specified search query and/or filter. If nil is passed for +// both parameters all maintenance [windows] will be returned. +func (a *API) SearchMaintenanceWindows(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]Maintenance, error) { + q := url.Values{} + + if searchCriteria != nil && *searchCriteria != "" { + q.Set("search", string(*searchCriteria)) + } + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchMaintenanceWindows() + } + + reqURL := url.URL{ + Path: config.MaintenancePrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var windows []Maintenance + if err := json.Unmarshal(result, &windows); err != nil { + return nil, err + } + + return &windows, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/metric.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/metric.go new file mode 100644 index 000000000..3608b06ff --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/metric.go @@ -0,0 +1,162 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Metric API support - Fetch, Create*, Update, Delete*, and Search +// See: https://login.circonus.com/resources/api/calls/metric +// * : create and delete are handled via check_bundle or check_bundle_metrics + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// Metric defines a metric. See https://login.circonus.com/resources/api/calls/metric for more information. +type Metric struct { + Active bool `json:"_active,omitempty"` // boolean + CheckActive bool `json:"_check_active,omitempty"` // boolean + CheckBundleCID string `json:"_check_bundle,omitempty"` // string + CheckCID string `json:"_check,omitempty"` // string + CheckTags []string `json:"_check_tags,omitempty"` // [] len >= 0 + CheckUUID string `json:"_check_uuid,omitempty"` // string + CID string `json:"_cid,omitempty"` // string + Histogram string `json:"_histogram,omitempty"` // string + Link *string `json:"link,omitempty"` // string or null + MetricName string `json:"_metric_name,omitempty"` // string + MetricType string `json:"_metric_type,omitempty"` // string + Notes *string `json:"notes,omitempty"` // string or null + Tags []string `json:"tags,omitempty"` // [] len >= 0 + Units *string `json:"units,omitempty"` // string or null +} + +// FetchMetric retrieves metric with passed cid. +func (a *API) FetchMetric(cid CIDType) (*Metric, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid metric CID [none]") + } + + metricCID := string(*cid) + + matched, err := regexp.MatchString(config.MetricCIDRegex, metricCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid metric CID [%s]", metricCID) + } + + result, err := a.Get(metricCID) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch metric, received JSON: %s", string(result)) + } + + metric := &Metric{} + if err := json.Unmarshal(result, metric); err != nil { + return nil, err + } + + return metric, nil +} + +// FetchMetrics retrieves all metrics available to API Token. +func (a *API) FetchMetrics() (*[]Metric, error) { + result, err := a.Get(config.MetricPrefix) + if err != nil { + return nil, err + } + + var metrics []Metric + if err := json.Unmarshal(result, &metrics); err != nil { + return nil, err + } + + return &metrics, nil +} + +// UpdateMetric updates passed metric. +func (a *API) UpdateMetric(cfg *Metric) (*Metric, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid metric config [nil]") + } + + metricCID := string(cfg.CID) + + matched, err := regexp.MatchString(config.MetricCIDRegex, metricCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid metric CID [%s]", metricCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] update metric, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(metricCID, jsonCfg) + if err != nil { + return nil, err + } + + metric := &Metric{} + if err := json.Unmarshal(result, metric); err != nil { + return nil, err + } + + return metric, nil +} + +// SearchMetrics returns metrics matching the specified search query +// and/or filter. If nil is passed for both parameters all metrics +// will be returned. +func (a *API) SearchMetrics(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]Metric, error) { + q := url.Values{} + + if searchCriteria != nil && *searchCriteria != "" { + q.Set("search", string(*searchCriteria)) + } + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchMetrics() + } + + reqURL := url.URL{ + Path: config.MetricPrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var metrics []Metric + if err := json.Unmarshal(result, &metrics); err != nil { + return nil, err + } + + return &metrics, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/metric_cluster.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/metric_cluster.go new file mode 100644 index 000000000..d29c5a674 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/metric_cluster.go @@ -0,0 +1,261 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Metric Cluster API support - Fetch, Create, Update, Delete, and Search +// See: https://login.circonus.com/resources/api/calls/metric_cluster + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// MetricQuery object +type MetricQuery struct { + Query string `json:"query"` + Type string `json:"type"` +} + +// MetricCluster defines a metric cluster. See https://login.circonus.com/resources/api/calls/metric_cluster for more information. +type MetricCluster struct { + CID string `json:"_cid,omitempty"` // string + Description string `json:"description"` // string + MatchingMetrics []string `json:"_matching_metrics,omitempty"` // [] len >= 1 (result info only, if query has extras - cannot be set) + MatchingUUIDMetrics map[string][]string `json:"_matching_uuid_metrics,omitempty"` // [] len >= 1 (result info only, if query has extras - cannot be set) + Name string `json:"name"` // string + Queries []MetricQuery `json:"queries"` // [] len >= 1 + Tags []string `json:"tags"` // [] len >= 0 +} + +// NewMetricCluster returns a new MetricCluster (with defaults, if applicable) +func NewMetricCluster() *MetricCluster { + return &MetricCluster{} +} + +// FetchMetricCluster retrieves metric cluster with passed cid. +func (a *API) FetchMetricCluster(cid CIDType, extras string) (*MetricCluster, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid metric cluster CID [none]") + } + + clusterCID := string(*cid) + + matched, err := regexp.MatchString(config.MetricClusterCIDRegex, clusterCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid metric cluster CID [%s]", clusterCID) + } + + reqURL := url.URL{ + Path: clusterCID, + } + + extra := "" + switch extras { + case "metrics": + extra = "_matching_metrics" + case "uuids": + extra = "_matching_uuid_metrics" + } + + if extra != "" { + q := url.Values{} + q.Set("extra", extra) + reqURL.RawQuery = q.Encode() + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch metric cluster, received JSON: %s", string(result)) + } + + cluster := &MetricCluster{} + if err := json.Unmarshal(result, cluster); err != nil { + return nil, err + } + + return cluster, nil +} + +// FetchMetricClusters retrieves all metric clusters available to API Token. +func (a *API) FetchMetricClusters(extras string) (*[]MetricCluster, error) { + reqURL := url.URL{ + Path: config.MetricClusterPrefix, + } + + extra := "" + switch extras { + case "metrics": + extra = "_matching_metrics" + case "uuids": + extra = "_matching_uuid_metrics" + } + + if extra != "" { + q := url.Values{} + q.Set("extra", extra) + reqURL.RawQuery = q.Encode() + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, err + } + + var clusters []MetricCluster + if err := json.Unmarshal(result, &clusters); err != nil { + return nil, err + } + + return &clusters, nil +} + +// UpdateMetricCluster updates passed metric cluster. +func (a *API) UpdateMetricCluster(cfg *MetricCluster) (*MetricCluster, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid metric cluster config [nil]") + } + + clusterCID := string(cfg.CID) + + matched, err := regexp.MatchString(config.MetricClusterCIDRegex, clusterCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid metric cluster CID [%s]", clusterCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] update metric cluster, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(clusterCID, jsonCfg) + if err != nil { + return nil, err + } + + cluster := &MetricCluster{} + if err := json.Unmarshal(result, cluster); err != nil { + return nil, err + } + + return cluster, nil +} + +// CreateMetricCluster creates a new metric cluster. +func (a *API) CreateMetricCluster(cfg *MetricCluster) (*MetricCluster, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid metric cluster config [nil]") + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] create metric cluster, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Post(config.MetricClusterPrefix, jsonCfg) + if err != nil { + return nil, err + } + + cluster := &MetricCluster{} + if err := json.Unmarshal(result, cluster); err != nil { + return nil, err + } + + return cluster, nil +} + +// DeleteMetricCluster deletes passed metric cluster. +func (a *API) DeleteMetricCluster(cfg *MetricCluster) (bool, error) { + if cfg == nil { + return false, fmt.Errorf("Invalid metric cluster config [nil]") + } + return a.DeleteMetricClusterByCID(CIDType(&cfg.CID)) +} + +// DeleteMetricClusterByCID deletes metric cluster with passed cid. +func (a *API) DeleteMetricClusterByCID(cid CIDType) (bool, error) { + if cid == nil || *cid == "" { + return false, fmt.Errorf("Invalid metric cluster CID [none]") + } + + clusterCID := string(*cid) + + matched, err := regexp.MatchString(config.MetricClusterCIDRegex, clusterCID) + if err != nil { + return false, err + } + if !matched { + return false, fmt.Errorf("Invalid metric cluster CID [%s]", clusterCID) + } + + _, err = a.Delete(clusterCID) + if err != nil { + return false, err + } + + return true, nil +} + +// SearchMetricClusters returns metric clusters matching the specified +// search query and/or filter. If nil is passed for both parameters +// all metric clusters will be returned. +func (a *API) SearchMetricClusters(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]MetricCluster, error) { + q := url.Values{} + + if searchCriteria != nil && *searchCriteria != "" { + q.Set("search", string(*searchCriteria)) + } + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchMetricClusters("") + } + + reqURL := url.URL{ + Path: config.MetricClusterPrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var clusters []MetricCluster + if err := json.Unmarshal(result, &clusters); err != nil { + return nil, err + } + + return &clusters, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/outlier_report.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/outlier_report.go new file mode 100644 index 000000000..bc1a4d2b3 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/outlier_report.go @@ -0,0 +1,221 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// OutlierReport API support - Fetch, Create, Update, Delete, and Search +// See: https://login.circonus.com/resources/api/calls/report + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// OutlierReport defines a outlier report. See https://login.circonus.com/resources/api/calls/report for more information. +type OutlierReport struct { + CID string `json:"_cid,omitempty"` // string + Config string `json:"config,omitempty"` // string + Created uint `json:"_created,omitempty"` // uint + CreatedBy string `json:"_created_by,omitempty"` // string + LastModified uint `json:"_last_modified,omitempty"` // uint + LastModifiedBy string `json:"_last_modified_by,omitempty"` // string + MetricClusterCID string `json:"metric_cluster,omitempty"` // st ring + Tags []string `json:"tags,omitempty"` // [] len >= 0 + Title string `json:"title,omitempty"` // string +} + +// NewOutlierReport returns a new OutlierReport (with defaults, if applicable) +func NewOutlierReport() *OutlierReport { + return &OutlierReport{} +} + +// FetchOutlierReport retrieves outlier report with passed cid. +func (a *API) FetchOutlierReport(cid CIDType) (*OutlierReport, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid outlier report CID [none]") + } + + reportCID := string(*cid) + + matched, err := regexp.MatchString(config.OutlierReportCIDRegex, reportCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid outlier report CID [%s]", reportCID) + } + + result, err := a.Get(reportCID) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch outlier report, received JSON: %s", string(result)) + } + + report := &OutlierReport{} + if err := json.Unmarshal(result, report); err != nil { + return nil, err + } + + return report, nil +} + +// FetchOutlierReports retrieves all outlier reports available to API Token. +func (a *API) FetchOutlierReports() (*[]OutlierReport, error) { + result, err := a.Get(config.OutlierReportPrefix) + if err != nil { + return nil, err + } + + var reports []OutlierReport + if err := json.Unmarshal(result, &reports); err != nil { + return nil, err + } + + return &reports, nil +} + +// UpdateOutlierReport updates passed outlier report. +func (a *API) UpdateOutlierReport(cfg *OutlierReport) (*OutlierReport, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid outlier report config [nil]") + } + + reportCID := string(cfg.CID) + + matched, err := regexp.MatchString(config.OutlierReportCIDRegex, reportCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid outlier report CID [%s]", reportCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] update outlier report, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(reportCID, jsonCfg) + if err != nil { + return nil, err + } + + report := &OutlierReport{} + if err := json.Unmarshal(result, report); err != nil { + return nil, err + } + + return report, nil +} + +// CreateOutlierReport creates a new outlier report. +func (a *API) CreateOutlierReport(cfg *OutlierReport) (*OutlierReport, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid outlier report config [nil]") + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] create outlier report, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Post(config.OutlierReportPrefix, jsonCfg) + if err != nil { + return nil, err + } + + report := &OutlierReport{} + if err := json.Unmarshal(result, report); err != nil { + return nil, err + } + + return report, nil +} + +// DeleteOutlierReport deletes passed outlier report. +func (a *API) DeleteOutlierReport(cfg *OutlierReport) (bool, error) { + if cfg == nil { + return false, fmt.Errorf("Invalid outlier report config [nil]") + } + return a.DeleteOutlierReportByCID(CIDType(&cfg.CID)) +} + +// DeleteOutlierReportByCID deletes outlier report with passed cid. +func (a *API) DeleteOutlierReportByCID(cid CIDType) (bool, error) { + if cid == nil || *cid == "" { + return false, fmt.Errorf("Invalid outlier report CID [none]") + } + + reportCID := string(*cid) + + matched, err := regexp.MatchString(config.OutlierReportCIDRegex, reportCID) + if err != nil { + return false, err + } + if !matched { + return false, fmt.Errorf("Invalid outlier report CID [%s]", reportCID) + } + + _, err = a.Delete(reportCID) + if err != nil { + return false, err + } + + return true, nil +} + +// SearchOutlierReports returns outlier report matching the +// specified search query and/or filter. If nil is passed for +// both parameters all outlier report will be returned. +func (a *API) SearchOutlierReports(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]OutlierReport, error) { + q := url.Values{} + + if searchCriteria != nil && *searchCriteria != "" { + q.Set("search", string(*searchCriteria)) + } + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchOutlierReports() + } + + reqURL := url.URL{ + Path: config.OutlierReportPrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var reports []OutlierReport + if err := json.Unmarshal(result, &reports); err != nil { + return nil, err + } + + return &reports, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/provision_broker.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/provision_broker.go new file mode 100644 index 000000000..5b432a236 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/provision_broker.go @@ -0,0 +1,151 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// ProvisionBroker API support - Fetch, Create, and Update +// See: https://login.circonus.com/resources/api/calls/provision_broker +// Note that the provision_broker endpoint does not return standard cid format +// of '/object/item' (e.g. /provision_broker/abc-123) it just returns 'item' + +package api + +import ( + "encoding/json" + "fmt" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// BrokerStratcon defines stratcons for broker +type BrokerStratcon struct { + CN string `json:"cn,omitempty"` // string + Host string `json:"host,omitempty"` // string + Port string `json:"port,omitempty"` // string +} + +// ProvisionBroker defines a provision broker [request]. See https://login.circonus.com/resources/api/calls/provision_broker for more details. +type ProvisionBroker struct { + Cert string `json:"_cert,omitempty"` // string + CID string `json:"_cid,omitempty"` // string + CSR string `json:"_csr,omitempty"` // string + ExternalHost string `json:"external_host,omitempty"` // string + ExternalPort string `json:"external_port,omitempty"` // string + IPAddress string `json:"ipaddress,omitempty"` // string + Latitude string `json:"latitude,omitempty"` // string + Longitude string `json:"longitude,omitempty"` // string + Name string `json:"noit_name,omitempty"` // string + Port string `json:"port,omitempty"` // string + PreferReverseConnection bool `json:"prefer_reverse_connection,omitempty"` // boolean + Rebuild bool `json:"rebuild,omitempty"` // boolean + Stratcons []BrokerStratcon `json:"_stratcons,omitempty"` // [] len >= 1 + Tags []string `json:"tags,omitempty"` // [] len >= 0 +} + +// NewProvisionBroker returns a new ProvisionBroker (with defaults, if applicable) +func NewProvisionBroker() *ProvisionBroker { + return &ProvisionBroker{} +} + +// FetchProvisionBroker retrieves provision broker [request] with passed cid. +func (a *API) FetchProvisionBroker(cid CIDType) (*ProvisionBroker, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid provision broker request CID [none]") + } + + brokerCID := string(*cid) + + matched, err := regexp.MatchString(config.ProvisionBrokerCIDRegex, brokerCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid provision broker request CID [%s]", brokerCID) + } + + result, err := a.Get(brokerCID) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch broker provision request, received JSON: %s", string(result)) + } + + broker := &ProvisionBroker{} + if err := json.Unmarshal(result, broker); err != nil { + return nil, err + } + + return broker, nil +} + +// UpdateProvisionBroker updates a broker definition [request]. +func (a *API) UpdateProvisionBroker(cid CIDType, cfg *ProvisionBroker) (*ProvisionBroker, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid provision broker request config [nil]") + } + + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid provision broker request CID [none]") + } + + brokerCID := string(*cid) + + matched, err := regexp.MatchString(config.ProvisionBrokerCIDRegex, brokerCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid provision broker request CID [%s]", brokerCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] update broker provision request, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(brokerCID, jsonCfg) + if err != nil { + return nil, err + } + + broker := &ProvisionBroker{} + if err := json.Unmarshal(result, broker); err != nil { + return nil, err + } + + return broker, nil +} + +// CreateProvisionBroker creates a new provison broker [request]. +func (a *API) CreateProvisionBroker(cfg *ProvisionBroker) (*ProvisionBroker, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid provision broker request config [nil]") + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] create broker provision request, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Post(config.ProvisionBrokerPrefix, jsonCfg) + if err != nil { + return nil, err + } + + broker := &ProvisionBroker{} + if err := json.Unmarshal(result, broker); err != nil { + return nil, err + } + + return broker, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/rule_set.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/rule_set.go new file mode 100644 index 000000000..3da0907f7 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/rule_set.go @@ -0,0 +1,234 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Rule Set API support - Fetch, Create, Update, Delete, and Search +// See: https://login.circonus.com/resources/api/calls/rule_set + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// RuleSetRule defines a ruleset rule +type RuleSetRule struct { + Criteria string `json:"criteria"` // string + Severity uint `json:"severity"` // uint + Value interface{} `json:"value"` // BUG doc: string, api: actual type returned switches based on Criteria + Wait uint `json:"wait"` // uint + WindowingDuration uint `json:"windowing_duration,omitempty"` // uint + WindowingFunction *string `json:"windowing_function,omitempty"` // string or null +} + +// RuleSet defines a ruleset. See https://login.circonus.com/resources/api/calls/rule_set for more information. +type RuleSet struct { + CheckCID string `json:"check"` // string + CID string `json:"_cid,omitempty"` // string + ContactGroups map[uint8][]string `json:"contact_groups"` // [] len 5 + Derive *string `json:"derive,omitempty"` // string or null + Link *string `json:"link"` // string or null + MetricName string `json:"metric_name"` // string + MetricTags []string `json:"metric_tags"` // [] len >= 0 + MetricType string `json:"metric_type"` // string + Notes *string `json:"notes"` // string or null + Parent *string `json:"parent,omitempty"` // string or null + Rules []RuleSetRule `json:"rules"` // [] len >= 1 + Tags []string `json:"tags"` // [] len >= 0 +} + +// NewRuleSet returns a new RuleSet (with defaults if applicable) +func NewRuleSet() *RuleSet { + return &RuleSet{} +} + +// FetchRuleSet retrieves rule set with passed cid. +func (a *API) FetchRuleSet(cid CIDType) (*RuleSet, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid rule set CID [none]") + } + + rulesetCID := string(*cid) + + matched, err := regexp.MatchString(config.RuleSetCIDRegex, rulesetCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid rule set CID [%s]", rulesetCID) + } + + result, err := a.Get(rulesetCID) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch rule set, received JSON: %s", string(result)) + } + + ruleset := &RuleSet{} + if err := json.Unmarshal(result, ruleset); err != nil { + return nil, err + } + + return ruleset, nil +} + +// FetchRuleSets retrieves all rule sets available to API Token. +func (a *API) FetchRuleSets() (*[]RuleSet, error) { + result, err := a.Get(config.RuleSetPrefix) + if err != nil { + return nil, err + } + + var rulesets []RuleSet + if err := json.Unmarshal(result, &rulesets); err != nil { + return nil, err + } + + return &rulesets, nil +} + +// UpdateRuleSet updates passed rule set. +func (a *API) UpdateRuleSet(cfg *RuleSet) (*RuleSet, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid rule set config [nil]") + } + + rulesetCID := string(cfg.CID) + + matched, err := regexp.MatchString(config.RuleSetCIDRegex, rulesetCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid rule set CID [%s]", rulesetCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] update rule set, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(rulesetCID, jsonCfg) + if err != nil { + return nil, err + } + + ruleset := &RuleSet{} + if err := json.Unmarshal(result, ruleset); err != nil { + return nil, err + } + + return ruleset, nil +} + +// CreateRuleSet creates a new rule set. +func (a *API) CreateRuleSet(cfg *RuleSet) (*RuleSet, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid rule set config [nil]") + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] create rule set, sending JSON: %s", string(jsonCfg)) + } + + resp, err := a.Post(config.RuleSetPrefix, jsonCfg) + if err != nil { + return nil, err + } + + ruleset := &RuleSet{} + if err := json.Unmarshal(resp, ruleset); err != nil { + return nil, err + } + + return ruleset, nil +} + +// DeleteRuleSet deletes passed rule set. +func (a *API) DeleteRuleSet(cfg *RuleSet) (bool, error) { + if cfg == nil { + return false, fmt.Errorf("Invalid rule set config [nil]") + } + return a.DeleteRuleSetByCID(CIDType(&cfg.CID)) +} + +// DeleteRuleSetByCID deletes rule set with passed cid. +func (a *API) DeleteRuleSetByCID(cid CIDType) (bool, error) { + if cid == nil || *cid == "" { + return false, fmt.Errorf("Invalid rule set CID [none]") + } + + rulesetCID := string(*cid) + + matched, err := regexp.MatchString(config.RuleSetCIDRegex, rulesetCID) + if err != nil { + return false, err + } + if !matched { + return false, fmt.Errorf("Invalid rule set CID [%s]", rulesetCID) + } + + _, err = a.Delete(rulesetCID) + if err != nil { + return false, err + } + + return true, nil +} + +// SearchRuleSets returns rule sets matching the specified search +// query and/or filter. If nil is passed for both parameters all +// rule sets will be returned. +func (a *API) SearchRuleSets(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]RuleSet, error) { + q := url.Values{} + + if searchCriteria != nil && *searchCriteria != "" { + q.Set("search", string(*searchCriteria)) + } + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchRuleSets() + } + + reqURL := url.URL{ + Path: config.RuleSetPrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var rulesets []RuleSet + if err := json.Unmarshal(result, &rulesets); err != nil { + return nil, err + } + + return &rulesets, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/rule_set_group.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/rule_set_group.go new file mode 100644 index 000000000..a15743061 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/rule_set_group.go @@ -0,0 +1,231 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// RuleSetGroup API support - Fetch, Create, Update, Delete, and Search +// See: https://login.circonus.com/resources/api/calls/rule_set_group + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// RuleSetGroupFormula defines a formula for raising alerts +type RuleSetGroupFormula struct { + Expression interface{} `json:"expression"` // string or uint BUG doc: string, api: string or numeric + RaiseSeverity uint `json:"raise_severity"` // uint + Wait uint `json:"wait"` // uint +} + +// RuleSetGroupCondition defines conditions for raising alerts +type RuleSetGroupCondition struct { + MatchingSeverities []string `json:"matching_serverities"` // [] len >= 1 + RuleSetCID string `json:"rule_set"` // string +} + +// RuleSetGroup defines a ruleset group. See https://login.circonus.com/resources/api/calls/rule_set_group for more information. +type RuleSetGroup struct { + CID string `json:"_cid,omitempty"` // string + ContactGroups map[uint8][]string `json:"contact_groups"` // [] len == 5 + Formulas []RuleSetGroupFormula `json:"formulas"` // [] len >= 0 + Name string `json:"name"` // string + RuleSetConditions []RuleSetGroupCondition `json:"rule_set_conditions"` // [] len >= 1 + Tags []string `json:"tags"` // [] len >= 0 +} + +// NewRuleSetGroup returns a new RuleSetGroup (with defaults, if applicable) +func NewRuleSetGroup() *RuleSetGroup { + return &RuleSetGroup{} +} + +// FetchRuleSetGroup retrieves rule set group with passed cid. +func (a *API) FetchRuleSetGroup(cid CIDType) (*RuleSetGroup, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid rule set group CID [none]") + } + + groupCID := string(*cid) + + matched, err := regexp.MatchString(config.RuleSetGroupCIDRegex, groupCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid rule set group CID [%s]", groupCID) + } + + result, err := a.Get(groupCID) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch rule set group, received JSON: %s", string(result)) + } + + rulesetGroup := &RuleSetGroup{} + if err := json.Unmarshal(result, rulesetGroup); err != nil { + return nil, err + } + + return rulesetGroup, nil +} + +// FetchRuleSetGroups retrieves all rule set groups available to API Token. +func (a *API) FetchRuleSetGroups() (*[]RuleSetGroup, error) { + result, err := a.Get(config.RuleSetGroupPrefix) + if err != nil { + return nil, err + } + + var rulesetGroups []RuleSetGroup + if err := json.Unmarshal(result, &rulesetGroups); err != nil { + return nil, err + } + + return &rulesetGroups, nil +} + +// UpdateRuleSetGroup updates passed rule set group. +func (a *API) UpdateRuleSetGroup(cfg *RuleSetGroup) (*RuleSetGroup, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid rule set group config [nil]") + } + + groupCID := string(cfg.CID) + + matched, err := regexp.MatchString(config.RuleSetGroupCIDRegex, groupCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid rule set group CID [%s]", groupCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] update rule set group, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(groupCID, jsonCfg) + if err != nil { + return nil, err + } + + groups := &RuleSetGroup{} + if err := json.Unmarshal(result, groups); err != nil { + return nil, err + } + + return groups, nil +} + +// CreateRuleSetGroup creates a new rule set group. +func (a *API) CreateRuleSetGroup(cfg *RuleSetGroup) (*RuleSetGroup, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid rule set group config [nil]") + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] create rule set group, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Post(config.RuleSetGroupPrefix, jsonCfg) + if err != nil { + return nil, err + } + + group := &RuleSetGroup{} + if err := json.Unmarshal(result, group); err != nil { + return nil, err + } + + return group, nil +} + +// DeleteRuleSetGroup deletes passed rule set group. +func (a *API) DeleteRuleSetGroup(cfg *RuleSetGroup) (bool, error) { + if cfg == nil { + return false, fmt.Errorf("Invalid rule set group config [nil]") + } + return a.DeleteRuleSetGroupByCID(CIDType(&cfg.CID)) +} + +// DeleteRuleSetGroupByCID deletes rule set group wiht passed cid. +func (a *API) DeleteRuleSetGroupByCID(cid CIDType) (bool, error) { + if cid == nil || *cid == "" { + return false, fmt.Errorf("Invalid rule set group CID [none]") + } + + groupCID := string(*cid) + + matched, err := regexp.MatchString(config.RuleSetGroupCIDRegex, groupCID) + if err != nil { + return false, err + } + if !matched { + return false, fmt.Errorf("Invalid rule set group CID [%s]", groupCID) + } + + _, err = a.Delete(groupCID) + if err != nil { + return false, err + } + + return true, nil +} + +// SearchRuleSetGroups returns rule set groups matching the +// specified search query and/or filter. If nil is passed for +// both parameters all rule set groups will be returned. +func (a *API) SearchRuleSetGroups(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]RuleSetGroup, error) { + q := url.Values{} + + if searchCriteria != nil && *searchCriteria != "" { + q.Set("search", string(*searchCriteria)) + } + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchRuleSetGroups() + } + + reqURL := url.URL{ + Path: config.RuleSetGroupPrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var groups []RuleSetGroup + if err := json.Unmarshal(result, &groups); err != nil { + return nil, err + } + + return &groups, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/user.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/user.go new file mode 100644 index 000000000..7771991d3 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/user.go @@ -0,0 +1,159 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// User API support - Fetch, Update, and Search +// See: https://login.circonus.com/resources/api/calls/user +// Note: Create and Delete are not supported directly via the User API +// endpoint. See the Account endpoint for inviting and removing users +// from specific accounts. + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// UserContactInfo defines known contact details +type UserContactInfo struct { + SMS string `json:"sms,omitempty"` // string + XMPP string `json:"xmpp,omitempty"` // string +} + +// User defines a user. See https://login.circonus.com/resources/api/calls/user for more information. +type User struct { + CID string `json:"_cid,omitempty"` // string + ContactInfo UserContactInfo `json:"contact_info,omitempty"` // UserContactInfo + Email string `json:"email"` // string + Firstname string `json:"firstname"` // string + Lastname string `json:"lastname"` // string +} + +// FetchUser retrieves user with passed cid. Pass nil for '/user/current'. +func (a *API) FetchUser(cid CIDType) (*User, error) { + var userCID string + + if cid == nil || *cid == "" { + userCID = config.UserPrefix + "/current" + } else { + userCID = string(*cid) + } + + matched, err := regexp.MatchString(config.UserCIDRegex, userCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid user CID [%s]", userCID) + } + + result, err := a.Get(userCID) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch user, received JSON: %s", string(result)) + } + + user := new(User) + if err := json.Unmarshal(result, user); err != nil { + return nil, err + } + + return user, nil +} + +// FetchUsers retrieves all users available to API Token. +func (a *API) FetchUsers() (*[]User, error) { + result, err := a.Get(config.UserPrefix) + if err != nil { + return nil, err + } + + var users []User + if err := json.Unmarshal(result, &users); err != nil { + return nil, err + } + + return &users, nil +} + +// UpdateUser updates passed user. +func (a *API) UpdateUser(cfg *User) (*User, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid user config [nil]") + } + + userCID := string(cfg.CID) + + matched, err := regexp.MatchString(config.UserCIDRegex, userCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid user CID [%s]", userCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] update user, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(userCID, jsonCfg) + if err != nil { + return nil, err + } + + user := &User{} + if err := json.Unmarshal(result, user); err != nil { + return nil, err + } + + return user, nil +} + +// SearchUsers returns users matching a filter (search queries +// are not suppoted by the user endpoint). Pass nil as filter for all +// users available to the API Token. +func (a *API) SearchUsers(filterCriteria *SearchFilterType) (*[]User, error) { + q := url.Values{} + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchUsers() + } + + reqURL := url.URL{ + Path: config.UserPrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var users []User + if err := json.Unmarshal(result, &users); err != nil { + return nil, err + } + + return &users, nil +} diff --git a/vendor/github.com/circonus-labs/circonus-gometrics/api/worksheet.go b/vendor/github.com/circonus-labs/circonus-gometrics/api/worksheet.go new file mode 100644 index 000000000..0dd5e9373 --- /dev/null +++ b/vendor/github.com/circonus-labs/circonus-gometrics/api/worksheet.go @@ -0,0 +1,232 @@ +// Copyright 2016 Circonus, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Worksheet API support - Fetch, Create, Update, Delete, and Search +// See: https://login.circonus.com/resources/api/calls/worksheet + +package api + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + + "github.com/circonus-labs/circonus-gometrics/api/config" +) + +// WorksheetGraph defines a worksheet cid to be include in the worksheet +type WorksheetGraph struct { + GraphCID string `json:"graph"` // string +} + +// WorksheetSmartQuery defines a query to include multiple worksheets +type WorksheetSmartQuery struct { + Name string `json:"name"` + Order []string `json:"order"` + Query string `json:"query"` +} + +// Worksheet defines a worksheet. See https://login.circonus.com/resources/api/calls/worksheet for more information. +type Worksheet struct { + CID string `json:"_cid,omitempty"` // string + Description *string `json:"description"` // string or null + Favorite bool `json:"favorite"` // boolean + Graphs []WorksheetGraph `json:"worksheets,omitempty"` // [] len >= 0 + Notes *string `json:"notes"` // string or null + SmartQueries []WorksheetSmartQuery `json:"smart_queries,omitempty"` // [] len >= 0 + Tags []string `json:"tags"` // [] len >= 0 + Title string `json:"title"` // string +} + +// NewWorksheet returns a new Worksheet (with defaults, if applicable) +func NewWorksheet() *Worksheet { + return &Worksheet{} +} + +// FetchWorksheet retrieves worksheet with passed cid. +func (a *API) FetchWorksheet(cid CIDType) (*Worksheet, error) { + if cid == nil || *cid == "" { + return nil, fmt.Errorf("Invalid worksheet CID [none]") + } + + worksheetCID := string(*cid) + + matched, err := regexp.MatchString(config.WorksheetCIDRegex, worksheetCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid worksheet CID [%s]", worksheetCID) + } + + result, err := a.Get(string(*cid)) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] fetch worksheet, received JSON: %s", string(result)) + } + + worksheet := new(Worksheet) + if err := json.Unmarshal(result, worksheet); err != nil { + return nil, err + } + + return worksheet, nil +} + +// FetchWorksheets retrieves all worksheets available to API Token. +func (a *API) FetchWorksheets() (*[]Worksheet, error) { + result, err := a.Get(config.WorksheetPrefix) + if err != nil { + return nil, err + } + + var worksheets []Worksheet + if err := json.Unmarshal(result, &worksheets); err != nil { + return nil, err + } + + return &worksheets, nil +} + +// UpdateWorksheet updates passed worksheet. +func (a *API) UpdateWorksheet(cfg *Worksheet) (*Worksheet, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid worksheet config [nil]") + } + + worksheetCID := string(cfg.CID) + + matched, err := regexp.MatchString(config.WorksheetCIDRegex, worksheetCID) + if err != nil { + return nil, err + } + if !matched { + return nil, fmt.Errorf("Invalid worksheet CID [%s]", worksheetCID) + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] update worksheet, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Put(worksheetCID, jsonCfg) + if err != nil { + return nil, err + } + + worksheet := &Worksheet{} + if err := json.Unmarshal(result, worksheet); err != nil { + return nil, err + } + + return worksheet, nil +} + +// CreateWorksheet creates a new worksheet. +func (a *API) CreateWorksheet(cfg *Worksheet) (*Worksheet, error) { + if cfg == nil { + return nil, fmt.Errorf("Invalid worksheet config [nil]") + } + + jsonCfg, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + if a.Debug { + a.Log.Printf("[DEBUG] create annotation, sending JSON: %s", string(jsonCfg)) + } + + result, err := a.Post(config.WorksheetPrefix, jsonCfg) + if err != nil { + return nil, err + } + + worksheet := &Worksheet{} + if err := json.Unmarshal(result, worksheet); err != nil { + return nil, err + } + + return worksheet, nil +} + +// DeleteWorksheet deletes passed worksheet. +func (a *API) DeleteWorksheet(cfg *Worksheet) (bool, error) { + if cfg == nil { + return false, fmt.Errorf("Invalid worksheet config [nil]") + } + return a.DeleteWorksheetByCID(CIDType(&cfg.CID)) +} + +// DeleteWorksheetByCID deletes worksheet with passed cid. +func (a *API) DeleteWorksheetByCID(cid CIDType) (bool, error) { + if cid == nil || *cid == "" { + return false, fmt.Errorf("Invalid worksheet CID [none]") + } + + worksheetCID := string(*cid) + + matched, err := regexp.MatchString(config.WorksheetCIDRegex, worksheetCID) + if err != nil { + return false, err + } + if !matched { + return false, fmt.Errorf("Invalid worksheet CID [%s]", worksheetCID) + } + + _, err = a.Delete(worksheetCID) + if err != nil { + return false, err + } + + return true, nil +} + +// SearchWorksheets returns worksheets matching the specified search +// query and/or filter. If nil is passed for both parameters all +// worksheets will be returned. +func (a *API) SearchWorksheets(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]Worksheet, error) { + q := url.Values{} + + if searchCriteria != nil && *searchCriteria != "" { + q.Set("search", string(*searchCriteria)) + } + + if filterCriteria != nil && len(*filterCriteria) > 0 { + for filter, criteria := range *filterCriteria { + for _, val := range criteria { + q.Add(filter, val) + } + } + } + + if q.Encode() == "" { + return a.FetchWorksheets() + } + + reqURL := url.URL{ + Path: config.WorksheetPrefix, + RawQuery: q.Encode(), + } + + result, err := a.Get(reqURL.String()) + if err != nil { + return nil, fmt.Errorf("[ERROR] API call error %+v", err) + } + + var worksheets []Worksheet + if err := json.Unmarshal(result, &worksheets); err != nil { + return nil, err + } + + return &worksheets, nil +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 31358ff5f..7b4ee76c0 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1171,6 +1171,18 @@ "revision": "7bfb7937d106522a9c6d659864dca47cddcccc8a", "revisionTime": "2017-01-10T09:44:45Z" }, + { + "checksumSHA1": "PDusd0EuHz0oKiQKwKxFhbETxd8=", + "path": "github.com/circonus-labs/circonus-gometrics/api", + "revision": "dbab9a33438e3f8317407ef5d3a51c29340541db", + "revisionTime": "2017-02-21T20:27:28Z" + }, + { + "checksumSHA1": "bQhz/fcyZPmuHSH2qwC4ZtATy5c=", + "path": "github.com/circonus-labs/circonus-gometrics/api/config", + "revision": "dbab9a33438e3f8317407ef5d3a51c29340541db", + "revisionTime": "2017-02-21T20:27:28Z" + }, { "checksumSHA1": "QhYMdplKQJAMptRaHZBB8CF6HdM=", "path": "github.com/cloudflare/cloudflare-go", diff --git a/website/source/assets/stylesheets/_docs.scss b/website/source/assets/stylesheets/_docs.scss index 76a7c80e0..226d1a2fd 100755 --- a/website/source/assets/stylesheets/_docs.scss +++ b/website/source/assets/stylesheets/_docs.scss @@ -18,6 +18,7 @@ body.layout-azure, body.layout-bitbucket, body.layout-chef, body.layout-azurerm, +body.layout-circonus, body.layout-clc, body.layout-cloudflare, body.layout-cloudstack, diff --git a/website/source/docs/import/importability.html.md b/website/source/docs/import/importability.html.md index 39e6908e1..e1949f83f 100644 --- a/website/source/docs/import/importability.html.md +++ b/website/source/docs/import/importability.html.md @@ -119,6 +119,11 @@ To make a resource importable, please see the * azurerm_storage_account * azurerm_virtual_network +### Circonus + +* circonus_check +* circonus_contact_group + ### DigitalOcean * digitalocean_domain diff --git a/website/source/docs/providers/circonus/d/account.html.markdown b/website/source/docs/providers/circonus/d/account.html.markdown new file mode 100644 index 000000000..e1560c3aa --- /dev/null +++ b/website/source/docs/providers/circonus/d/account.html.markdown @@ -0,0 +1,82 @@ +--- +layout: "circonus" +page_title: "Circonus: account" +sidebar_current: "docs-circonus-datasource-account" +description: |- + Provides details about a specific Circonus Account. +--- + +# circonus_account + +`circonus_account` provides +[details](https://login.circonus.com/resources/api/calls/account) about a specific +[Circonus Account](https://login.circonus.com/user/docs/Administration/Account). + +The `circonus_account` data source can be used for pulling various attributes +about a specific Circonus Account. + +## Example Usage + +The following example shows how the resource might be used to obtain the metrics +usage and limit of a given Circonus Account. + +``` +data "circonus_account" "current" { + current = true +} +``` + +## Argument Reference + +The arguments of this data source act as filters for querying the available +regions. The given filters must match exactly one region whose data will be +exported as attributes. + +* `id` - (Optional) The Circonus ID of a given account. +* `current` - (Optional) Automatically use the current Circonus Account attached + to the API token making the request. + +At least one of the above attributes should be provided when searching for a +account. + +## Attributes Reference + +The following attributes are exported: + +* `address1` - The first line of the address associated with the account. + +* `address2` - The second line of the address associated with the account. + +* `cc_email` - An optionally specified email address used in the CC line of invoices. + +* `id` - The Circonus ID of the selected Account. + +* `city` - The city part of the address associated with the account. + +* `contact_groups` - A list of IDs for each contact group in the account. + +* `country` - The country of the user's address. + +* `description` - Description of the account. + +* `invites` - An list of users invited to use the platform. Each element in the + list has both an `email` and `role` attribute. + +* `name` - The name of the account. + +* `owner` - The Circonus ID of the user who owns this account. + +* `state_prov` - The state or province of the address associated with the account. + +* `timezone` - The timezone that events will be displayed in the web interface + for this account. + +* `ui_base_url` - The base URL of this account. + +* `usage` - A list of account usage limits. Each element in the list will have + a `limit` attribute, a limit `type`, and a `used` attribute. + +* `users` - A list of users who have access to this account. Each element in + the list has both an `id` and a `role`. The `id` is a Circonus ID referencing + the user. + diff --git a/website/source/docs/providers/circonus/d/collector.html.markdown b/website/source/docs/providers/circonus/d/collector.html.markdown new file mode 100644 index 000000000..3f7be5919 --- /dev/null +++ b/website/source/docs/providers/circonus/d/collector.html.markdown @@ -0,0 +1,98 @@ +--- +layout: "circonus" +page_title: "Circonus: collector" +sidebar_current: "docs-circonus-datasource-collector" +description: |- + Provides details about a specific Circonus Collector. +--- + +# circonus_collector + +`circonus_collector` provides +[details](https://login.circonus.com/resources/api/calls/broker) about a specific +[Circonus Collector](https://login.circonus.com/user/docs/Administration/Brokers). + +As well as validating a given Circonus ID, this resource can be used to discover +the additional details about a collector configured within the provider. The +results of a `circonus_collector` API call can return more than one collector +per Circonus ID. Details of each individual collector in the group of +collectors can be found via the `details` attribute described below. + +~> **NOTE regarding `cirocnus_collector`:** The `circonus_collector` data source +actually queries and operates on Circonus "brokers" at the broker group level. +The `circonus_collector` is simply a renamed Circonus "broker" to make it clear +what the function of the "broker" actually does: act as a fan-in agent that +either pulls or has metrics pushed into it and funneled back through Circonus. + +## Example Usage + +The following example shows how the resource might be used to obtain +the name of the Circonus Collector configured on the provider. + +``` +data "circonus_collector" "ashburn" { + id = "/broker/1" +} +``` + +## Argument Reference + +The arguments of this data source act as filters for querying the available +regions. The given filters must match exactly one region whose data will be +exported as attributes. + +* `id` - (Optional) The Circonus ID of a given collector. + +At least one of the above attributes should be provided when searching for a +collector. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The Circonus ID of the selected Collector. + +* `details` - A list of details about the individual Collector instances that + make up the group of collectors. See below for a list of attributes within + each collector. + +* `latitude` - The latitude of the selected Collector. + +* `longitude` - The longitude of the selected Collector. + +* `name` - The name of the selected Collector. + +* `tags` - A list of tags assigned to the selected Collector. + +* `type` - The of the selected Collector. This value is either `circonus` for a + Circonus-managed, public Collector, or `enterprise` for a private collector that is + private to an account. + +## Collector Details + +* `cn` - The CN of an individual Collector in the Collector Group. + +* `external_host` - The external host information for an individual Collector in + the Collector Group. This is useful or important when talking with a Collector + through a NAT'ing firewall. + +* `external_port` - The external port number for an individual Collector in the + Collector Group. This is useful or important when talking with a Collector through + a NAT'ing firewall. + +* `ip` - The IP address of an individual Collector in the Collector Group. This is + the IP address of the interface listening on the network. + +* `min_version` - ?? + +* `modules` - A list of what modules (types of checks) this collector supports. + +* `port` - The port the collector responds to the Circonus HTTPS REST wire protocol + on. + +* `skew` - The clock drift between this collector and the Circonus server. + +* `status` - The status of this particular collector. A string containing either + `active`, `unprovisioned`, `pending`, `provisioned`, or `retired`. + +* `version` - The version of the collector software the collector is running. diff --git a/website/source/docs/providers/circonus/index.html.markdown b/website/source/docs/providers/circonus/index.html.markdown new file mode 100644 index 000000000..6652443d8 --- /dev/null +++ b/website/source/docs/providers/circonus/index.html.markdown @@ -0,0 +1,28 @@ +--- +layout: "circonus" +page_title: "Provider: Circonus" +sidebar_current: "docs-circonus-index" +description: |- + A provider for Circonus. +--- + +# Circonus Provider + +The Circonus provider gives the ability to manage a Circonus account. + +Use the navigation to the left to read about the available resources. + +## Usage + +``` +provider "circonus" { + key = "b8fec159-f9e5-4fe6-ad2c-dc1ec6751586" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `key` - (Required) The Circonus API Key. +* `api_url` - (Optional) The API URL to use to talk with. The default is `https://api.circonus.com/v2`. diff --git a/website/source/docs/providers/circonus/r/check.html.markdown b/website/source/docs/providers/circonus/r/check.html.markdown new file mode 100644 index 000000000..6879e9f10 --- /dev/null +++ b/website/source/docs/providers/circonus/r/check.html.markdown @@ -0,0 +1,520 @@ +--- +layout: "circonus" +page_title: "Circonus: circonus_check" +sidebar_current: "docs-circonus-resource-circonus_check" +description: |- + Manages a Circonus check. +--- + +# circonus\_check + +The ``circonus_check`` resource creates and manages a +[Circonus Check](https://login.circonus.com/resources/api/calls/check_bundle). + +~> **NOTE regarding `cirocnus_check` vs a Circonus Check Bundle:** The +`circonus_check` resource is implemented in terms of a +[Circonus Check Bundle](https://login.circonus.com/resources/api/calls/check_bundle). +The `circonus_check` creates a higher-level abstraction over the implementation +of a Check Bundle. As such, the naming and structure does not map 1:1 with the +underlying Circonus API. + +## Usage + +``` +variable api_token { + default = "my-token" +} + +resource "circonus_check" "usage" { + name = "Circonus Usage Check" + + notes = <<-EOF +A check to extract a usage metric. +EOF + + collector { + id = "/broker/1" + } + + metric { + name = "${circonus_metric.used.name}" + tags = "${circonus_metric.used.tags}" + type = "${circonus_metric.used.type}" + unit = "${circonus_metric.used.unit}" + } + + json { + url = "https://api.circonus.com/v2" + + http_headers = { + Accept = "application/json" + X-Circonus-App-Name = "TerraformCheck" + X-Circonus-Auth-Token = "${var.api_token}" + } + } + + period = 60 + tags = ["source:circonus", "author:terraform"] + timeout = 10 +} + +resource "circonus_metric" "used" { + name = "_usage`0`_used" + type = "numeric" + unit = "qty" + + tags = { + source = "circonus" + } +} +``` + +## Argument Reference + +* `active` - (Optional) Whether or not the check is enabled or not (default + `true`). + +* `caql` - (Optional) A [Circonus Analytics Query Language + (CAQL)](https://login.circonus.com/user/docs/CAQL) check. See below for + details on how to configure a `caql` check. + +* `cloudwatch` - (Optional) A [CloudWatch + check](https://login.circonus.com/user/docs/Data/CheckTypes/CloudWatch) check. + See below for details on how to configure a `cloudwatch` check. + +* `collector` - (Required) A collector ID. The collector(s) that are + responsible for running a `circonus_check`. The `id` can be the Circonus ID + for a Circonus collector (a.k.a. "broker") running in the cloud or an + enterprise collector running in your datacenter. One collection of metrics + will be automatically created for each `collector` specified. + +* `http` - (Optional) A poll-based HTTP check. See below for details on how to configure + the `http` check. + +* `httptrap` - (Optional) An push-based HTTP check. This check method expects + clients to send a specially crafted HTTP JSON payload. See below for details + on how to configure the `httptrap` check. + +* `icmp_ping` - (Optional) An ICMP ping check. See below for details on how to + configure the `icmp_ping` check. + +* `json` - (Optional) A JSON check. See below for details on how to configure + the `json` check. + +* `metric_limit` - (Optional) Setting a metric limit will tell the Circonus + backend to periodically look at the check to see if there are additional + metrics the collector has seen that we should collect. It will not reactivate + metrics previously collected and then marked as inactive. Values are `0` to + disable, `-1` to enable all metrics or `N+` to collect up to the value `N` + (both `-1` and `N+` can not exceed other account restrictions). + +* `mysql` - (Optional) A MySQL check. See below for details on how to configure + the `mysql` check. + +* `name` - (Optional) The name of the check that will be displayed in the web + interface. + +* `notes` - (Optional) Notes about this check. + +* `period` - (Optional) The period between each time the check is made in + seconds. + +* `postgresql` - (Optional) A PostgreSQL check. See below for details on how to + configure the `postgresql` check. + +* `metric` - (Required) A list of one or more `metric` configurations. All + metrics obtained from this check instance will be available as individual + metric streams. See below for a list of supported `metric` attrbutes. + +* `tags` - (Optional) A list of tags assigned to this check. + +* `target` - (Required) A string containing the location of the thing being + checked. This value changes based on the check type. For example, for an + `http` check type this would be the URL you're checking. For a DNS check it + would be the hostname you wanted to look up. + +* `tcp` - (Optional) A TCP check. See below for details on how to configure the + `tcp` check (includes TLS support). + +* `timeout` - (Optional) A floating point number representing the maximum number + of seconds this check should wait for a result. Defaults to `10.0`. + +## Supported `metric` Attributes + +The following attributes are available within a `metric`. + +* `active` - (Optional) Whether or not the metric is active or not. Defaults to `true`. +* `name` - (Optional) The name of the metric. A string containing freeform text. +* `tags` - (Optional) A list of tags assigned to the metric. +* `type` - (Required) A string containing either `numeric`, `text`, `histogram`, `composite`, or `caql`. +* `units` - (Optional) The unit of measurement the metric represents (e.g., bytes, seconds, milliseconds). A string containing freeform text. + +## Supported Check Types + +Circonus supports a variety of different checks. Each check type has its own +set of options that must be configured. Each check type conflicts with every +other check type (i.e. a `circonus_check` configured for a `json` check will +conflict with all other check types, therefore a `postgresql` check must be a +different `circonus_check` resource). + +### `caql` Check Type Attributes + +* `query` - (Required) The [CAQL + Query](https://login.circonus.com/user/docs/caql_reference) to run. + +Available metrics depend on the payload returned in the `caql` check. See the +[`caql` check type](https://login.circonus.com/resources/api/calls/check_bundle) for +additional details. + +### `cloudwatch` Check Type Attributes + +* `api_key` - (Required) The AWS access key. If this value is not explicitly + set, this value is populated by the environment variable `AWS_ACCESS_KEY_ID`. + +* `api_secret` - (Required) The AWS secret key. If this value is not explicitly + set, this value is populated by the environment variable `AWS_SECRET_ACCESS_KEY`. + +* `dimmensions` - (Required) A map of the CloudWatch dimmensions to include in + the check. + +* `metric` - (Required) A list of metric names to collect in this check. + +* `namespace` - (Required) The namespace to pull parameters from. + +* `url` - (Required) The AWS URL to pull from. This should be set to the + region-specific endpoint (e.g. prefer + `https://monitoring.us-east-1.amazonaws.com` over + `https://monitoring.amazonaws.com`). + +* `version` - (Optional) The version of the Cloudwatch API to use. Defaults to + `2010-08-01`. + +Available metrics depend on the payload returned in the `cloudwatch` check. See the +[`cloudwatch` check type](https://login.circonus.com/resources/api/calls/check_bundle) for +additional details. The `circonus_check` `period` attribute must be set to +either `60s` or `300s` for CloudWatch metrics. + +Example CloudWatch check (partial metrics collection): + +``` +variable "cloudwatch_rds_tags" { + type = "list" + default = [ + "app:postgresql", + "app:rds", + "source:cloudwatch", + ] +} + +resource "circonus_check" "rds_metrics" { + active = true + name = "Terraform test: RDS Metrics via CloudWatch" + notes = "Collect RDS metrics" + period = "60s" + + collector { + id = "/broker/1" + } + + cloudwatch { + dimmensions = { + DBInstanceIdentifier = "my-db-name", + } + + metric = [ + "CPUUtilization", + "DatabaseConnections", + ] + + namespace = "AWS/RDS" + url = "https://monitoring.us-east-1.amazonaws.com" + } + + metric { + name = "CPUUtilization" + tags = [ "${var.cloudwatch_rds_tags}" ] + type = "numeric" + unit = "%" + } + + metric { + name = "DatabaseConnections" + tags = [ "${var.cloudwatch_rds_tags}" ] + type = "numeric" + unit = "connections" + } +} +``` + +### `http` Check Type Attributes + +* `auth_method` - (Optional) HTTP Authentication method to use. When set must + be one of the values `Basic`, `Digest`, or `Auto`. + +* `auth_password` - (Optional) The password to use during authentication. + +* `auth_user` - (Optional) The user to authenticate as. + +* `body_regexp` - (Optional) This regular expression is matched against the body + of the response. If a match is not found, the check will be marked as "bad." + +* `ca_chain` - (Optional) A path to a file containing all the certificate + authorities that should be loaded to validate the remote certificate (for TLS + checks). + +* `certificate_file` - (Optional) A path to a file containing the client + certificate that will be presented to the remote server (for TLS checks). + +* `ciphers` - (Optional) A list of ciphers to be used in the TLS protocol (for + HTTPS checks). + +* `code` - (Optional) The HTTP code that is expected. If the code received does + not match this regular expression, the check is marked as "bad." + +* `extract` - (Optional) This regular expression is matched against the body of + the response globally. The first capturing match is the key and the second + capturing match is the value. Each key/value extracted is registered as a + metric for the check. + +* `headers` - (Optional) A map of the HTTP headers to be sent when executing the + check. + +* `key_file` - (Optional) A path to a file containing key to be used in + conjunction with the cilent certificate (for TLS checks). + +* `method` - (Optional) The HTTP Method to use. Defaults to `GET`. + +* `payload` - (Optional) The information transferred as the payload of an HTTP + request. + +* `read_limit` - (Optional) Sets an approximate limit on the data read (`0` + means no limit). Default `0`. + +* `redirects` - (Optional) The maximum number of HTTP `Location` header + redirects to follow. Default `0`. + +* `url` - (Required) The target for this `json` check. The `url` must include + the scheme, host, port (optional), and path to use + (e.g. `https://app1.example.org/healthz`) + +* `version` - (Optional) The HTTP version to use. Defaults to `1.1`. + +Available metrics include: `body_match`, `bytes`, `cert_end`, `cert_end_in`, +`cert_error`, `cert_issuer`, `cert_start`, `cert_subject`, `code`, `duration`, +`truncated`, `tt_connect`, and `tt_firstbyte`. See the +[`http` check type](https://login.circonus.com/resources/api/calls/check_bundle) for +additional details. + +### `httptrap` Check Type Attributes + +* `async_metrics` - (Optional) Boolean value specifies whether or not httptrap + metrics are logged immediately or held until the status message is to be + emitted. Default `false`. + +* `secret` - (Optional) Specify the secret with which metrics may be + submitted. + +Available metrics depend on the payload returned in the `httptrap` doc. See +the [`httptrap` check type](https://login.circonus.com/resources/api/calls/check_bundle) +for additional details. + +### `json` Check Type Attributes + +* `auth_method` - (Optional) HTTP Authentication method to use. When set must + be one of the values `Basic`, `Digest`, or `Auto`. + +* `auth_password` - (Optional) The password to use during authentication. + +* `auth_user` - (Optional) The user to authenticate as. + +* `ca_chain` - (Optional) A path to a file containing all the certificate + authorities that should be loaded to validate the remote certificate (for TLS + checks). + +* `certificate_file` - (Optional) A path to a file containing the client + certificate that will be presented to the remote server (for TLS checks). + +* `ciphers` - (Optional) A list of ciphers to be used in the TLS protocol (for + HTTPS checks). + +* `headers` - (Optional) A map of the HTTP headers to be sent when executing the + check. + +* `key_file` - (Optional) A path to a file containing key to be used in + conjunction with the cilent certificate (for TLS checks). + +* `method` - (Optional) The HTTP Method to use. Defaults to `GET`. + +* `port` - (Optional) The TCP Port number to use. Defaults to `81`. + +* `read_limit` - (Optional) Sets an approximate limit on the data read (`0` + means no limit). Default `0`. + +* `redirects` - (Optional) The maximum number of HTTP `Location` header + redirects to follow. Default `0`. + +* `url` - (Required) The target for this `json` check. The `url` must include + the scheme, host, port (optional), and path to use + (e.g. `https://app1.example.org/healthz`) + +* `version` - (Optional) The HTTP version to use. Defaults to `1.1`. + +Available metrics depend on the payload returned in the `json` doc. See the +[`json` check type](https://login.circonus.com/resources/api/calls/check_bundle) for +additional details. + +### `icmp_ping` Check Type Attributes + +The `icmp_ping` check requires the `target` top-level attribute to be set. + +* `availability` - (Optional) The percentage of ping packets that must be + returned for this measurement to be considered successful. Defaults to + `100.0`. +* `count` - (Optional) The number of ICMP ping packets to send. Defaults to + `5`. +* `interval` - (Optional) Interval between packets. Defaults to `2s`. + +Available metrics include: `available`, `average`, `count`, `maximum`, and +`minimum`. See the +[`ping_icmp` check type](https://login.circonus.com/resources/api/calls/check_bundle) +for additional details. + +### `mysql` Check Type Attributes + +The `mysql` check requires the `target` top-level attribute to be set. + +* `dsn` - (Required) The [MySQL DSN/connect + string](https://github.com/go-sql-driver/mysql/blob/master/README.md) to + use to talk to MySQL. +* `query` - (Required) The SQL query to execute. + +### `postgresql` Check Type Attributes + +The `postgresql` check requires the `target` top-level attribute to be set. + +* `dsn` - (Required) The [PostgreSQL DSN/connect + string](https://www.postgresql.org/docs/current/static/libpq-connect.html) to + use to talk to PostgreSQL. +* `query` - (Required) The SQL query to execute. + +Available metric names are dependent on the output of the `query` being run. + +### `tcp` Check Type Attributes + +* `banner_regexp` - (Optional) This regular expression is matched against the + response banner. If a match is not found, the check will be marked as bad. + +* `ca_chain` - (Optional) A path to a file containing all the certificate + authorities that should be loaded to validate the remote certificate (for TLS + checks). + +* `certificate_file` - (Optional) A path to a file containing the client + certificate that will be presented to the remote server (for TLS checks). + +* `ciphers` - (Optional) A list of ciphers to be used in the TLS protocol (for + HTTPS checks). + +* `host` - (Required) Hostname or IP address of the host to connect to. + +* `key_file` - (Optional) A path to a file containing key to be used in + conjunction with the cilent certificate (for TLS checks). + +* `port` - (Required) Integer specifying the port on which the management + interface can be reached. + +* `tls` - (Optional) When enabled establish a TLS connection. + +Available metrics include: `banner`, `banner_match`, `cert_end`, `cert_end_in`, +`cert_error`, `cert_issuer`, `cert_start`, `cert_subject`, `duration`, +`tt_connect`, `tt_firstbyte`. See the +[`tcp` check type](https://login.circonus.com/resources/api/calls/check_bundle) +for additional details. + +Sample `tcp` check: + +``` +resource "circonus_check" "tcp_check" { + name = "TCP and TLS check" + notes = "Obtains the connect time and TTL for the TLS cert" + period = "60s" + + collector { + id = "/broker/1" + } + + tcp { + host = "127.0.0.1" + port = 443 + tls = true + } + + metric { + name = "cert_end_in" + tags = [ "${var.tcp_check_tags}" ] + type = "numeric" + unit = "seconds" + } + + metric { + name = "tt_connect" + tags = [ "${var.tcp_check_tags}" ] + type = "numeric" + unit = "miliseconds" + } + + tags = [ "${var.tcp_check_tags}" ] +} +``` + +## Out Parameters + +* `check_by_collector` - Map of each check (value) that was created for every + specified broker (key). + +## Import Example + +`circonus_check` supports importing resources. Supposing the following +Terraform (and that the referenced [`circonus_metric`](metric.html) has already +been imported): + +``` +provider "circonus" { + alias = "b8fec159-f9e5-4fe6-ad2c-dc1ec6751586" +} + +resource "circonus_metric" "used" { + name = "_usage`0`_used" + type = "numeric" +} + +resource "circonus_check" "usage" { + collector { + id = "/broker/1" + } + + json { + url = "https://api.circonus.com/account/current" + + http_headers = { + "Accept" = "application/json" + "X-Circonus-App-Name" = "TerraformCheck" + "X-Circonus-Auth-Token" = "${var.api_token}" + } + } + + metric { + name = "${circonus_metric.used.name}" + type = "${circonus_metric.used.type}" + } +} +``` + +It is possible to import a `circonus_check` resource with the following command: + +``` +$ terraform import circonus_check.usage ID +``` + +Where `ID` is the `_cid` or Circonus ID of the Check Bundle +(e.g. `/check_bundle/12345`) and `circonus_check.usage` is the name of the +resource whose state will be populated as a result of the command. diff --git a/website/source/docs/providers/circonus/r/contact_group.html.markdown b/website/source/docs/providers/circonus/r/contact_group.html.markdown new file mode 100644 index 000000000..49a080da7 --- /dev/null +++ b/website/source/docs/providers/circonus/r/contact_group.html.markdown @@ -0,0 +1,289 @@ +--- +layout: "circonus" +page_title: "Circonus: circonus_contact_group" +sidebar_current: "docs-circonus-resource-circonus_contact_group" +description: |- + Manages a Circonus Contact Group. +--- + +# circonus\_contact_group + +The ``circonus_contact_group`` resource creates and manages a +[Circonus Contact Group](https://login.circonus.com/user/docs/Alerting/ContactGroups). + + +## Usage + +``` +resource "circonus_contact_group" "myteam-alerts" { + name = "MyTeam Alerts" + + email { + user = "/user/1234" + } + + email { + user = "/user/5678" + } + + email { + address = "user@example.com" + } + + http { + address = "https://www.example.org/post/endpoint" + format = "json" + method = "POST" + } + + irc { + user = "/user/6331" + } + + slack { + channel = "#myteam" + team = "T038UT13D" + } + + sms { + user = "/user/1234" + } + + sms { + address = "8005551212" + } + + victorops { + api_key = "xxxx" + critical = 2 + info = 5 + team = "myteam" + warning = 3 + } + + xmpp { + user = "/user/9876" + } + + aggregation_window = "5m" + + alert_option { + severity = 1 + reminder = "5m" + escalate_to = "/contact_group/4444" + } + + alert_option { + severity = 2 + reminder = "15m" + escalate_after = "2h" + escalate_to = "/contact_group/4444" + } + + alert_option { + severity = 3 + reminder = "24m" + escalate_after = "3d" + escalate_to = "/contact_group/4444" + } +} +``` + +## Argument Reference + +* `aggregation_window` - (Optional) The aggregation window for batching up alert + notifications. + +* `alert_option` - (Optional) There is one `alert_option` per severity, where + severity can be any number between 1 (high) and 5 (low). If configured, the + alerting system will remind or escalate alerts to further contact groups if an + alert sent to this contact group is not acknowledged or resolved. See below + for details. + +* `email` - (Optional) Zero or more `email` attributes may be present to + dispatch email to Circonus users by referencing their user ID, or by + specifying an email address. See below for details on supported attributes. + +* `http` - (Optional) Zero or more `http` attributes may be present to dispatch + [Webhook/HTTP requests](https://login.circonus.com/user/docs/Alerting/ContactGroups#WebhookNotifications) + by Circonus. See below for details on supported attributes. + +* `irc` - (Optional) Zero or more `irc` attributes may be present to dispatch + IRC notifications to users. See below for details on supported attributes. + +* `long_message` - (Optional) The bulk of the message used in long form alert + messages. + +* `long_subject` - (Optional) The subject used in long form alert messages. + +* `long_summary` - (Optional) The brief summary used in long form alert messages. + +* `name` - (Required) The name of the contact group. + +* `pager_duty` - (Optional) Zero or more `pager_duty` attributes may be present + to dispatch to + [Pager Duty teams](https://login.circonus.com/user/docs/Alerting/ContactGroups#PagerDutyOptions). + See below for details on supported attributes. + +* `short_message` - (Optional) The subject used in short form alert messages. + +* `short_summary` - (Optional) The brief summary used in short form alert + messages. + +* `slack` - (Optional) Zero or more `pager_duty` attributes may be present to + dispatch to Pager Duty teams. See below for details on supported attributes. + +* `sms` - (Optional) Zero or more `sms` attributes may be present to dispatch + SMS messages to Circonus users by referencing their user ID, or by specifying + an SMS Phone Number. See below for details on supported attributes. + +* `tags` - (Optional) A list of tags attached to the Contact Group. + +* `victorops` - (Optional) Zero or more `victorops` attributes may be present + to dispatch to + [VictorOps teams](https://login.circonus.com/user/docs/Alerting/ContactGroups#VictorOps). + See below for details on supported attributes. + +## Supported Contact Group `alert_option` Attributes + +* `escalate_after` - (Optional) How long to wait before escalating an alert that + is received at a given severity. + +* `escalate_to` - (Optional) The Contact Group ID who will receive the + escalation. + +* `reminder` - (Optional) If specified, reminders will be sent after a user + configurable number of minutes for open alerts. + +* `severity` - (Required) An `alert_option` must be assigned to a given severity + level. Valid severity levels range from 1 (highest severity) to 5 (lowest + severity). + +## Supported Contact Group `email` Attributes + +Either an `address` or `user` attribute is required. + +* `address` - (Optional) A well formed email address. + +* `user` - (Optional) An email will be sent to the email address of record for + the corresponding user ID (e.g. `/user/1234`). + +A `user`'s email address is automatically maintained and kept up to date by the +recipient, whereas an `address` provides no automatic layer of indirection for +keeping the information accurate (including LDAP and SAML-based authentication +mechanisms). + +## Supported Contact Group `http` Attributes + +* `address` - (Required) URL to send a webhook request to. + +* `format` - (Optional) The payload of the request is a JSON-encoded payload + when the `format` is set to `json` (the default). The alternate payload + encoding is `params`. + +* `method` - (Optional) The HTTP verb to use when making a request. Either + `GET` or `POST` may be specified. The default verb is `POST`. + +## Supported Contact Group `irc` Attributes + +* `user` - (Required) When a user has configured IRC on their user account, they + will receive an IRC notification. + +## Supported Contact Group `pager_duty` Attributes + +* `contact_group_fallback` - (Optional) If there is a problem contacting Pager + Duty, relay the notification automatically to the specified Contact Group + (e.g. `/contact_group/1234`). + +* `service_key` - (Required) The Pager Duty Service Key. + +* `webook_url` - (Required) The Pager Duty webhook URL that Pager Duty uses to + notify Circonus of acknowledged actions. + +## Supported Contact Group `slack` Attributes + +* `contact_group_fallback` - (Optional) If there is a problem contacting Slack, + relay the notification automatically to the specified Contact Group + (e.g. `/contact_group/1234`). + +* `buttons` - (Optional) Slack notifications can have acknowledgement buttons + built into the notification message itself when enabled. Defaults to `true`. + +* `channel` - (Required) Specify what Slack channel Circonus should send alerts + to. + +* `team` - (Required) Specify what Slack team Circonus should look in for the + aforementioned `channel`. + +* `username` - (Optional) Specify the username Circonus should advertise itself + as in Slack. Defaults to `Circonus`. + +## Supported Contact Group `sms` Attributes + +Either an `address` or `user` attribute is required. + +* `address` - (Optional) SMS Phone Number to send a short notification to. + +* `user` - (Optional) An SMS page will be sent to the phone number of record for + the corresponding user ID (e.g. `/user/1234`). + +A `user`'s phone number is automatically maintained and kept up to date by the +recipient, whereas an `address` provides no automatic layer of indirection for +keeping the information accurate (including LDAP and SAML-based authentication +mechanisms). + +## Supported Contact Group `victorops` Attributes + +* `contact_group_fallback` - (Optional) If there is a problem contacting + VictorOps, relay the notification automatically to the specified Contact Group + (e.g. `/contact_group/1234`). + +* `api_key` - (Required) The API Key for talking with VictorOps. + +* `critical` - (Required) +* `info` - (Required) +* `team` - (Required) +* `warning` - (Required) + +## Supported Contact Group `xmpp` Attributes + +Either an `address` or `user` attribute is required. + +* `address` - (Optional) XMPP address to send a short notification to. + +* `user` - (Optional) An XMPP notification will be sent to the XMPP address of + record for the corresponding user ID (e.g. `/user/1234`). + +## Import Example + +`circonus_contact_group` supports importing resources. Supposing the following +Terraform: + +``` +provider "circonus" { + alias = "b8fec159-f9e5-4fe6-ad2c-dc1ec6751586" +} + +resource "circonus_contact_group" "myteam" { + name = "My Team's Contact Group" + + email { + address = "myteam@example.com" + } + + slack { + channel = "#myteam" + team = "T024UT03C" + } +} +``` + +It is possible to import a `circonus_contact_group` resource with the following command: + +``` +$ terraform import circonus_contact_group.myteam ID +``` + +Where `ID` is the `_cid` or Circonus ID of the Contact Group +(e.g. `/contact_group/12345`) and `circonus_contact_group.myteam` is the name of +the resource whose state will be populated as a result of the command. diff --git a/website/source/docs/providers/circonus/r/graph.html.markdown b/website/source/docs/providers/circonus/r/graph.html.markdown new file mode 100644 index 000000000..65169b335 --- /dev/null +++ b/website/source/docs/providers/circonus/r/graph.html.markdown @@ -0,0 +1,179 @@ +--- +layout: "circonus" +page_title: "Circonus: circonus_graph" +sidebar_current: "docs-circonus-resource-circonus_graph" +description: |- + Manages a Circonus graph. +--- + +# circonus\_graph + +The ``circonus_graph`` resource creates and manages a +[Circonus Graph](https://login.circonus.com/user/docs/Visualization/Graph/Create). + +https://login.circonus.com/resources/api/calls/graph). + +## Usage + +``` +variable "myapp-tags" { + type = "list" + default = [ "app:myapp", "owner:myteam" ] +} + +resource "circonus_graph" "latency-graph" { + name = "Latency Graph" + description = "A sample graph showing off two data points" + notes = "Misc notes about this graph" + graph_style = "line" + line_style = "stepped" + + metric { + check = "${circonus_check.api_latency.checks[0]}" + metric_name = "maximum" + metric_type = "numeric" + name = "Maximum Latency" + axis = "left" + color = "#657aa6" + } + + metric { + check = "${circonus_check.api_latency.checks[0]}" + metric_name = "minimum" + metric_type = "numeric" + name = "Minimum Latency" + axis = "right" + color = "#0000ff" + } + + tags = [ "${var.myapp-tags}" ] +} +``` + +## Argument Reference + +* `description` - (Optional) Description of what the graph is for. + +* `graph_style` - (Optional) How the graph should be rendered. Valid options + are `area` or `line` (default). + +* `left` - (Optional) A map of graph left axis options. Valid values in `left` + include: `logarithmic` can be set to `0` (default) or `1`; `min` is the `min` + Y axis value on the left; and `max` is the Y axis max value on the left. + +* `line_style` - (Optional) How the line should change between points. Can be + either `stepped` (default) or `interpolated`. + +* `name` - (Required) The title of the graph. + +* `notes` - (Optional) A place for storing notes about this graph. + +* `right` - (Optional) A map of graph right axis options. Valid values in + `right` include: `logarithmic` can be set to `0` (default) or `1`; `min` is + the `min` Y axis value on the right; and `max` is the Y axis max value on the + right. + +* `metric` - (Optional) A list of metric streams to graph. See below for + options. + +* `metric_cluster` - (Optional) A metric cluster to graph. See below for options. + +* `tags` - (Optional) A list of tags assigned to this graph. + +## `metric` Configuration + +An individual metric stream is the underlying source of data points used for +visualization in a graph. Either a `caql` attribute is required or a `check` and +`metric` must be set. The `metric` attribute can have the following options +set. + +* `active` - (Optional) A boolean if the metric stream is enabled or not. + +* `alpha` - (Optional) A floating point number between 0 and 1. + +* `axis` - (Optional) The axis that the metric stream will use. Valid options + are `left` (default) or `right`. + +* `caql` - (Optional) A CAQL formula. Conflicts with the `check` and `metric` + attributes. + +* `check` - (Optional) The check that this metric stream belongs to. + +* `color` - (Optional) A hex-encoded color of the line / area on the graph. + +* `formula` - (Optional) Formula that should be aplied to both the values in the + graph and the legend. + +* `legend_formula` - (Optional) Formula that should be applied to values in the + legend. + +* `function` - (Optional) What derivative value, if any, should be used. Valid + values are: `gauge` (default), `derive`, and `counter (_stddev)` + +* `metric_type` - (Required) The type of the metric. Valid values are: + `numeric`, `text`, `histogram`, `composite`, or `caql`. + +* `name` - (Optional) A name which will appear in the graph legend. + +* `metric_name` - (Optional) The name of the metric stream within the check to + graph. + +* `stack` - (Optional) If this metric is to be stacked, which stack set does it + belong to (starting at `0`). + +## `metric_cluster` Configuration + +A metric cluster selects multiple metric streams together dynamically using a +query language and returns the set of matching metric streams as a single result +set to the graph rendering engine. + +* `active` - (Optional) A boolean if the metric cluster is enabled or not. + +* `aggregate` - (Optional) The aggregate function to apply across this metric + cluster to create a single value. Valid values are: `none` (default), `min`, + `max`, `sum`, `mean`, or `geometric_mean`. + +* `axis` - (Optional) The axis that the metric cluster will use. Valid options + are `left` (default) or `right`. + +* `color` - (Optional) A hex-encoded color of the line / area on the graph. + This is a required attribute when `aggregate` is specified. + +* `group` - (Optional) The `metric_cluster` that will provide datapoints for this + graph. + +* `name` - (Optional) A name which will appear in the graph legend for this + metric cluster. + +## Import Example + +`circonus_graph` supports importing resources. Supposing the following +Terraform (and that the referenced [`circonus_metric`](metric.html) +and [`circonus_check`](check.html) have already been imported): + +``` +resource "circonus_graph" "icmp-graph" { + name = "Test graph" + graph_style = "line" + line_style = "stepped" + + metric { + check = "${circonus_check.api_latency.checks[0]}" + metric_name = "maximum" + metric_type = "numeric" + name = "Maximum Latency" + axis = "left" + } +} +``` + +It is possible to import a `circonus_graph` resource with the following command: + +``` +$ terraform import circonus_graph.usage ID +``` + +Where `ID` is the `_cid` or Circonus ID of the graph +(e.g. `/graph/bd72aabc-90b9-4039-cc30-c9ab838c18f5`) and +`circonus_graph.icmp-graph` is the name of the resource whose state will be +populated as a result of the command. diff --git a/website/source/docs/providers/circonus/r/metric.html.markdown b/website/source/docs/providers/circonus/r/metric.html.markdown new file mode 100644 index 000000000..0070ea987 --- /dev/null +++ b/website/source/docs/providers/circonus/r/metric.html.markdown @@ -0,0 +1,73 @@ +--- +layout: "circonus" +page_title: "Circonus: circonus_metric" +sidebar_current: "docs-circonus-resource-circonus_metric" +description: |- + Manages a Circonus metric. +--- + +# circonus\_metric + +The ``circonus_metric`` resource creates and manages a +single [metric resource](https://login.circonus.com/resources/api/calls/metric) +that will be instantiated only once a referencing `circonus_check` has been +created. + +## Usage + +``` +resource "circonus_metric" "used" { + name = "_usage`0`_used" + type = "numeric" + units = "qty" + + tags = { + author = "terraform" + source = "circonus" + } +} +``` + +## Argument Reference + +* `active` - (Optional) A boolean indicating if the metric is being filtered out + at the `circonus_check`'s collector(s) or not. + +* `name` - (Required) The name of the metric. A `name` must be unique within a + `circonus_check` and its meaning is `circonus_check.type` specific. + +* `tags` - (Optional) A list of tags assigned to the metric. + +* `type` - (Required) The type of metric. This value must be present and can be + one of the following values: `numeric`, `text`, `histogram`, `composite`, or + `caql`. + +* `unit` - (Optional) The unit of measurement for this `circonus_metric`. + +## Import Example + +`circonus_metric` supports importing resources. Supposing the following +Terraform: + +``` +provider "circonus" { + alias = "b8fec159-f9e5-4fe6-ad2c-dc1ec6751586" +} + +resource "circonus_metric" "usage" { + name = "_usage`0`_used" + type = "numeric" + unit = "qty" + tags = { source = "circonus" } +} +``` + +It is possible to import a `circonus_metric` resource with the following command: + +``` +$ terraform import circonus_metric.usage ID +``` + +Where `ID` is a random, never before used UUID and `circonus_metric.usage` is +the name of the resource whose state will be populated as a result of the +command. diff --git a/website/source/docs/providers/circonus/r/metric_cluster.html.markdown b/website/source/docs/providers/circonus/r/metric_cluster.html.markdown new file mode 100644 index 000000000..87dbd09d4 --- /dev/null +++ b/website/source/docs/providers/circonus/r/metric_cluster.html.markdown @@ -0,0 +1,86 @@ +--- +layout: "circonus" +page_title: "Circonus: circonus_metric_cluster" +sidebar_current: "docs-circonus-resource-circonus_metric_cluster" +description: |- + Manages a Circonus Metric Cluster. +--- + +# circonus\_metric\_cluster + +The ``circonus_metric_cluster`` resource creates and manages a +[Circonus Metric Cluster](https://login.circonus.com/user/docs/Data/View/MetricClusters). + +## Usage + +``` +resource "circonus_metric_cluster" "nomad-job-memory-rss" { + name = "My Job's Resident Memory" + description = <<-EOF +An aggregation of all resident memory metric streams across allocations in a Nomad job. +EOF + + query { + definition = "*`nomad-jobname`memory`rss" + type = "average" + } + tags = ["source:nomad","resource:memory"] +} +``` + +## Argument Reference + +* `description` - (Optional) A long-form description of the metric cluster. + +* `name` - (Required) The name of the metric cluster. This name must be unique + across all metric clusters in a given Circonus Account. + +* `query` - (Required) One or more `query` attributes must be present. Each + `query` must contain both a `definition` and a `type`. See below for details + on supported attributes. + +* `tags` - (Optional) A list of tags attached to the metric cluster. + +## Supported Metric Cluster `query` Attributes + +* `definition` - (Required) The definition of a metric cluster [query](https://login.circonus.com/resources/api/calls/metric_cluster). + +* `type` - (Required) The query type to execute per metric cluster. Valid query + types are: `average`, `count`, `counter`, `counter2`, `counter2_stddev`, + `counter_stddev`, `derive`, `derive2`, `derive2_stddev`, `derive_stddev`, + `histogram`, `stddev`, `text`. + +## Out parameters + +* `id` - ID of the Metric Cluster. + +## Import Example + +`circonus_metric_cluster` supports importing resources. Supposing the following +Terraform: + +``` +provider "circonus" { + alias = "b8fec159-f9e5-4fe6-ad2c-dc1ec6751586" +} + +resource "circonus_metric_cluster" "mymetriccluster" { + name = "Metric Cluster for a particular metric in a job" + + query { + definition = "*`nomad-jobname`memory`rss" + type = "average" + } +} +``` + +It is possible to import a `circonus_metric_cluster` resource with the following +command: + +``` +$ terraform import circonus_metric_cluster.mymetriccluster ID +``` + +Where `ID` is the `_cid` or Circonus ID of the Metric Cluster +(e.g. `/metric_cluster/12345`) and `circonus_metric_cluster.mymetriccluster` is the +name of the resource whose state will be populated as a result of the command. diff --git a/website/source/docs/providers/circonus/r/rule_set.html.markdown b/website/source/docs/providers/circonus/r/rule_set.html.markdown new file mode 100644 index 000000000..4c1c481b3 --- /dev/null +++ b/website/source/docs/providers/circonus/r/rule_set.html.markdown @@ -0,0 +1,377 @@ +--- +layout: "circonus" +page_title: "Circonus: circonus_rule_set" +sidebar_current: "docs-circonus-resource-circonus_rule_set" +description: |- + Manages a Circonus rule set. +--- + +# circonus\_rule_set + +The ``circonus_rule_set`` resource creates and manages a +[Circonus Rule Set](https://login.circonus.com/resources/api/calls/rule_set). + +## Usage + +``` +variable "myapp-tags" { + type = "list" + default = [ "app:myapp", "owner:myteam" ] +} + +resource "circonus_rule_set" "myapp-cert-ttl-alert" { + check = "${circonus_check.myapp-https.checks[0]}" + metric_name = "cert_end_in" + link = "https://wiki.example.org/playbook/how-to-renew-cert" + + if { + value { + min_value = "${2 * 24 * 3600}" + } + + then { + notify = [ "${circonus_contact_group.myapp-owners.id}" ] + severity = 1 + } + } + + if { + value { + min_value = "${7 * 24 * 3600}" + } + + then { + notify = [ "${circonus_contact_group.myapp-owners.id}" ] + severity = 2 + } + } + + if { + value { + min_value = "${21 * 24 * 3600}" + } + + then { + notify = [ "${circonus_contact_group.myapp-owners.id}" ] + severity = 3 + } + } + + if { + value { + absent = "24h" + } + + then { + notify = [ "${circonus_contact_group.myapp-owners.id}" ] + severity = 1 + } + } + + tags = [ "${var.myapp-tags}" ] +} + +resource "circonus_rule_set" "myapp-healthy-alert" { + check = "${circonus_check.myapp-https.checks[0]}" + metric_name = "duration" + link = "https://wiki.example.org/playbook/debug-down-app" + + if { + value { + # SEV1 if it takes more than 9.5s for us to complete an HTTP request + max_value = "${9.5 * 1000}" + } + + then { + notify = [ "${circonus_contact_group.myapp-owners.id}" ] + severity = 1 + } + } + + if { + value { + # SEV2 if it takes more than 5s for us to complete an HTTP request + max_value = "${5 * 1000}" + } + + then { + notify = [ "${circonus_contact_group.myapp-owners.id}" ] + severity = 2 + } + } + + if { + value { + # SEV3 if the average response time is more than 500ms using a moving + # average over the last 10min. Any transient problems should have + # resolved themselves by now. Something's wrong, need to page someone. + over { + last = "10m" + using = "average" + } + max_value = "500" + } + + then { + notify = [ "${circonus_contact_group.myapp-owners.id}" ] + severity = 3 + } + } + + if { + value { + # SEV4 if it takes more than 500ms for us to complete an HTTP request. We + # want to record that things were slow, but not wake anyone up if it + # momentarily pops above 500ms. + min_value = "500" + } + + then { + notify = [ "${circonus_contact_group.myapp-owners.id}" ] + severity = 3 + } + } + + if { + value { + # If for whatever reason we're not recording any values for the last + # 24hrs, fire off a SEV1. + absent = "24h" + } + + then { + notify = [ "${circonus_contact_group.myapp-owners.id}" ] + severity = 1 + } + } + + tags = [ "${var.myapp-tags}" ] +} + +resource "circonus_contact_group" "myapp-owners" { + name = "My App Owners" + tags = [ "${var.myapp-tags}" ] +} + +resource "circonus_check" "myapp-https" { + name = "My App's HTTPS Check" + + notes = <<-EOF +A check to create metric streams for Time to First Byte, HTTP transaction +duration, and the TTL of a TLS cert. +EOF + + collector { + id = "/broker/1" + } + + http { + code = "^200$" + headers = { + X-Request-Type = "health-check", + } + url = "https://www.example.com/myapp/healthz" + } + + metric { + name = "${circonus_metric.myapp-cert-ttl.name}" + tags = "${circonus_metric.myapp-cert-ttl.tags}" + type = "${circonus_metric.myapp-cert-ttl.type}" + unit = "${circonus_metric.myapp-cert-ttl.unit}" + } + + metric { + name = "${circonus_metric.myapp-duration.name}" + tags = "${circonus_metric.myapp-duration.tags}" + type = "${circonus_metric.myapp-duration.type}" + unit = "${circonus_metric.myapp-duration.unit}" + } + + period = 60 + tags = ["source:circonus", "author:terraform"] + timeout = 10 +} + +resource "circonus_metric" "myapp-cert-ttl" { + name = "cert_end_in" + type = "numeric" + unit = "seconds" + tags = [ "${var.myapp-tags}", "resource:tls" ] +} + +resource "circonus_metric" "myapp-duration" { + name = "duration" + type = "numeric" + unit = "miliseconds" + tags = [ "${var.myapp-tags}" ] +} +``` + +## Argument Reference + +* `check` - (Required) The Circonus ID that this Rule Set will use to search for + a metric stream to alert on. + +* `if` - (Required) One or more ordered predicate clauses that describe when + Circonus should generate a notification. See below for details on the + structure of an `if` configuration clause. + +* `link` - (Optional) A link to external documentation (or anything else you + feel is important) when a notification is sent. This value will show up in + email alerts and the Circonus UI. + +* `metric_type` - (Optional) The type of metric this rule set will operate on. + Valid values are `numeric` (the default) and `text`. + +* `notes` - (Optional) Notes about this rule set. + +* `parent` - (Optional) A Circonus Metric ID that, if specified and active with + a severity 1 alert, will silence this rule set until all of the severity 1 + alerts on the parent clear. This value must match the format + `${check_id}_${metric_name}`. + +* `metric_name` - (Required) The name of the metric stream within a given check + that this rule set is active on. + +* `tags` - (Optional) A list of tags assigned to this rule set. + +## `if` Configuration + +The `if` configuration block is an +[ordered list of rules](https://login.circonus.com/user/docs/Alerting/Rules/Configure) that +are evaluated in order, first to last. The first `if` condition to evaluate +true shortcircuits all other `if` blocks in this rule set. An `if` block is also +referred to as a "rule." It is advised that all high-severity rules are ordered +before low-severity rules otherwise low-severity rules will mask notifications +that should be delivered with a high-severity. + +`if` blocks are made up of two configuration blocks: `value` and `then`. The +`value` configuration block specifies the criteria underwhich the metric streams +are evaluated. The `then` configuration block, optional, specifies what action +to take. + +### `value` Configuration + +A `value` block can have only one of several "predicate" attributes specified +because they conflict with each other. The list of mutually exclusive +predicates is dependent on the `metric_type`. To evaluate multiple predicates, +create multiple `if` configuration blocks in the proper order. + +#### `numeric` Predicates + +Metric types of type `numeric` support the following predicates. Only one of +the following predicates may be specified at a time. + +* `absent` - (Optional) If a metric has not been observed in this duration the + rule will fire. When present, this duration is evaluated in terms of seconds. + +* `changed` - (Optional) A boolean indicating this rule should fire when the + value changes (e.g. `n != n1`). + +* `min_value` - (Optional) When the value is less than this value, this rule will + fire (e.g. `n < ${min_value}`). + +* `max_value` - (Optional) When the value is greater than this value, this rule + will fire (e.g. `n > ${max_value}`). + +Additionally, a `numeric` check can also evaluate data based on a windowing +function versus the last measured value in the metric stream. In order to have +a rule evaluate on derived value from a window, include a nested `over` +attribute inside of the `value` configuration block. An `over` attribute needs +two attributes: + +* `last` - (Optional) A duration for the sliding window. Default `300s`. + +* `using` - (Optional) The window function to use over the `last` interval. + Valid window functions include: `average` (the default), `stddev`, `derive`, + `derive_stddev`, `counter`, `counter_stddev`, `derive_2`, `derive_2_stddev`, + `counter_2`, and `counter_2_stddev`. + +#### `text` Predicates + +Metric types of type `text` support the following predicates: + +* `absent` - (Optional) If a metric has not been observed in this duration the + rule will fire. When present, this duration is evaluated in terms of seconds. + +* `changed` - (Optional) A boolean indicating this rule should fire when the + last value in the metric stream changed from it's previous value (e.g. `n != + n-1`). + +* `contains` - (Optional) When the last value in the metric stream the value is + less than this value, this rule will fire (e.g. `strstr(n, ${contains}) != + NULL`). + +* `match` - (Optional) When the last value in the metric stream value exactly + matches this configured value, this rule will fire (e.g. `strcmp(n, ${match}) + == 0`). + +* `not_contain` - (Optional) When the last value in the metric stream does not + match this configured value, this rule will fire (e.g. `strstr(n, ${contains}) + == NULL`). + +* `not_match` - (Optional) When the last value in the metric stream does not match + this configured value, this rule will fire (e.g. `strstr(n, ${not_match}) == + NULL`). + +### `then` Configuration + +A `then` block can have the following attributes: + +* `after` - (Optional) Only execute this notification after waiting for this + number of minutes. Defaults to immediately, or `0m`. +* `notify` - (Optional) A list of contact group IDs to notify when this rule is + sends off a notification. +* `severity` - (Optional) The severity level of the notification. This can be + set to any value between `1` and `5`. Defaults to `1`. + +## Import Example + +`circonus_rule_set` supports importing resources. Supposing the following +Terraform (and that the referenced [`circonus_metric`](metric.html) +and [`circonus_check`](check.html) have already been imported): + +``` +resource "circonus_rule_set" "icmp-latency-alert" { + check = "${circonus_check.api_latency.checks[0]}" + metric_name = "maximum" + + if { + value { + absent = "600s" + } + + then { + notify = [ "${circonus_contact_group.test-trigger.id}" ] + severity = 1 + } + } + + if { + value { + over { + last = "120s" + using = "average" + } + + max_value = 0.5 # units are in miliseconds + } + + then { + notify = [ "${circonus_contact_group.test-trigger.id}" ] + severity = 2 + } + } +} +``` + +It is possible to import a `circonus_rule_set` resource with the following command: + +``` +$ terraform import circonus_rule_set.usage ID +``` + +Where `ID` is the `_cid` or Circonus ID of the Rule Set +(e.g. `/rule_set/201285_maximum`) and `circonus_rule_set.icmp-latency-alert` is +the name of the resource whose state will be populated as a result of the +command. diff --git a/website/source/layouts/circonus.erb b/website/source/layouts/circonus.erb new file mode 100644 index 000000000..58920a9b0 --- /dev/null +++ b/website/source/layouts/circonus.erb @@ -0,0 +1,60 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + + <%= yield %> + <% end %> diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 5f334a9c4..5b7b3242a 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -218,6 +218,10 @@ Chef + > + Circonus + + > CloudFlare