From 17fb98afa253ced7df709cd2f3b041f6eccd043d Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Fri, 10 Mar 2017 12:19:17 -0800 Subject: [PATCH] Circonus Provider (#12338) * Begin stubbing out the Circonus provider. * Remove all references to `reverse:secret_key`. This value is dynamically set by the service and unused by Terraform. * Update the `circonus_check` resource. Still a WIP. * Add docs for the `circonus_check` resource. Commit miss, this should have been included in the last commit. * "Fix" serializing check tags I still need to figure out how I can make them order agnostic w/o using a TypeSet. I'm worried that's what I'm going to have to do. * Spike a quick circonus_broker data source. * Convert tags to a Set so the order does not matter. * Add a `circonus_account` data source. * Correctly spell account. Pointed out by: @postwait * Add the `circonus_contact_group` resource. * Push descriptions into their own file in order to reduce the busyness of the schema when reviewing code. * Rename `circonus_broker` and `broker` to `circonus_collector` and `collector`, respectively. Change made with concent by Circonus to reduce confusion (@postwait, @maier, and several others). * Use upstream contsants where available. * Import the latest circonus-gometrics. * Move to using a Set of collectors vs a list attached to a single attribute. * Rename "cid" to "id" in the circonus_account data source and elsewhere where possible. * Inject a tag automatically. Update gometrics. * Checkpoint `circonus_metric` resource. * Enable provider-level auto-tagging. This is disabled by default. * Rearrange metric. This is an experimental "style" of a provider. We'll see. That moment. When you think you've gone off the rails on a mad scientist experiment but like the outcome and think you may be onto something but haven't proven it to yourself or anyone else yet? That. That exact feeling of semi-confidence while being alone in the wilderness. Please let this not be the Terraform provider equivalent of DJB's C style of coding. We'll know in another resource or two if this was a horrible mistake or not. * Begin moving `resource_circonus_check` over to the new world order/structure: Much of this is WIP and incomplete, but here is the new supported structure: ``` variable "used_metric_name" { default = "_usage`0`_used" } resource "circonus_check" "usage" { # collectors = ["${var.collectors}"] collector { id = "${var.collectors[0]}" } name = "${var.check_name}" notes = "${var.notes}" json { url = "https://${var.target}/account/current" http_headers = { "Accept" = "application/json" "X-Circonus-App-Name" = "TerraformCheck" "X-Circonus-Auth-Token" = "${var.api_token}" } } stream { name = "${circonus_metric.used.name}" tags = "${circonus_metric.used.tags}" type = "${circonus_metric.used.type}" } tags = { source = "circonus" } } resource "circonus_metric" "used" { name = "${var.used_metric_name}" tags = { source = "circonus" } type = "numeric" } ``` * Document the `circonus_metric` resource. * Updated `circonus_check` docs. * If a port was present, automatically set it in the Config. * Alpha sort the check parameters now that they've been renamed. * Fix a handful of panics as a result of the schema changing. * Move back to a `TypeSet` for tags. After a stint with `TypeMap`, move back to `TypeSet`. A set of strings seems to match the API the best. The `map` type was convenient because it reduced the amount of boilerplate, but you loose out on other things. For instance, tags come in the form of `category:value`, so naturally it seems like you could use a map, but you can't without severe loss of functionality because assigning two values to the same category is common. And you can't normalize map input or suppress the output correctly (this was eventually what broke the camel's back). I tried an experiment of normalizing the input to be `category:value` as the key in the map and a value of `""`, but... seee diff suppress. In this case, simple is good. While here bring some cleanups to _Metric since that was my initial testing target. * Rename `providerConfig` to `_ProviderConfig` * Checkpoint the `json` check type. * Fix a few residual issues re: missing descriptions. * Rename `validateRegexp` to `_ValidateRegexp` * Use tags as real sets, not just a slice of strings. * Move the DiffSuppressFunc for tags down to the Elem. * Fix up unit tests to chase the updated, default hasher function being used. * Remove `Computed` attribute from `TypeSet` objects. This fixes a pile of issues re: update that I was having. * Rename functions. `GetStringOk` -> `GetStringOK` `GetSetAsListOk` -> `GetSetAsListOK` `GetIntOk` -> `GetIntOK` * Various small cleanups and comments rolled into a single commit. * Add a `postgresql` check type for the `circonus_check` resource. * Rename various validator functions to be _CapitalCase vs capitalCase. * Err... finish the validator renames. * Add `GetFloat64()` support. * Add `icmp_ping` check type support. * Catch up to the _API*Attr renames. Deliberately left out of the previous commit in order to create a clean example of what is required to add a new check type to the `circonus_check` resource. * Clarify when the `target` attribute is required for the `postgresql` check type. * Correctly pull the metric ID attribute from the right location. * Add a circonus_stream_group resource (a.k.a. a Circonus "metric cluster") * Add support for the [`caql`](https://login.circonus.com/user/docs/caql_reference) check type. * Add support for the `http` check type. * `s/SSL/TLS/g` * Add support for `tcp` check types. * Enumerate the available metrics that are supported for each check type. * Add [`cloudwatch`](https://login.circonus.com/user/docs/Data/CheckTypes/CloudWatch) check type support. * Add a `circonus_trigger` resource (a.k.a Circonus Ruleset). * Rename a handful of functions to make it clear in the function name the direction of flow for information moving through the provider. TL;DR: Replace `parse` and `read` with "foo to bar"-like names. * Fix the attribute name used in a validator. Absent != After. * Set the minimum `absent` predicate to 70s per testing. * Fix the regression tests for circonus_trigger now that absent has a 70s min * Fix up the `tcp` check to require a `host` attribute. Fix tests. It's clear I didn't run these before committing/pushing the `tcp` check last time. * Fix `circonus_check` for `cloudwatch` checks. * Rename `parsePerCheckTypeConfig()` to `_CheckConfigToAPI` to be consistent with other function names. grep(1)ability of code++ * Slack buttons as an integer are string encoded. * Fix updates for `circonus_contact`. * Fix the out parameters for contact groups. * Move to using `_CastSchemaToTF()` where appropriate. * Fix circonus_contact_group. Updates work as expected now. * Use `_StateSet()` in place of `d.Set()` everywhere. * Make a quick pass over the collector datasource to modernize its style * Quick pass for items identified by `golint`. * Fix up collectors * Fix the `json` check type. Reconcile possible sources of drift. Update now works as expected. * Normalize trigger durations to seconds. * Improve the robustness of the state handling for the `circonus_contact_group` resource. * I'm torn on this, but sort the contact groups in the notify list. This does mean that if the first contact group in the list has a higher lexical sort order the plan won't converge until the offending resource is tainted and recreated. But there's also some sorting happening elsewhere, so.... sort and taint for now and this will need to be revisited in the future. * Add support for the `httptrap` check type. * Remove empty units from the state file. * Metric clusters can return a 404. Detect this accordingly in its respective Exists handler. * Add a `circonus_graph` resource. * Fix a handful of bugs in the graph provider. * Re-enable the necessary `ConflictsWith` definitions and normalize attribute names. * Objects that have been deleted via the UI return a 404. Handle in Exists(). * Teach `circonus_graph`'s Stack set to accept nil values. * Set `ForceNew: true` for a graph's name. * Chase various API fixes required to make `circonus_graph` work as expected. * Fix up the handling of sub-1 zoom resolutions for graphs. * Add the `check_by_collector` out parameter to the `circonus_check` resource. * Improve validation of line vs area graphs. Fix graph_style. * Fix up the `logarithmic` graph axis option. * Resolve various trivial `go vet` issues. * Add a stream_group out parameter. * Remove incorrectly applied `Optional` attributes to the `circonus_account` resource. * Remove various `Optional` attributes from the `circonus_collector` data source. * Centralize the common need to suppress leading and trailing whitespace into `suppressWhitespace`. * Sync up with upstream vendor fixes for circonus_graph. * Update the checksum value for the http check. * Chase `circonus_graph`'s underlying `line_style` API object change from `string` to `*string`. * Clean up tests to use a generic terraform regression testing account. * Add support for the MySQL to the `circonus_check` resource. * Begin stubbing out the Circonus provider. * Remove all references to `reverse:secret_key`. This value is dynamically set by the service and unused by Terraform. * Update the `circonus_check` resource. Still a WIP. * Add docs for the `circonus_check` resource. Commit miss, this should have been included in the last commit. * "Fix" serializing check tags I still need to figure out how I can make them order agnostic w/o using a TypeSet. I'm worried that's what I'm going to have to do. * Spike a quick circonus_broker data source. * Convert tags to a Set so the order does not matter. * Add a `circonus_account` data source. * Correctly spell account. Pointed out by: @postwait * Add the `circonus_contact_group` resource. * Push descriptions into their own file in order to reduce the busyness of the schema when reviewing code. * Rename `circonus_broker` and `broker` to `circonus_collector` and `collector`, respectively. Change made with concent by Circonus to reduce confusion (@postwait, @maier, and several others). * Use upstream contsants where available. * Import the latest circonus-gometrics. * Move to using a Set of collectors vs a list attached to a single attribute. * Rename "cid" to "id" in the circonus_account data source and elsewhere where possible. * Inject a tag automatically. Update gometrics. * Checkpoint `circonus_metric` resource. * Enable provider-level auto-tagging. This is disabled by default. * Rearrange metric. This is an experimental "style" of a provider. We'll see. That moment. When you think you've gone off the rails on a mad scientist experiment but like the outcome and think you may be onto something but haven't proven it to yourself or anyone else yet? That. That exact feeling of semi-confidence while being alone in the wilderness. Please let this not be the Terraform provider equivalent of DJB's C style of coding. We'll know in another resource or two if this was a horrible mistake or not. * Begin moving `resource_circonus_check` over to the new world order/structure: Much of this is WIP and incomplete, but here is the new supported structure: ``` variable "used_metric_name" { default = "_usage`0`_used" } resource "circonus_check" "usage" { # collectors = ["${var.collectors}"] collector { id = "${var.collectors[0]}" } name = "${var.check_name}" notes = "${var.notes}" json { url = "https://${var.target}/account/current" http_headers = { "Accept" = "application/json" "X-Circonus-App-Name" = "TerraformCheck" "X-Circonus-Auth-Token" = "${var.api_token}" } } stream { name = "${circonus_metric.used.name}" tags = "${circonus_metric.used.tags}" type = "${circonus_metric.used.type}" } tags = { source = "circonus" } } resource "circonus_metric" "used" { name = "${var.used_metric_name}" tags = { source = "circonus" } type = "numeric" } ``` * Document the `circonus_metric` resource. * Updated `circonus_check` docs. * If a port was present, automatically set it in the Config. * Alpha sort the check parameters now that they've been renamed. * Fix a handful of panics as a result of the schema changing. * Move back to a `TypeSet` for tags. After a stint with `TypeMap`, move back to `TypeSet`. A set of strings seems to match the API the best. The `map` type was convenient because it reduced the amount of boilerplate, but you loose out on other things. For instance, tags come in the form of `category:value`, so naturally it seems like you could use a map, but you can't without severe loss of functionality because assigning two values to the same category is common. And you can't normalize map input or suppress the output correctly (this was eventually what broke the camel's back). I tried an experiment of normalizing the input to be `category:value` as the key in the map and a value of `""`, but... seee diff suppress. In this case, simple is good. While here bring some cleanups to _Metric since that was my initial testing target. * Rename `providerConfig` to `_ProviderConfig` * Checkpoint the `json` check type. * Fix a few residual issues re: missing descriptions. * Rename `validateRegexp` to `_ValidateRegexp` * Use tags as real sets, not just a slice of strings. * Move the DiffSuppressFunc for tags down to the Elem. * Fix up unit tests to chase the updated, default hasher function being used. * Remove `Computed` attribute from `TypeSet` objects. This fixes a pile of issues re: update that I was having. * Rename functions. `GetStringOk` -> `GetStringOK` `GetSetAsListOk` -> `GetSetAsListOK` `GetIntOk` -> `GetIntOK` * Various small cleanups and comments rolled into a single commit. * Add a `postgresql` check type for the `circonus_check` resource. * Rename various validator functions to be _CapitalCase vs capitalCase. * Err... finish the validator renames. * Add `GetFloat64()` support. * Add `icmp_ping` check type support. * Catch up to the _API*Attr renames. Deliberately left out of the previous commit in order to create a clean example of what is required to add a new check type to the `circonus_check` resource. * Clarify when the `target` attribute is required for the `postgresql` check type. * Correctly pull the metric ID attribute from the right location. * Add a circonus_stream_group resource (a.k.a. a Circonus "metric cluster") * Add support for the [`caql`](https://login.circonus.com/user/docs/caql_reference) check type. * Add support for the `http` check type. * `s/SSL/TLS/g` * Add support for `tcp` check types. * Enumerate the available metrics that are supported for each check type. * Add [`cloudwatch`](https://login.circonus.com/user/docs/Data/CheckTypes/CloudWatch) check type support. * Add a `circonus_trigger` resource (a.k.a Circonus Ruleset). * Rename a handful of functions to make it clear in the function name the direction of flow for information moving through the provider. TL;DR: Replace `parse` and `read` with "foo to bar"-like names. * Fix the attribute name used in a validator. Absent != After. * Set the minimum `absent` predicate to 70s per testing. * Fix the regression tests for circonus_trigger now that absent has a 70s min * Fix up the `tcp` check to require a `host` attribute. Fix tests. It's clear I didn't run these before committing/pushing the `tcp` check last time. * Fix `circonus_check` for `cloudwatch` checks. * Rename `parsePerCheckTypeConfig()` to `_CheckConfigToAPI` to be consistent with other function names. grep(1)ability of code++ * Slack buttons as an integer are string encoded. * Fix updates for `circonus_contact`. * Fix the out parameters for contact groups. * Move to using `_CastSchemaToTF()` where appropriate. * Fix circonus_contact_group. Updates work as expected now. * Use `_StateSet()` in place of `d.Set()` everywhere. * Make a quick pass over the collector datasource to modernize its style * Quick pass for items identified by `golint`. * Fix up collectors * Fix the `json` check type. Reconcile possible sources of drift. Update now works as expected. * Normalize trigger durations to seconds. * Improve the robustness of the state handling for the `circonus_contact_group` resource. * I'm torn on this, but sort the contact groups in the notify list. This does mean that if the first contact group in the list has a higher lexical sort order the plan won't converge until the offending resource is tainted and recreated. But there's also some sorting happening elsewhere, so.... sort and taint for now and this will need to be revisited in the future. * Add support for the `httptrap` check type. * Remove empty units from the state file. * Metric clusters can return a 404. Detect this accordingly in its respective Exists handler. * Add a `circonus_graph` resource. * Fix a handful of bugs in the graph provider. * Re-enable the necessary `ConflictsWith` definitions and normalize attribute names. * Objects that have been deleted via the UI return a 404. Handle in Exists(). * Teach `circonus_graph`'s Stack set to accept nil values. * Set `ForceNew: true` for a graph's name. * Chase various API fixes required to make `circonus_graph` work as expected. * Fix up the handling of sub-1 zoom resolutions for graphs. * Add the `check_by_collector` out parameter to the `circonus_check` resource. * Improve validation of line vs area graphs. Fix graph_style. * Fix up the `logarithmic` graph axis option. * Resolve various trivial `go vet` issues. * Add a stream_group out parameter. * Remove incorrectly applied `Optional` attributes to the `circonus_account` resource. * Remove various `Optional` attributes from the `circonus_collector` data source. * Centralize the common need to suppress leading and trailing whitespace into `suppressWhitespace`. * Sync up with upstream vendor fixes for circonus_graph. * Update the checksum value for the http check. * Chase `circonus_graph`'s underlying `line_style` API object change from `string` to `*string`. * Clean up tests to use a generic terraform regression testing account. * Rename all identifiers that began with a `_` and replace with a corresponding lowercase glyph. * Remove stale comment in types. * Move the calls to `ResourceData`'s `SetId()` calls to be first in the list so that no resources are lost in the event of a `panic()`. * Remove `stateSet` from the `circonus_trigger` resource. * Remove `stateSet` from the `circonus_stream_group` resource. * Remove `schemaSet` from the `circonus_graph` resource. * Remove `stateSet` from the `circonus_contact` resource. * Remove `stateSet` from the `circonus_metric` resource. * Remove `stateSet` from the `circonus_account` data source. * Remove `stateSet` from the `circonus_collector` data source. * Remove stray `stateSet` call from the `circonus_contact` resource. This is an odd artifact to find... I'm completely unsure as to why it was there to begin with but am mostly certain it's a bug and needs to be removed. * Remove `stateSet` from the `circonus_check` resource. * Remove the `stateSet` helper function. All call sites have been converted to return errors vs `panic()`'ing at runtime. * Remove a pile of unused functions and type definitions. * Remove the last of the `attrReader` interface. * Remove an unused `Sprintf` call. * Update `circonus-gometrics` and remove unused files. * Document what `convertToHelperSchema()` does. Rename `castSchemaToTF` to `convertToHelperSchema`. Change the function parameter ordering so the `map` of attribute descriptions: this is much easier to maintain when the description map is first when creating schema inline. * Move descriptions into their respective source files. * Remove all instances of `panic()`. In the case of software bugs, log an error. Never `panic()` and always return a value. * Rename `stream_group` to `metric_cluster`. * Rename triggers to rule sets * Rename `stream` to `metric`. * Chase the `stream` -> `metric` change into the docs. * Remove some unused test functions. * Add the now required `color` attribute for graphing a `metric_cluster`. * Add a missing description to silence a warning. * Add `id` as a selector for the account data source. * Futureproof testing: Randomize all asset names to prevent any possible resource conflicts. This isn't a necessary change for our current build and regression testing, but *just in case* we have a radical change to our testing framework in the future, make all resource names fully random. * Rename various values to match the Circonus docs. * s/alarm/alert/g * Ensure ruleset criteria can not be empty. --- builtin/providers/circonus/GNUmakefile | 3 + builtin/providers/circonus/check.go | 122 ++ builtin/providers/circonus/consts.go | 127 ++ .../circonus/data_source_circonus_account.go | 271 ++++ .../data_source_circonus_account_test.go | 66 + .../data_source_circonus_collector.go | 214 +++ .../data_source_circonus_collector_test.go | 47 + builtin/providers/circonus/interface.go | 83 ++ builtin/providers/circonus/metric.go | 182 +++ builtin/providers/circonus/metric_cluster.go | 57 + builtin/providers/circonus/metric_test.go | 19 + builtin/providers/circonus/provider.go | 135 ++ builtin/providers/circonus/provider_test.go | 35 + .../circonus/resource_circonus_check.go | 607 ++++++++ .../circonus/resource_circonus_check_caql.go | 89 ++ .../resource_circonus_check_caql_test.go | 74 + .../resource_circonus_check_cloudwatch.go | 260 ++++ ...resource_circonus_check_cloudwatch_test.go | 390 +++++ .../circonus/resource_circonus_check_http.go | 383 +++++ .../resource_circonus_check_http_test.go | 184 +++ .../resource_circonus_check_httptrap.go | 156 ++ .../resource_circonus_check_httptrap_test.go | 120 ++ .../resource_circonus_check_icmp_ping.go | 157 ++ .../resource_circonus_check_icmp_ping_test.go | 137 ++ .../circonus/resource_circonus_check_json.go | 366 +++++ .../resource_circonus_check_json_test.go | 230 +++ .../circonus/resource_circonus_check_mysql.go | 102 ++ .../resource_circonus_check_mysql_test.go | 80 ++ .../resource_circonus_check_postgresql.go | 166 +++ ...resource_circonus_check_postgresql_test.go | 151 ++ .../circonus/resource_circonus_check_tcp.go | 254 ++++ .../resource_circonus_check_tcp_test.go | 224 +++ .../circonus/resource_circonus_check_test.go | 43 + .../circonus/resource_circonus_contact.go | 1278 +++++++++++++++++ .../resource_circonus_contact_test.go | 241 ++++ .../circonus/resource_circonus_graph.go | 930 ++++++++++++ .../circonus/resource_circonus_graph_test.go | 199 +++ .../circonus/resource_circonus_metric.go | 138 ++ .../resource_circonus_metric_cluster.go | 262 ++++ .../resource_circonus_metric_cluster_test.go | 95 ++ .../circonus/resource_circonus_metric_test.go | 252 ++++ .../circonus/resource_circonus_rule_set.go | 866 +++++++++++ .../resource_circonus_rule_set_test.go | 226 +++ builtin/providers/circonus/tags.go | 65 + builtin/providers/circonus/types.go | 21 + builtin/providers/circonus/utils.go | 149 ++ builtin/providers/circonus/validators.go | 370 +++++ command/internal_plugin_list.go | 2 + .../circonus-labs/circonus-gometrics/LICENSE | 28 + .../circonus-gometrics/api/README.md | 163 +++ .../circonus-gometrics/api/account.go | 181 +++ .../circonus-gometrics/api/acknowledgement.go | 190 +++ .../circonus-gometrics/api/alert.go | 131 ++ .../circonus-gometrics/api/annotation.go | 223 +++ .../circonus-gometrics/api/api.go | 371 +++++ .../circonus-gometrics/api/broker.go | 131 ++ .../circonus-gometrics/api/check.go | 119 ++ .../circonus-gometrics/api/check_bundle.go | 255 ++++ .../api/check_bundle_metrics.go | 95 ++ .../circonus-gometrics/api/config/consts.go | 538 +++++++ .../circonus-gometrics/api/contact_group.go | 263 ++++ .../circonus-gometrics/api/dashboard.go | 399 +++++ .../circonus-gometrics/api/doc.go | 63 + .../circonus-gometrics/api/graph.go | 349 +++++ .../circonus-gometrics/api/maintenance.go | 220 +++ .../circonus-gometrics/api/metric.go | 162 +++ .../circonus-gometrics/api/metric_cluster.go | 261 ++++ .../circonus-gometrics/api/outlier_report.go | 221 +++ .../api/provision_broker.go | 151 ++ .../circonus-gometrics/api/rule_set.go | 234 +++ .../circonus-gometrics/api/rule_set_group.go | 231 +++ .../circonus-gometrics/api/user.go | 159 ++ .../circonus-gometrics/api/worksheet.go | 232 +++ vendor/vendor.json | 12 + website/source/assets/stylesheets/_docs.scss | 1 + .../source/docs/import/importability.html.md | 5 + .../circonus/d/account.html.markdown | 82 ++ .../circonus/d/collector.html.markdown | 98 ++ .../providers/circonus/index.html.markdown | 28 + .../providers/circonus/r/check.html.markdown | 520 +++++++ .../circonus/r/contact_group.html.markdown | 289 ++++ .../providers/circonus/r/graph.html.markdown | 179 +++ .../providers/circonus/r/metric.html.markdown | 73 + .../circonus/r/metric_cluster.html.markdown | 86 ++ .../circonus/r/rule_set.html.markdown | 377 +++++ website/source/layouts/circonus.erb | 60 + website/source/layouts/docs.erb | 4 + 87 files changed, 17812 insertions(+) create mode 100644 builtin/providers/circonus/GNUmakefile create mode 100644 builtin/providers/circonus/check.go create mode 100644 builtin/providers/circonus/consts.go create mode 100644 builtin/providers/circonus/data_source_circonus_account.go create mode 100644 builtin/providers/circonus/data_source_circonus_account_test.go create mode 100644 builtin/providers/circonus/data_source_circonus_collector.go create mode 100644 builtin/providers/circonus/data_source_circonus_collector_test.go create mode 100644 builtin/providers/circonus/interface.go create mode 100644 builtin/providers/circonus/metric.go create mode 100644 builtin/providers/circonus/metric_cluster.go create mode 100644 builtin/providers/circonus/metric_test.go create mode 100644 builtin/providers/circonus/provider.go create mode 100644 builtin/providers/circonus/provider_test.go create mode 100644 builtin/providers/circonus/resource_circonus_check.go create mode 100644 builtin/providers/circonus/resource_circonus_check_caql.go create mode 100644 builtin/providers/circonus/resource_circonus_check_caql_test.go create mode 100644 builtin/providers/circonus/resource_circonus_check_cloudwatch.go create mode 100644 builtin/providers/circonus/resource_circonus_check_cloudwatch_test.go create mode 100644 builtin/providers/circonus/resource_circonus_check_http.go create mode 100644 builtin/providers/circonus/resource_circonus_check_http_test.go create mode 100644 builtin/providers/circonus/resource_circonus_check_httptrap.go create mode 100644 builtin/providers/circonus/resource_circonus_check_httptrap_test.go create mode 100644 builtin/providers/circonus/resource_circonus_check_icmp_ping.go create mode 100644 builtin/providers/circonus/resource_circonus_check_icmp_ping_test.go create mode 100644 builtin/providers/circonus/resource_circonus_check_json.go create mode 100644 builtin/providers/circonus/resource_circonus_check_json_test.go create mode 100644 builtin/providers/circonus/resource_circonus_check_mysql.go create mode 100644 builtin/providers/circonus/resource_circonus_check_mysql_test.go create mode 100644 builtin/providers/circonus/resource_circonus_check_postgresql.go create mode 100644 builtin/providers/circonus/resource_circonus_check_postgresql_test.go create mode 100644 builtin/providers/circonus/resource_circonus_check_tcp.go create mode 100644 builtin/providers/circonus/resource_circonus_check_tcp_test.go create mode 100644 builtin/providers/circonus/resource_circonus_check_test.go create mode 100644 builtin/providers/circonus/resource_circonus_contact.go create mode 100644 builtin/providers/circonus/resource_circonus_contact_test.go create mode 100644 builtin/providers/circonus/resource_circonus_graph.go create mode 100644 builtin/providers/circonus/resource_circonus_graph_test.go create mode 100644 builtin/providers/circonus/resource_circonus_metric.go create mode 100644 builtin/providers/circonus/resource_circonus_metric_cluster.go create mode 100644 builtin/providers/circonus/resource_circonus_metric_cluster_test.go create mode 100644 builtin/providers/circonus/resource_circonus_metric_test.go create mode 100644 builtin/providers/circonus/resource_circonus_rule_set.go create mode 100644 builtin/providers/circonus/resource_circonus_rule_set_test.go create mode 100644 builtin/providers/circonus/tags.go create mode 100644 builtin/providers/circonus/types.go create mode 100644 builtin/providers/circonus/utils.go create mode 100644 builtin/providers/circonus/validators.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/LICENSE create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/README.md create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/account.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/acknowledgement.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/alert.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/annotation.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/api.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/broker.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/check.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/check_bundle.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/check_bundle_metrics.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/config/consts.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/contact_group.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/dashboard.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/doc.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/graph.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/maintenance.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/metric.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/metric_cluster.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/outlier_report.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/provision_broker.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/rule_set.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/rule_set_group.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/user.go create mode 100644 vendor/github.com/circonus-labs/circonus-gometrics/api/worksheet.go create mode 100644 website/source/docs/providers/circonus/d/account.html.markdown create mode 100644 website/source/docs/providers/circonus/d/collector.html.markdown create mode 100644 website/source/docs/providers/circonus/index.html.markdown create mode 100644 website/source/docs/providers/circonus/r/check.html.markdown create mode 100644 website/source/docs/providers/circonus/r/contact_group.html.markdown create mode 100644 website/source/docs/providers/circonus/r/graph.html.markdown create mode 100644 website/source/docs/providers/circonus/r/metric.html.markdown create mode 100644 website/source/docs/providers/circonus/r/metric_cluster.html.markdown create mode 100644 website/source/docs/providers/circonus/r/rule_set.html.markdown create mode 100644 website/source/layouts/circonus.erb 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