From 2a4bbba60c7309b290cf2f8f136f56d4d22906a0 Mon Sep 17 00:00:00 2001 From: dwalasek <138129050+dwalasek@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:43:34 +0100 Subject: [PATCH] slo: dedicated slo resource in terraform provider (#463) * slo: dedicated slo resource in terraform provider * slo: dedicated slo resource in terraform provider * slo: dedicated slo resource in terraform provider * slo: dedicated slo resource in terraform provider * slo: dedicated slo resource in terraform provider * Update .github/workflows/build-and-test.yml Co-authored-by: Ben Keith <130169123+benkeith-splunk@users.noreply.github.com> --------- Co-authored-by: Ben Keith <130169123+benkeith-splunk@users.noreply.github.com> --- .github/workflows/build-and-test.yml | 6 +- CHANGELOG.md | 5 + go.mod | 42 +- go.sum | 98 ++-- signalfx/provider.go | 1 + signalfx/resource_signalfx_detector.go | 279 ++++----- signalfx/resource_signalfx_slo.go | 761 +++++++++++++++++++++++++ signalfx/resource_signalfx_slo_test.go | 313 ++++++++++ website/docs/r/slo.html.markdown | 167 ++++++ 9 files changed, 1473 insertions(+), 199 deletions(-) create mode 100644 signalfx/resource_signalfx_slo.go create mode 100644 signalfx/resource_signalfx_slo_test.go create mode 100644 website/docs/r/slo.html.markdown diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 8a9038bf..de6f3e96 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -7,7 +7,7 @@ on: pull_request: env: - GO_VERSION: 1.19 + GO_VERSION: "1.20" jobs: build: @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: go-version: ${{ env.GO_VERSION }} @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: go-version: ${{ env.GO_VERSION }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 15a821a4..d8caa5c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## Unreleased +## 9.1.0 + +IMPROVEMENTS: +* Add resource/resource_signalfx_slo to support creating SLO via terraform [#463](https://github.com/splunk-terraform/terraform-provider-signalfx/pull/463) + ## 9.0.1 IMPROVEMENTS: diff --git a/go.mod b/go.mod index 557dabf5..2a9c4c40 100644 --- a/go.mod +++ b/go.mod @@ -1,42 +1,42 @@ module github.com/splunk-terraform/terraform-provider-signalfx -go 1.19 +go 1.20 require ( github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d github.com/davecgh/go-spew v1.1.1 github.com/hashicorp/go-retryablehttp v0.7.4 - github.com/hashicorp/terraform-plugin-sdk/v2 v2.26.1 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0 github.com/mitchellh/go-homedir v1.1.0 - github.com/signalfx/signalfx-go v1.33.0 + github.com/signalfx/signalfx-go v1.34.0 github.com/stretchr/testify v1.8.2 ) require ( - github.com/ProtonMail/go-crypto v0.0.0-20230619160724-3fbb1f12458c // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/agext/levenshtein v1.2.3 // indirect - github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/cloudflare/circl v1.3.3 // indirect github.com/fatih/color v1.15.0 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.4.10 // indirect + github.com/hashicorp/go-plugin v1.5.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-version v1.6.0 // indirect - github.com/hashicorp/hc-install v0.5.2 // indirect - github.com/hashicorp/hcl/v2 v2.17.0 // indirect + github.com/hashicorp/hc-install v0.6.1 // indirect + github.com/hashicorp/hcl/v2 v2.19.1 // indirect github.com/hashicorp/logutils v1.0.0 // indirect - github.com/hashicorp/terraform-exec v0.18.1 // indirect - github.com/hashicorp/terraform-json v0.17.0 // indirect - github.com/hashicorp/terraform-plugin-go v0.16.0 // indirect + github.com/hashicorp/terraform-exec v0.19.0 // indirect + github.com/hashicorp/terraform-json v0.17.1 // indirect + github.com/hashicorp/terraform-plugin-go v0.19.0 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect - github.com/hashicorp/terraform-registry-address v0.2.1 // indirect + github.com/hashicorp/terraform-registry-address v0.2.2 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/kr/pretty v0.3.0 // indirect @@ -53,17 +53,17 @@ require ( github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/zclconf/go-cty v1.13.2 // indirect - golang.org/x/crypto v0.10.0 // indirect - golang.org/x/mod v0.11.0 // indirect - golang.org/x/net v0.11.0 // indirect + github.com/zclconf/go-cty v1.14.1 // indirect + golang.org/x/crypto v0.15.0 // indirect + golang.org/x/mod v0.13.0 // indirect + golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.9.0 // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/text v0.10.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/text v0.14.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/grpc v1.56.1 // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/grpc v1.57.1 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 130246a7..9cc578b3 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,21 @@ -github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= -github.com/ProtonMail/go-crypto v0.0.0-20230619160724-3fbb1f12458c h1:figwFwYep1Qnl64Y+Rc8tyQWE0xvYAN+5EX+rD40pTU= -github.com/ProtonMail/go-crypto v0.0.0-20230619160724-3fbb1f12458c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= -github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= -github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -20,10 +23,11 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= -github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= -github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= -github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-git/v5 v5.9.0 h1:cD9SFA7sHVRdJ7AYck1ZaAa/yeuBvGPxwXDL8cxrObY= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -31,8 +35,8 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -48,8 +52,8 @@ github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+ github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.4.10 h1:xUbmA4jC6Dq163/fWcp8P3JuHilrHHMLNRxzGQJ9hNk= -github.com/hashicorp/go-plugin v1.4.10/go.mod h1:6/1TEzT0eQznvI/gV2CM29DLSkAK/e58mUWKVsPaph0= +github.com/hashicorp/go-plugin v1.5.1 h1:oGm7cWBaYIp3lJpx1RUEfLWophprE2EV/KUeqBYo+6k= +github.com/hashicorp/go-plugin v1.5.1/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -57,31 +61,30 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hc-install v0.5.2 h1:SfwMFnEXVVirpwkDuSF5kymUOhrUxrTq3udEseZdOD0= -github.com/hashicorp/hc-install v0.5.2/go.mod h1:9QISwe6newMWIfEiXpzuu1k9HAGtQYgnSH8H9T8wmoI= -github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= -github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= +github.com/hashicorp/hc-install v0.6.1 h1:IGxShH7AVhPaSuSJpKtVi/EFORNjO+OYVJJrAtGG2mY= +github.com/hashicorp/hc-install v0.6.1/go.mod h1:0fW3jpg+wraYSnFDJ6Rlie3RvLf1bIqVIkzoon4KoVE= +github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI= +github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.18.1 h1:LAbfDvNQU1l0NOQlTuudjczVhHj061fNX5H8XZxHlH4= -github.com/hashicorp/terraform-exec v0.18.1/go.mod h1:58wg4IeuAJ6LVsLUeD2DWZZoc/bYi6dzhLHzxM41980= -github.com/hashicorp/terraform-json v0.17.0 h1:EiA1Wp07nknYQAiv+jIt4dX4Cq5crgP+TsTE45MjMmM= -github.com/hashicorp/terraform-json v0.17.0/go.mod h1:Huy6zt6euxaY9knPAFKjUITn8QxUFIe9VuSzb4zn/0o= -github.com/hashicorp/terraform-plugin-go v0.16.0 h1:DSOQ0rz5FUiVO4NUzMs8ln9gsPgHMTsfns7Nk+6gPuE= -github.com/hashicorp/terraform-plugin-go v0.16.0/go.mod h1:4sn8bFuDbt+2+Yztt35IbOrvZc0zyEi87gJzsTgCES8= +github.com/hashicorp/terraform-exec v0.19.0 h1:FpqZ6n50Tk95mItTSS9BjeOVUb4eg81SpgVtZNNtFSM= +github.com/hashicorp/terraform-exec v0.19.0/go.mod h1:tbxUpe3JKruE9Cuf65mycSIT8KiNPZ0FkuTE3H4urQg= +github.com/hashicorp/terraform-json v0.17.1 h1:eMfvh/uWggKmY7Pmb3T85u86E2EQg6EQHgyRwf3RkyA= +github.com/hashicorp/terraform-json v0.17.1/go.mod h1:Huy6zt6euxaY9knPAFKjUITn8QxUFIe9VuSzb4zn/0o= +github.com/hashicorp/terraform-plugin-go v0.19.0 h1:BuZx/6Cp+lkmiG0cOBk6Zps0Cb2tmqQpDM3iAtnhDQU= +github.com/hashicorp/terraform-plugin-go v0.19.0/go.mod h1:EhRSkEPNoylLQntYsk5KrDHTZJh9HQoumZXbOGOXmec= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.26.1 h1:G9WAfb8LHeCxu7Ae8nc1agZlQOSCUWsb610iAogBhCs= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.26.1/go.mod h1:xcOSYlRVdPLmDUoqPhO9fiO/YCN/l6MGYeTzGt5jgkQ= -github.com/hashicorp/terraform-registry-address v0.2.1 h1:QuTf6oJ1+WSflJw6WYOHhLgwUiQ0FrROpHPYFtwTYWM= -github.com/hashicorp/terraform-registry-address v0.2.1/go.mod h1:BSE9fIFzp0qWsJUUyGquo4ldV9k2n+psif6NYkBRS3Y= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0 h1:X7vB6vn5tON2b49ILa4W7mFAsndeqJ7bZFOGbVO+0Cc= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0/go.mod h1:ydFcxbdj6klCqYEPkPvdvFKiNGKZLUs+896ODUXCyao= +github.com/hashicorp/terraform-registry-address v0.2.2 h1:lPQBg403El8PPicg/qONZJDC6YlgCVbWDtNmmZKtBno= +github.com/hashicorp/terraform-registry-address v0.2.2/go.mod h1:LtwNbCihUoUZ3RYriyS2wF/lGPB6gF9ICLRtuDk7hSo= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -122,9 +125,9 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.6.2 h1:aIihoIOHCiLZHxyoNQ+ABL4NKhFTgKLBdMLyEAh98m0= github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/signalfx/signalfx-go v1.33.0 h1:+v1fa+is8rYSxGoN1W+9PiDj1dCF5sVjJx60dhNLsTA= -github.com/signalfx/signalfx-go v1.33.0/go.mod h1:IpGZLPvCKNFyspAXoS480jB02mocTpo0KYd8jbl6/T8= -github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0= +github.com/signalfx/signalfx-go v1.34.0 h1:OQ6tyMY4efWB57EPIQqrpWrAfcSdyfa+bLtmAe7GLfE= +github.com/signalfx/signalfx-go v1.34.0/go.mod h1:IpGZLPvCKNFyspAXoS480jB02mocTpo0KYd8jbl6/T8= +github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -144,18 +147,18 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= -github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA= +github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -164,8 +167,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs= golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -187,8 +190,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -201,12 +204,13 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -214,12 +218,12 @@ google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ= -google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.57.1 h1:upNTNqv0ES+2ZOOqACwVtS3Il8M12/+Hz41RCPzAjQg= +google.golang.org/grpc v1.57.1/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/signalfx/provider.go b/signalfx/provider.go index 7cde32c4..1a8e87c8 100644 --- a/signalfx/provider.go +++ b/signalfx/provider.go @@ -115,6 +115,7 @@ func Provider() *schema.Provider { "signalfx_log_timeline": logTimelineResource(), "signalfx_table_chart": tableChartResource(), "signalfx_metric_ruleset": metricRulesetResource(), + "signalfx_slo": sloResource(), }, ConfigureFunc: signalfxConfigure, } diff --git a/signalfx/resource_signalfx_detector.go b/signalfx/resource_signalfx_detector.go index e3c32f46..728986f9 100644 --- a/signalfx/resource_signalfx_detector.go +++ b/signalfx/resource_signalfx_detector.go @@ -20,6 +20,62 @@ const ( DetectorAppPath = "/detector/" ) +var ( + detectorRuleSchema = map[string]*schema.Schema{ + "severity": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateSeverity, + Description: "The severity of the rule, must be one of: Critical, Warning, Major, Minor, Info", + }, + "detect_label": { + Type: schema.TypeString, + Required: true, + Description: "A detect label which matches a detect label within the program text", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "Description of the rule", + }, + "notifications": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateNotification, + }, + Description: "List of strings specifying where notifications will be sent when an incident occurs. See https://developers.signalfx.com/v2/docs/detector-model#notifications-models for more info", + }, + "disabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "(default: false) When true, notifications and events will not be generated for the detect label", + }, + "parameterized_body": { + Type: schema.TypeString, + Optional: true, + Description: "Custom notification message body when an alert is triggered. See https://developers.signalfx.com/v2/reference#detector-model for more info", + }, + "parameterized_subject": { + Type: schema.TypeString, + Optional: true, + Description: "Custom notification message subject when an alert is triggered. See https://developers.signalfx.com/v2/reference#detector-model for more info", + }, + "runbook_url": { + Type: schema.TypeString, + Optional: true, + Description: "URL of page to consult when an alert is triggered", + }, + "tip": { + Type: schema.TypeString, + Optional: true, + Description: "Plain text suggested first course of action, such as a command to execute.", + }, + } +) + func detectorResource() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -32,7 +88,7 @@ func detectorResource() *schema.Resource { Type: schema.TypeString, Required: true, Description: "Signalflow program text for the detector. More info at \"https://developers.signalfx.com/docs/signalflow-overview\"", - ValidateFunc: validation.StringLenBetween(18, 50000), + ValidateFunc: validation.StringLenBetween(1, 50000), }, "description": { Type: schema.TypeString, @@ -113,108 +169,56 @@ func detectorResource() *schema.Resource { Required: true, Description: "Set of rules used for alerting", Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "severity": &schema.Schema{ - Type: schema.TypeString, - Required: true, - ValidateFunc: validateSeverity, - Description: "The severity of the rule, must be one of: Critical, Warning, Major, Minor, Info", - }, - "detect_label": &schema.Schema{ - Type: schema.TypeString, - Required: true, - Description: "A detect label which matches a detect label within the program text", - }, - "description": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Description: "Description of the rule", - }, - "notifications": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateFunc: validateNotification, - }, - Description: "List of strings specifying where notifications will be sent when an incident occurs. See https://developers.signalfx.com/v2/docs/detector-model#notifications-models for more info", - }, - "disabled": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "(default: false) When true, notifications and events will not be generated for the detect label", - }, - "parameterized_body": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Description: "Custom notification message body when an alert is triggered. See https://developers.signalfx.com/v2/reference#detector-model for more info", - }, - "parameterized_subject": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Description: "Custom notification message subject when an alert is triggered. See https://d evelopers.signalfx.com/v2/reference#detector-model for more info", - }, - "runbook_url": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Description: "URL of page to consult when an alert is triggered", - }, - "tip": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Description: "Plain text suggested first course of action, such as a command to execute.", - }, - }, + Schema: detectorRuleSchema, }, Set: resourceRuleHash, }, - "authorized_writer_teams": &schema.Schema{ + "authorized_writer_teams": { Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, Description: "Team IDs that have write access to this dashboard", }, - "authorized_writer_users": &schema.Schema{ + "authorized_writer_users": { Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, Description: "User IDs that have write access to this dashboard", }, - "viz_options": &schema.Schema{ + "viz_options": { Type: schema.TypeSet, Optional: true, Description: "Plot-level customization options, associated with a publish statement", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "label": &schema.Schema{ + "label": { Type: schema.TypeString, Required: true, Description: "The label used in the publish statement that displays the plot (metric time series data) you want to customize", }, - "color": &schema.Schema{ + "color": { Type: schema.TypeString, Optional: true, Description: "Color to use", ValidateFunc: validatePerSignalColor, }, - "display_name": &schema.Schema{ + "display_name": { Type: schema.TypeString, Optional: true, Description: "Specifies an alternate value for the Plot Name column of the Data Table associated with the chart.", }, - "value_unit": &schema.Schema{ + "value_unit": { Type: schema.TypeString, Optional: true, ValidateFunc: validateUnitTimeChart, Description: "A unit to attach to this plot. Units support automatic scaling (eg thousands of bytes will be displayed as kilobytes)", }, - "value_prefix": &schema.Schema{ + "value_prefix": { Type: schema.TypeString, Optional: true, Description: "An arbitrary prefix to display with the value of this plot", }, - "value_suffix": &schema.Schema{ + "value_suffix": { Type: schema.TypeString, Optional: true, Description: "An arbitrary suffix to display with the value of this plot", @@ -222,7 +226,7 @@ func detectorResource() *schema.Resource { }, }, }, - "label_resolutions": &schema.Schema{ + "label_resolutions": { Type: schema.TypeMap, Computed: true, Description: "Resolutions of the detector alerts in milliseconds that indicate how often data is analyzed to determine if an alert should be triggered", @@ -230,7 +234,7 @@ func detectorResource() *schema.Resource { Type: schema.TypeInt, }, }, - "url": &schema.Schema{ + "url": { Type: schema.TypeString, Computed: true, Description: "URL of the detector", @@ -292,50 +296,9 @@ func getPayloadDetector(d *schema.ResourceData) (*detector.CreateUpdateDetectorR rulesList := make([]*detector.Rule, len(tfRules)) for i, tfRule := range tfRules { tfRule := tfRule.(map[string]interface{}) - rule := &detector.Rule{ - Description: tfRule["description"].(string), - DetectLabel: tfRule["detect_label"].(string), - Disabled: tfRule["disabled"].(bool), - } - - tfSev := tfRule["severity"].(string) - sev := detector.INFO - switch tfSev { - case "Critical": - sev = detector.CRITICAL - case "Warning": - sev = detector.WARNING - case "Major": - sev = detector.MAJOR - case "Minor": - sev = detector.MINOR - case "Info": - sev = detector.INFO - } - rule.Severity = sev - - if val, ok := tfRule["parameterized_body"]; ok { - rule.ParameterizedBody = val.(string) - } - - if val, ok := tfRule["parameterized_subject"]; ok { - rule.ParameterizedSubject = val.(string) - } - - if val, ok := tfRule["runbook_url"]; ok { - rule.RunbookUrl = val.(string) - } - - if val, ok := tfRule["tip"]; ok { - rule.Tip = val.(string) - } - - if notifications, ok := tfRule["notifications"]; ok { - notify, err := getNotifications(notifications.([]interface{})) - if err != nil { - return nil, err - } - rule.Notifications = notify + rule, err := getDetectorRule(tfRule) + if err != nil { + return nil, err } rulesList[i] = rule } @@ -393,6 +356,58 @@ func getPayloadDetector(d *schema.ResourceData) (*detector.CreateUpdateDetectorR return cudr, nil } +func getDetectorRule(tfRule map[string]interface{}) (*detector.Rule, error) { + rule := &detector.Rule{ + Description: tfRule["description"].(string), + Disabled: tfRule["disabled"].(bool), + } + + if detectLabel, ok := tfRule["detect_label"]; ok { + rule.DetectLabel = detectLabel.(string) + } + + tfSev := tfRule["severity"].(string) + sev := detector.INFO + switch tfSev { + case "Critical": + sev = detector.CRITICAL + case "Warning": + sev = detector.WARNING + case "Major": + sev = detector.MAJOR + case "Minor": + sev = detector.MINOR + case "Info": + sev = detector.INFO + } + rule.Severity = sev + + if val, ok := tfRule["parameterized_body"]; ok { + rule.ParameterizedBody = val.(string) + } + + if val, ok := tfRule["parameterized_subject"]; ok { + rule.ParameterizedSubject = val.(string) + } + + if val, ok := tfRule["runbook_url"]; ok { + rule.RunbookUrl = val.(string) + } + + if val, ok := tfRule["tip"]; ok { + rule.Tip = val.(string) + } + + if notifications, ok := tfRule["notifications"]; ok { + notify, err := getNotifications(notifications.([]interface{})) + if err != nil { + return nil, err + } + rule.Notifications = notify + } + return rule, nil +} + func getVisualizationOptionsDetector(d *schema.ResourceData) *detector.Visualization { viz := detector.Visualization{} @@ -622,25 +637,10 @@ func detectorAPIToTF(d *schema.ResourceData, det *detector.Detector) error { rules := make([]map[string]interface{}, len(det.Rules)) for i, r := range det.Rules { - rule := make(map[string]interface{}) - rule["severity"] = r.Severity - rule["detect_label"] = r.DetectLabel - rule["description"] = r.Description - - notifications := make([]string, len(r.Notifications)) - for i, not := range r.Notifications { - tfNot, err := getNotifyStringFromAPI(not) - if err != nil { - return err - } - notifications[i] = tfNot - } - rule["notifications"] = notifications - rule["disabled"] = r.Disabled - rule["parameterized_body"] = r.ParameterizedBody - rule["parameterized_subject"] = r.ParameterizedSubject - rule["runbook_url"] = r.RunbookUrl - rule["tip"] = r.Tip + rule, err := getTfDetectorRule(r) + if err != nil { + return err + } rules[i] = rule } if err := d.Set("rule", rules); err != nil { @@ -650,6 +650,29 @@ func detectorAPIToTF(d *schema.ResourceData, det *detector.Detector) error { return nil } +func getTfDetectorRule(r *detector.Rule) (map[string]interface{}, error) { + rule := make(map[string]interface{}) + rule["severity"] = r.Severity + rule["detect_label"] = r.DetectLabel + rule["description"] = r.Description + + notifications := make([]string, len(r.Notifications)) + for i, not := range r.Notifications { + tfNot, err := getNotifyStringFromAPI(not) + if err != nil { + return nil, err + } + notifications[i] = tfNot + } + rule["notifications"] = notifications + rule["disabled"] = r.Disabled + rule["parameterized_body"] = r.ParameterizedBody + rule["parameterized_subject"] = r.ParameterizedSubject + rule["runbook_url"] = r.RunbookUrl + rule["tip"] = r.Tip + return rule, nil +} + func detectorUpdate(d *schema.ResourceData, meta interface{}) error { config := meta.(*signalfxConfig) payload, err := getPayloadDetector(d) diff --git a/signalfx/resource_signalfx_slo.go b/signalfx/resource_signalfx_slo.go new file mode 100644 index 00000000..4e646ed0 --- /dev/null +++ b/signalfx/resource_signalfx_slo.go @@ -0,0 +1,761 @@ +package signalfx + +import ( + "context" + "encoding/json" + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/signalfx/signalfx-go/detector" + "github.com/signalfx/signalfx-go/slo" + "log" + "net/http" + "sort" + "strconv" + "strings" + "time" +) + +const ( + nameLabel = "name" + descriptionLabel = "description" + typeLabel = "type" + inputLabel = "input" + programTextLabel = "program_text" + goodEventsLabel = "good_events_label" + totalEventsLabel = "total_events_label" + targetLabel = "target" + sloLabel = "slo" + compliancePeriodLabel = "compliance_period" + alertRuleLabel = "alert_rule" + ruleLabel = "rule" + parametersLabel = "parameters" + fireLastingLabel = "fire_lasting" + percentOfLastingLabel = "percent_of_lasting" + percentErrorBudgetLeftLabel = "percent_error_budget_left" + shortWindow1Label = "short_window_1" + longWindow1Label = "long_window_1" + shortWindow2Label = "short_window_2" + longWindow2Label = "long_window_2" + burnRateThreshold1Label = "burn_rate_threshold_1" + burnRateThreshold2Label = "burn_rate_threshold_2" +) + +func sloResource() *schema.Resource { + + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + nameLabel: { + Type: schema.TypeString, + Required: true, + Description: "Name of the SLO", + ValidateFunc: validation.StringLenBetween(0, 256), + }, + descriptionLabel: { + Type: schema.TypeString, + Optional: true, + Description: "Description of the SLO", + ValidateFunc: validation.StringLenBetween(0, 1024), + }, + typeLabel: { + Type: schema.TypeString, + Required: true, + Description: "Type of the SLO. Currently only RequestBased SLO is supported", + ValidateFunc: validation.StringInSlice([]string{slo.RequestBased}, false), + }, + inputLabel: { + Type: schema.TypeList, + Required: true, + Description: "SignalFlow program and arguments text strings that define the streams used as successful event count and total event count", + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + programTextLabel: { + Type: schema.TypeString, + Required: true, + Description: "Signalflow program text for the SLO. More info at \"https://dev.splunk.com/observability/docs/signalflow\". " + + "We require this Signalflow program text to contain at least 2 data blocks - one for the total stream and one for the good stream, whose labels are specified by goodEventsLabel and totalEventsLabel", + ValidateFunc: validation.StringLenBetween(18, 50000), + }, + goodEventsLabel: { + Type: schema.TypeString, + Optional: true, + Description: "Label used in `program_text` that refers to the data block which contains the stream of successful events", + ValidateFunc: validation.StringIsNotEmpty, + }, + totalEventsLabel: { + Type: schema.TypeString, + Optional: true, + Description: "Label used in `program_text` that refers to the data block which contains the stream of total events", + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + targetLabel: { + Type: schema.TypeList, + Required: true, + Description: "Define target value of the service level indicator in the appropriate time period.", + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + typeLabel: { + Type: schema.TypeString, + Required: true, + Description: "SLO target type can be the following type: `RollingWindow`", + ValidateFunc: validation.StringInSlice([]string{slo.RollingWindowTarget}, false), + }, + sloLabel: { + Type: schema.TypeFloat, + Required: true, + Description: "Target value in the form of a percentage", + ValidateFunc: validation.FloatBetween(0, 100.0), + }, + compliancePeriodLabel: { + Type: schema.TypeString, + Optional: true, + Description: "(Required for `RollingWindow` type) Compliance period of this SLO. This value must be within the range of 1d (1 days) to 30d (30 days), inclusive.", + ValidateFunc: validation.StringIsNotEmpty, + }, + alertRuleLabel: { + Type: schema.TypeList, + Required: true, + Description: "SLO alert rules", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + typeLabel: { + Type: schema.TypeString, + Required: true, + Description: "SLO alert rule type", + ValidateFunc: validation.StringInSlice([]string{slo.BreachRule, slo.ErrorBudgetLeftRule, slo.BurnRateRule}, false), + }, + ruleLabel: { + Type: schema.TypeList, + Required: true, + Description: "Set of rules used for alerting", + Elem: &schema.Resource{ + SchemaFunc: func() map[string]*schema.Schema { + // The alert rules for SLO are very similar to those in Detector with 2 exceptions: + // 1. We don't expect detect_label. The user can send it - but we will ignore it - so we remove it from the TF schema here + // 2. There is an additional field called parameters, which the user can use to parameterize the program text of the SLO + ruleSchema := make(map[string]*schema.Schema) + + for k, v := range detectorRuleSchema { + if k != "detect_label" { + ruleSchema[k] = v + } + } + + ruleSchema[parametersLabel] = &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Description: "Parameters for the SLO alert rule. Each SLO alert rule type accepts different parameters. If not specified, default parameters are used.", + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + fireLastingLabel: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Duration that indicates how long the alert condition is met before the alert is triggered. The value must be positive and smaller than the compliance period of the SLO target. Note: BREACH and ERROR_BUDGET_LEFT alert rules use the fire_lasting parameter", + }, + percentOfLastingLabel: { + Type: schema.TypeFloat, + Optional: true, + Computed: true, + Description: "Percentage of the fire_lasting duration that the alert condition is met before the alert is triggered. Note: BREACH and ERROR_BUDGET_LEFT alert rules use the percent_of_lasting parameter", + }, + percentErrorBudgetLeftLabel: { + Type: schema.TypeFloat, + Optional: true, + Computed: true, + Description: "Error budget must be equal to or smaller than this percentage for the alert to be triggered. Note: ERROR_BUDGET_LEFT alert rules use the percent_error_budget_left parameter.", + }, + shortWindow1Label: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Short window 1 used in burn rate alert calculation. This value must be longer than 1/30 of long_window_1. Note: BURN_RATE alert rules use the short_window_1 parameter.", + }, + shortWindow2Label: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Short window 2 used in burn rate alert calculation. This value must be longer than 1/30 of long_window_2. Note: BURN_RATE alert rules use the short_window_2 parameter.", + }, + longWindow1Label: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Long window 1 used in burn rate alert calculation. This value must be longer than short_window_1` and shorter than 90 days. Note: BURN_RATE alert rules use the long_window_1 parameter. ", + }, + longWindow2Label: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Long window 2 used in burn rate alert calculation. This value must be longer than short_window_2` and shorter than 90 days. Note: BURN_RATE alert rules use the long_window_2 parameter. ", + }, + burnRateThreshold1Label: { + Type: schema.TypeFloat, + Optional: true, + Computed: true, + Description: "Burn rate threshold 1 used in burn rate alert calculation. This value must be between 0 and 100/(100-SLO target). Note: BURN_RATE alert rules use the burn_rate_threshold_1 parameter.", + }, + burnRateThreshold2Label: { + Type: schema.TypeFloat, + Optional: true, + Computed: true, + Description: "Burn rate threshold 2 used in burn rate alert calculation. This value must be between 0 and 100/(100-SLO target). Note: BURN_RATE alert rules use the burn_rate_threshold_2 parameter.", + }, + }, + }, + } + + return ruleSchema + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + + SchemaVersion: 1, + + CustomizeDiff: sloValidate, + CreateContext: sloCreate, + ReadContext: sloRead, + UpdateContext: sloUpdate, + DeleteContext: sloDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +func sloValidate(ctx context.Context, sloObject *schema.ResourceDiff, config interface{}) error { + payloadSlo, err := getPayloadSlo(sloObject) + + // we are checking the uniqueness of the slo name, so we need some randomness here if it's an SLO update(resource ID exists) + if sloObject.Id() != "" { + payloadSlo.Name = payloadSlo.Name + time.Now().String() + } + + if err != nil { + return err + } + + err = config.(*signalfxConfig).Client.ValidateSlo(ctx, payloadSlo) + if err != nil { + return err + } + + return nil +} + +func sloCreate(ctx context.Context, sloResource *schema.ResourceData, config interface{}) diag.Diagnostics { + client := config.(*signalfxConfig).Client + payload, err := getPayloadSlo(sloResource) + if err != nil { + return diag.Errorf("Failed creating SLO json payload: %s", err.Error()) + } + + debugOutput, _ := json.Marshal(payload) + log.Printf("[DEBUG] Create SLO Payload: %s", string(debugOutput)) + + createdSlo, err := client.CreateSlo(ctx, payload) + if err != nil { + return diag.FromErr(err) + } + + id := createdSlo.Id + sloResource.SetId(id) + + err = sloAPIToTF(sloResource, createdSlo) + return diag.FromErr(err) +} + +func sloUpdate(ctx context.Context, sloResource *schema.ResourceData, config interface{}) diag.Diagnostics { + client := config.(*signalfxConfig).Client + payload, err := getPayloadSlo(sloResource) + if err != nil { + return diag.Errorf("Failed creating SLO json payload: %s", err.Error()) + } + + debugOutput, _ := json.Marshal(payload) + log.Printf("[DEBUG] Update SLO Payload: %s", string(debugOutput)) + + updatedSlo, err := client.UpdateSlo(ctx, sloResource.Id(), payload) + if err != nil { + return diag.FromErr(err) + } + + err = sloAPIToTF(sloResource, updatedSlo) + return diag.FromErr(err) +} + +func sloRead(ctx context.Context, sloResource *schema.ResourceData, config interface{}) diag.Diagnostics { + client := config.(*signalfxConfig).Client + + returnedSlo, err := client.GetSlo(ctx, sloResource.Id()) + if err != nil { + if isSloNotFound(err) { + sloResource.SetId("") + } + return diag.FromErr(err) + } + + err = sloAPIToTF(sloResource, returnedSlo) + return diag.FromErr(err) +} + +func sloDelete(ctx context.Context, d *schema.ResourceData, config interface{}) diag.Diagnostics { + client := config.(*signalfxConfig).Client + + err := client.DeleteSlo(ctx, d.Id()) + + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func isSloNotFound(err error) bool { + return strings.Contains(err.Error(), strconv.Itoa(http.StatusNotFound)) +} + +// getPayloadSlo is called with objects: *schema.ResourceDiff or *schema.ResourceData - that's why we need this interface to have only one method +type Resource interface { + Get(key string) interface{} +} + +func getPayloadSlo(sloTfResource Resource) (*slo.SloObject, error) { + sloApiObject := &slo.SloObject{ + BaseSlo: slo.BaseSlo{ + Name: sloTfResource.Get(nameLabel).(string), + Description: sloTfResource.Get(descriptionLabel).(string), + Type: sloTfResource.Get(typeLabel).(string), + }, + } + + targets, err := getApiTargets(sloTfResource.Get(targetLabel).([]interface{})) + if err != nil { + return nil, err + } + sloApiObject.Targets = targets + + return setSloInput(sloTfResource, sloApiObject) +} + +func getApiTargets(tfTargets []interface{}) ([]slo.SloTarget, error) { + apiTargets := make([]slo.SloTarget, len(tfTargets)) + + for ind, rawTfTarget := range tfTargets { + tfTarget := rawTfTarget.(map[string]interface{}) + + apiTarget := slo.SloTarget{ + BaseSloTarget: slo.BaseSloTarget{ + Slo: tfTarget[sloLabel].(float64), + Type: tfTarget[typeLabel].(string), + }, + } + + switch apiTarget.Type { + case slo.RollingWindowTarget: + apiTarget.RollingWindowSloTarget = &slo.RollingWindowSloTarget{ + CompliancePeriod: tfTarget[compliancePeriodLabel].(string), + } + default: + return nil, fmt.Errorf("unsupported SLO target type: %s", apiTarget.Type) + } + + alertRules, err := getApiAlertRules(tfTarget[alertRuleLabel].([]interface{})) + if err != nil { + return nil, err + } + + apiTarget.SloAlertRules = alertRules + apiTargets[ind] = apiTarget + } + + return apiTargets, nil + +} + +func getApiAlertRules(tfAlertRules []interface{}) ([]slo.SloAlertRule, error) { + apiAlertRules := make([]slo.SloAlertRule, len(tfAlertRules)) + + for ind, rawTfAlertRule := range tfAlertRules { + tfAlertRule := rawTfAlertRule.(map[string]interface{}) + + alertType := tfAlertRule[typeLabel].(string) + switch alertType { + case slo.BreachRule: + detectorRules, err := getApiDetectorRules[slo.BreachDetectorRule]( + tfAlertRule[ruleLabel].([]interface{}), + func(rule *detector.Rule) *slo.BreachDetectorRule { + return &slo.BreachDetectorRule{ + Rule: *rule, + } + }, + func(rule *slo.BreachDetectorRule, parameters map[string]interface{}) { + rule.Parameters = &slo.BreachDetectorParameters{ + FireLasting: parameters[fireLastingLabel].(string), + PercentOfLasting: parameters[percentOfLastingLabel].(float64), + } + }, + ) + if err != nil { + return nil, err + } + + apiAlertRules[ind].BreachSloAlertRule = &slo.BreachSloAlertRule{ + Rules: detectorRules, + } + apiAlertRules[ind].Type = alertType + + case slo.ErrorBudgetLeftRule: + detectorRules, err := getApiDetectorRules[slo.ErrorBudgetLeftDetectorRule]( + tfAlertRule[ruleLabel].([]interface{}), + func(rule *detector.Rule) *slo.ErrorBudgetLeftDetectorRule { + return &slo.ErrorBudgetLeftDetectorRule{ + Rule: *rule, + } + }, + func(rule *slo.ErrorBudgetLeftDetectorRule, parameters map[string]interface{}) { + rule.Parameters = &slo.ErrorBudgetLeftDetectorParameters{ + FireLasting: parameters[fireLastingLabel].(string), + PercentOfLasting: parameters[percentOfLastingLabel].(float64), + PercentErrorBudgetLeft: parameters[percentErrorBudgetLeftLabel].(float64), + } + }, + ) + if err != nil { + return nil, err + } + + apiAlertRules[ind].ErrorBudgetLeftSloAlertRule = &slo.ErrorBudgetLeftSloAlertRule{ + Rules: detectorRules, + } + apiAlertRules[ind].Type = alertType + case slo.BurnRateRule: + detectorRules, err := getApiDetectorRules[slo.BurnRateDetectorRule]( + tfAlertRule[ruleLabel].([]interface{}), + func(rule *detector.Rule) *slo.BurnRateDetectorRule { + return &slo.BurnRateDetectorRule{ + Rule: *rule, + } + }, + func(rule *slo.BurnRateDetectorRule, parameters map[string]interface{}) { + rule.Parameters = &slo.BurnRateDetectorParameters{ + ShortWindow1: parameters[shortWindow1Label].(string), + LongWindow1: parameters[longWindow1Label].(string), + ShortWindow2: parameters[shortWindow2Label].(string), + LongWindow2: parameters[longWindow2Label].(string), + BurnRateThreshold1: parameters[burnRateThreshold1Label].(float64), + BurnRateThreshold2: parameters[burnRateThreshold2Label].(float64), + } + }, + ) + if err != nil { + return nil, err + } + + apiAlertRules[ind].BurnRateSloAlertRule = &slo.BurnRateSloAlertRule{ + Rules: detectorRules, + } + apiAlertRules[ind].Type = alertType + default: + return nil, fmt.Errorf("unsupported SLO alert rule type: %s", alertType) + } + } + + return apiAlertRules, nil +} + +type DetectorRuleType interface { + slo.BreachDetectorRule | slo.ErrorBudgetLeftDetectorRule | slo.BurnRateDetectorRule +} + +func getApiDetectorRules[DetectorRule DetectorRuleType]( + tfRules []interface{}, + newSloDetectorRule func(*detector.Rule) *DetectorRule, + setSloDetectorParameters func(rule *DetectorRule, source map[string]interface{})) ([]*DetectorRule, error) { + + apiDetectorRules := make([]*DetectorRule, len(tfRules)) + + for ind, tfRule := range tfRules { + detectorRule, err := getDetectorRule(tfRule.(map[string]interface{})) + if err != nil { + return nil, err + } + + apiDetectorRules[ind] = newSloDetectorRule(detectorRule) + + parameters, err := getParametersFromRule(tfRule.(map[string]interface{})) + if err != nil { + return nil, err + } + + if parameters != nil { + setSloDetectorParameters(apiDetectorRules[ind], parameters) + } + } + return apiDetectorRules, nil +} + +func getParametersFromRule(tfRule map[string]interface{}) (map[string]interface{}, error) { + parameters := tfRule[parametersLabel].([]interface{}) + + switch len(parameters) { + case 0: + return nil, nil + case 1: + if parameters[0] != nil { + return parameters[0].(map[string]interface{}), nil + } else { + return nil, nil + } + default: + return nil, fmt.Errorf("expecting at most one parameter to be present") + } +} + +func setSloInput(sloTfResource Resource, sloApiObject *slo.SloObject) (*slo.SloObject, error) { + switch sloApiObject.Type { + case slo.RequestBased: + requestBasedInput, err := getRequestBasedApiInput(sloTfResource) + if err != nil { + return nil, err + } + sloApiObject.RequestBasedSlo = &slo.RequestBasedSlo{requestBasedInput} + default: + return nil, fmt.Errorf("unsupported SLO type: %s", sloApiObject.Type) + } + return sloApiObject, nil +} + +func getRequestBasedApiInput(sloTfResource Resource) (*slo.RequestBasedSloInput, error) { + inputs := sloTfResource.Get(inputLabel).([]interface{}) + + if len(inputs) != 1 { + return nil, fmt.Errorf("expecting exactly one input to be present") + } + + tfInput := inputs[0].(map[string]interface{}) + requestBasedInput := &slo.RequestBasedSloInput{ + ProgramText: tfInput[programTextLabel].(string), + GoodEventsLabel: tfInput[goodEventsLabel].(string), + TotalEventsLabel: tfInput[totalEventsLabel].(string), + } + return requestBasedInput, nil +} + +func sloAPIToTF(sloTfResource *schema.ResourceData, sloApiObject *slo.SloObject) error { + debugOutput, _ := json.Marshal(sloApiObject) + log.Printf("[DEBUG] Convert SLO to TF State: %s", string(debugOutput)) + + if errSet := sloTfResource.Set(nameLabel, sloApiObject.Name); errSet != nil { + return errSet + } + if errSet := sloTfResource.Set(descriptionLabel, sloApiObject.Description); errSet != nil { + return errSet + } + if errSet := sloTfResource.Set(typeLabel, sloApiObject.Type); errSet != nil { + return errSet + } + + tfSloInput, err := getTfSloInput(sloApiObject) + if err != nil { + return err + } + + if errSet := sloTfResource.Set(inputLabel, []map[string]interface{}{tfSloInput}); errSet != nil { + return errSet + } + + tfTargets, err := getTfTargets(sloApiObject) + if err != nil { + return err + } + + if errSet := sloTfResource.Set(targetLabel, tfTargets); errSet != nil { + return errSet + } + + return nil +} + +func getTfSloInput(sloApiObject *slo.SloObject) (map[string]interface{}, error) { + switch sloApiObject.Type { + case slo.RequestBased: + tfInput := getRequestBasedTerraformInput(sloApiObject.RequestBasedSlo.Inputs) + return tfInput, nil + default: + return nil, fmt.Errorf("Unsupported SLO type: " + sloApiObject.Type) + } +} + +func getRequestBasedTerraformInput(sloInput *slo.RequestBasedSloInput) map[string]interface{} { + tfInput := make(map[string]interface{}) + tfInput[programTextLabel] = sloInput.ProgramText + tfInput[goodEventsLabel] = sloInput.GoodEventsLabel + tfInput[totalEventsLabel] = sloInput.TotalEventsLabel + return tfInput +} + +func getTfTargets(sloApiObject *slo.SloObject) ([]map[string]interface{}, error) { + tfTargets := make([]map[string]interface{}, len(sloApiObject.Targets)) + for ind, apiTarget := range sloApiObject.Targets { + tfTarget := make(map[string]interface{}) + tfTarget[sloLabel] = apiTarget.Slo + tfTarget[typeLabel] = apiTarget.Type + + switch apiTarget.Type { + case slo.RollingWindowTarget: + tfTarget[compliancePeriodLabel] = apiTarget.RollingWindowSloTarget.CompliancePeriod + default: + return nil, fmt.Errorf("unsupported SLO target type: %s", apiTarget.Type) + } + + tfAlertRules, err := getTfAlertRules(apiTarget.SloAlertRules) + if err != nil { + return nil, err + } + tfTarget[alertRuleLabel] = tfAlertRules + tfTargets[ind] = tfTarget + } + return tfTargets, nil +} + +func getTfAlertRules(apiAlertRules []slo.SloAlertRule) (interface{}, error) { + tfAlertRules := make([]map[string]interface{}, len(apiAlertRules)) + + // Since the API can return alert rules in any order, we need to sort here to avoid TF wanting to update a resource because the order has changed. + sort.SliceStable(apiAlertRules, func(i, j int) bool { + return apiAlertRules[i].Type < apiAlertRules[j].Type + }) + + for ind, apiAlertRule := range apiAlertRules { + tfAlertRule := make(map[string]interface{}) + tfAlertRule[typeLabel] = apiAlertRule.Type + + switch apiAlertRule.Type { + case slo.BreachRule: + + tfDetectorRules, err := getTfDetectorRules[slo.BreachDetectorRule]( + apiAlertRule.BreachSloAlertRule.Rules, + func(apiRule slo.BreachDetectorRule) *detector.Rule { + return &apiRule.Rule + }, + func(apiRule slo.BreachDetectorRule) []map[string]interface{} { + if parameters := apiRule.Parameters; parameters != nil { + return []map[string]interface{}{{ + percentOfLastingLabel: parameters.PercentOfLasting, + fireLastingLabel: parameters.FireLasting, + }, + } + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + tfAlertRule[ruleLabel] = tfDetectorRules + + case slo.ErrorBudgetLeftRule: + tfDetectorRules, err := getTfDetectorRules[slo.ErrorBudgetLeftDetectorRule]( + apiAlertRule.ErrorBudgetLeftSloAlertRule.Rules, + func(apiRule slo.ErrorBudgetLeftDetectorRule) *detector.Rule { + return &apiRule.Rule + }, + func(apiRule slo.ErrorBudgetLeftDetectorRule) []map[string]interface{} { + if parameters := apiRule.Parameters; parameters != nil { + return []map[string]interface{}{{ + percentOfLastingLabel: parameters.PercentOfLasting, + fireLastingLabel: parameters.FireLasting, + percentErrorBudgetLeftLabel: parameters.PercentErrorBudgetLeft, + }, + } + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + tfAlertRule[ruleLabel] = tfDetectorRules + + case slo.BurnRateRule: + tfDetectorRules, err := getTfDetectorRules[slo.BurnRateDetectorRule]( + apiAlertRule.BurnRateSloAlertRule.Rules, + func(apiRule slo.BurnRateDetectorRule) *detector.Rule { + return &apiRule.Rule + }, + func(apiRule slo.BurnRateDetectorRule) []map[string]interface{} { + if parameters := apiRule.Parameters; parameters != nil { + return []map[string]interface{}{{ + shortWindow1Label: parameters.ShortWindow1, + longWindow1Label: parameters.LongWindow1, + shortWindow2Label: parameters.ShortWindow2, + longWindow2Label: parameters.LongWindow2, + burnRateThreshold1Label: parameters.BurnRateThreshold1, + burnRateThreshold2Label: parameters.BurnRateThreshold2, + }, + } + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + tfAlertRule[ruleLabel] = tfDetectorRules + default: + return nil, fmt.Errorf("unsupported SLO alert rule type: %s", apiAlertRule.Type) + + } + + tfAlertRules[ind] = tfAlertRule + } + + return tfAlertRules, nil +} + +type DetectorRuleProvider[Rule DetectorRuleType] func(rule Rule) (detectorRule *detector.Rule) + +type RuleParametersProvider[Rule DetectorRuleType] func(rule Rule) []map[string]interface{} + +func getTfDetectorRules[Rule DetectorRuleType](alertRules []*Rule, + detectorRuleProvider DetectorRuleProvider[Rule], + ruleParametersProvider RuleParametersProvider[Rule]) ([]map[string]interface{}, error) { + + tfDetectorRules := make([]map[string]interface{}, len(alertRules)) + + for ind, apiRule := range alertRules { + tfDetectorRule, err := getTfDetectorRule(detectorRuleProvider(*apiRule)) + delete(tfDetectorRule, "detect_label") // We don't expect detect_label. The user can send it - but we will ignore it - so we remove it from the TF schema here + + if err != nil { + return nil, err + } + + tfDetectorRule[parametersLabel] = ruleParametersProvider(*apiRule) + tfDetectorRules[ind] = tfDetectorRule + + } + return tfDetectorRules, nil +} diff --git a/signalfx/resource_signalfx_slo_test.go b/signalfx/resource_signalfx_slo_test.go new file mode 100644 index 00000000..172868ab --- /dev/null +++ b/signalfx/resource_signalfx_slo_test.go @@ -0,0 +1,313 @@ +package signalfx + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/signalfx/signalfx-go/slo" + "regexp" + "strconv" + "strings" + "testing" + "time" +) + +const ( + sloDescription = "SLO description" + sloProgramText = `G = data('spans.count', filter=filter('sf_error', 'false') and filter('sf_environment', 'lab0') and filter('sf_service', 'apm-indexer-api'))\nT = data('spans.count', filter=filter('sf_environment', 'lab0') and filter('sf_service', 'apm-indexer-api'))` + sloTarget = 98 + compliancePeriod = "30d" + alertNotification = "Email,foo-alerts@example.com" + fireLasting = "15m" + shortWindow = "15m" + + updatedSloDescription = "Updated SLO description" + updatedSloProgramText = `G = data('spans.count', filter=filter('sf_error', 'false') and filter('sf_service', 'apm-indexer-api'))\nT = data('spans.count', filter=filter('sf_service', 'apm-indexer-api'))` + updatedSloTarget = 99 + updatedAlertNotification = "Email,new-alerts@example.com" + updateFireLasting = "1m" + percentErrorBudgetLeft = 12 +) + +var sloName = "test slo " + time.Now().String() // we are checking the uniqueness of the slo name, so we need some randomness here + +var newSloConfig = fmt.Sprintf(` +resource "signalfx_slo" "test_slo" { + name = "%s" + type = "RequestBased" + description = "%s" + input { + program_text = "%s" + good_events_label = "G" + total_events_label = "T" + } + + target { + type="RollingWindow" + slo=%s + compliance_period = "%s" + + alert_rule { + type = "BREACH" + + rule { + severity = "Critical" + notifications = ["%s"] + parameters { + fire_lasting = "%s" + } + } + } + + alert_rule { + type = "BURN_RATE" + + rule { + severity = "Warning" + parameters { + short_window_1 = "%s" + } + } + } + } +} +`, sloName, sloDescription, sloProgramText, strconv.Itoa(sloTarget), compliancePeriod, alertNotification, fireLasting, shortWindow) + +var updateSloConfig = fmt.Sprintf(` +resource "signalfx_slo" "test_slo" { + name = "%s" + type = "RequestBased" + description = "%s" + input { + program_text = "%s" + good_events_label = "G" + total_events_label = "T" + } + + target { + type="RollingWindow" + slo=%s + compliance_period = "%s" + + alert_rule { + type = "BREACH" + + rule { + severity = "Critical" + notifications = ["%s"] + parameters { + fire_lasting = "%s" + } + } + } + + alert_rule { + type = "ERROR_BUDGET_LEFT" + + rule { + severity = "Warning" + parameters { + percent_error_budget_left = %s + } + } + } + } +} +`, sloName, updatedSloDescription, updatedSloProgramText, strconv.Itoa(updatedSloTarget), compliancePeriod, updatedAlertNotification, updateFireLasting, strconv.Itoa(percentErrorBudgetLeft)) + +var invalidSloProgramTextInput = fmt.Sprintf(` +resource "signalfx_slo" "test_slo" { + name = "%s" + type = "RequestBased" + input { + program_text = "G = da('spans.count', filter=filter('sf_error', 'false') and filter('sf_environment', 'lab0') and filter('sf_service', 'apm-indexer-api'))\n" + good_events_label = "G" + total_events_label = "T" + } + + target { + type="RollingWindow" + slo=97 + compliance_period = "30d" + + alert_rule { + type = "BREACH" + + rule { + severity = "Critical" + } + } + } +} +`, sloName) + +var invalidSloTargetValue = fmt.Sprintf(` +resource "signalfx_slo" "test_slo" { + name = "%s" + type = "RequestBased" + input { + program_text = "G = data('spans.count', filter=filter('sf_error', 'false') and filter('sf_environment', 'lab0') and filter('sf_service', 'apm-indexer-api'))\nT = data('spans.count', filter=filter('sf_environment', 'lab0') and filter('sf_service', 'apm-indexer-api'))" + good_events_label = "G" + total_events_label = "T" + } + + target { + type="RollingWindow" + slo=101 + compliance_period = "30d" + + alert_rule { + type = "BREACH" + + rule { + severity = "Warning" + } + } + } +} +`, sloName) + +func TestAccCreateUpdateSlo(t *testing.T) { + const sloResourceName = "signalfx_slo.test_slo" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccSloDestroy, + Steps: []resource.TestStep{ + // Check invalid slo programText input + { + Config: invalidSloProgramTextInput, + PlanOnly: true, + ExpectError: regexp.MustCompile("Invalid programText"), + }, + // Check invalid SLO target + { + Config: invalidSloTargetValue, + PlanOnly: true, + ExpectError: regexp.MustCompile("expected target.0.slo to be in the range \\(0.000000 - 100.000000\\), got 101.000000"), + }, + // Validate plan + { + Config: newSloConfig, + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + // Create It + { + Config: newSloConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckSloResourceExists, + resource.TestCheckResourceAttr(sloResourceName, "name", sloName), + resource.TestCheckResourceAttr(sloResourceName, "type", "RequestBased"), + resource.TestCheckResourceAttr(sloResourceName, "description", sloDescription), + + resource.TestCheckResourceAttr(sloResourceName, "input.#", "1"), + resource.TestCheckResourceAttr(sloResourceName, "input.0.program_text", strings.Replace(sloProgramText, "\\n", "\n", -1)), + resource.TestCheckResourceAttr(sloResourceName, "input.0.good_events_label", "G"), + resource.TestCheckResourceAttr(sloResourceName, "input.0.total_events_label", "T"), + + resource.TestCheckResourceAttr(sloResourceName, "target.#", "1"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.type", "RollingWindow"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.slo", strconv.Itoa(sloTarget)), + resource.TestCheckResourceAttr(sloResourceName, "target.0.compliance_period", compliancePeriod), + + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.#", "2"), + + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.0.type", "BREACH"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.0.rule.0.severity", "Critical"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.0.rule.0.notifications.#", "1"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.0.rule.0.notifications.0", alertNotification), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.0.rule.0.parameters.#", "1"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.0.rule.0.parameters.0.fire_lasting", fireLasting), + + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.1.type", "BURN_RATE"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.1.rule.0.severity", "Warning"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.1.rule.0.notifications.#", "0"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.1.rule.0.parameters.#", "1"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.1.rule.0.parameters.0.short_window_1", shortWindow), + + // Force sleep before refresh at the end of test execution + waitBeforeTestStepPlanRefresh, + ), + }, + { + ResourceName: sloResourceName, + ImportState: true, + ImportStateIdFunc: testAccStateIdFunc(sloResourceName), + ImportStateVerify: true, + }, + // Update It + { + Config: updateSloConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckSloResourceExists, + resource.TestCheckResourceAttr(sloResourceName, "name", sloName), + resource.TestCheckResourceAttr(sloResourceName, "type", "RequestBased"), + resource.TestCheckResourceAttr(sloResourceName, "description", updatedSloDescription), + + resource.TestCheckResourceAttr(sloResourceName, "input.#", "1"), + resource.TestCheckResourceAttr(sloResourceName, "input.0.program_text", strings.Replace(updatedSloProgramText, "\\n", "\n", -1)), + resource.TestCheckResourceAttr(sloResourceName, "input.0.good_events_label", "G"), + resource.TestCheckResourceAttr(sloResourceName, "input.0.total_events_label", "T"), + + resource.TestCheckResourceAttr(sloResourceName, "target.#", "1"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.type", "RollingWindow"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.slo", strconv.Itoa(updatedSloTarget)), + resource.TestCheckResourceAttr(sloResourceName, "target.0.compliance_period", compliancePeriod), + + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.#", "2"), + + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.0.type", "BREACH"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.0.rule.0.severity", "Critical"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.0.rule.0.notifications.#", "1"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.0.rule.0.notifications.0", updatedAlertNotification), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.0.rule.0.parameters.#", "1"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.0.rule.0.parameters.0.fire_lasting", updateFireLasting), + + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.1.type", "ERROR_BUDGET_LEFT"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.1.rule.0.severity", "Warning"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.1.rule.0.notifications.#", "0"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.1.rule.0.parameters.#", "1"), + resource.TestCheckResourceAttr(sloResourceName, "target.0.alert_rule.1.rule.0.parameters.0.percent_error_budget_left", strconv.Itoa(percentErrorBudgetLeft)), + ), + }, + }, + }) +} + +func testAccCheckSloResourceExists(s *terraform.State) error { + sloId, sloObject, err := getSloObject(s) + + if err != nil || sloObject.Id != sloId { + return fmt.Errorf("Error finding SLO %s: %s", sloId, err) + } + + return nil +} + +func testAccSloDestroy(s *terraform.State) error { + sloId, sloObject, _ := getSloObject(s) + + if sloObject != nil { + return fmt.Errorf("Found deleted SLO %s", sloId) + } + + return nil +} + +func getSloObject(s *terraform.State) (string, *slo.SloObject, error) { + client := newTestClient() + for _, rs := range s.RootModule().Resources { + sloId := rs.Primary.ID + + switch rs.Type { + case "signalfx_slo": + sloObject, err := client.GetSlo(context.TODO(), sloId) + return sloId, sloObject, err + default: + return sloId, nil, fmt.Errorf("Unexpected resource of type: %s", rs.Type) + } + } + return "", nil, nil +} diff --git a/website/docs/r/slo.html.markdown b/website/docs/r/slo.html.markdown new file mode 100644 index 00000000..20ffad5c --- /dev/null +++ b/website/docs/r/slo.html.markdown @@ -0,0 +1,167 @@ +--- +layout: "signalfx" +page_title: "Splunk Observability Cloud: signalfx_slo" +sidebar_current: "docs-signalfx-resource-slo" +description: |- + Allows Terraform to create and manage SLOs in Splunk Observability Cloud +--- + +# Resource: signalfx_detector + +Provides a Splunk Observability Cloud slo resource. This can be used to create and manage SLOs. + +To learn more about this feature take a look on [documentation for SLO](https://docs.splunk.com/observability/en/alerts-detectors-notifications/slo/slo-intro.html). + +## Example + +```tf +resource "signalfx_slo" "foo_service_slo" { + name = "foo service SLO" + type = "RequestBased" + description = "SLO monitoring for foo service" + + input { + program_text = "G = data('spans.count', filter=filter('sf_error', 'false') and filter('sf_service', 'foo-service'))\nT = data('spans.count', filter=filter('sf_service', 'foo-service'))" + good_events_label = "G" + total_events_label = "T" + } + + target { + type = "RollingWindow" + slo = 95 + compliance_period = "30d" + + alert_rule { + type = "BREACH" + + rule { + severity = "Warning" + notifications = ["Email,foo-alerts@bar.com"] + } + } + } +} + +provider "signalfx" {} + +``` + +## Notification format + +As Splunk Observability Cloud supports different notification mechanisms, use a comma-delimited string to provide inputs. If you want to specify multiple notifications, each must be a member in the list, like so: + +``` +notifications = ["Email,foo-alerts@example.com", "Slack,credentialId,channel"] +``` + +See [Splunk Observability Cloud Docs](https://dev.splunk.com/observability/reference/api/detectors/latest) for more information. + +Here are some example of how to configure each notification type: + +### Email + +``` +notifications = ["Email,foo-alerts@bar.com"] +``` + +### Jira + +Note that the `credentialId` is the Splunk-provided ID shown after setting up your Jira integration. See also `signalfx_jira_integration`. + +``` +notifications = ["Jira,credentialId"] +``` + +### OpsGenie + +Note that the `credentialId` is the Splunk-provided ID shown after setting up your Opsgenie integration. `Team` here is hardcoded as the `responderType` as that is the only acceptable type as per the API docs. + +``` +notifications = ["Opsgenie,credentialId,responderName,responderId,Team"] +``` + +### PagerDuty + +``` +notifications = ["PagerDuty,credentialId"] +``` + +### Slack + +Exclude the `#` on the channel name: + +``` +notifications = ["Slack,credentialId,channel"] +``` + +### Team + +Sends [notifications to a team](https://docs.signalfx.com/en/latest/managing/teams/team-notifications.html). + +``` +notifications = ["Team,teamId"] +``` + +### TeamEmail + +Sends an email to every member of a team. + +``` +notifications = ["TeamEmail,teamId"] +``` + +### Splunk On-Call (formerly VictorOps) + +``` +notifications = ["VictorOps,credentialId,routingKey"] +``` + +### Webhooks + +You need to include all the commas even if you only use a credential id. + +You can either configure a Webhook to use an existing integration's credential id: +``` +notifications = ["Webhook,credentialId,,"] +``` + +Or configure one inline: + +``` +notifications = ["Webhook,,secret,url"] +``` + +## Arguments + +* `name` - (Required) Name of the SLO. Each SLO name must be unique within an organization. +* `description` - (Optional) Description of the SLO. +* `type` - (Required) Type of the SLO. Currently just: `"RequestBased"` is supported. +* `input` - (Required) Properties to configure an SLO object inputs + * `program_text` - (Required) SignalFlow program and arguments text strings that define the streams used as successful event count and total event count + * `good_events_label` - (Required) Label used in `"program_text"` that refers to the data block which contains the stream of successful events + * `total_events_label` - (Required) Label used in `"program_text"` that refers to the data block which contains the stream of total events +* `target` - (Required) Define target value of the service level indicator in the appropriate time period. + * `type` - (Required) SLO target type can be the following type: `"RollingWindow"` + * `compliance_period` - (Required for `"RollingWindow"` type) Compliance period of this SLO. This value must be within the range of 1d (1 days) to 30d (30 days), inclusive. + * `slo` - (Required) Target value in the form of a percentage + * `alert_rule` - (Required) List of alert rules you want to set for this SLO target. An SLO alert rule of type BREACH is always required. + * `type` - (Required) SLO alert rule can be one of the following types: BREACH, ERROR_BUDGET_LEFT, BURN_RATE. Within an SLO object, you can only specify one SLO alert_rule per type. For example, you can't specify two alert_rule of type BREACH. See [SLO alerts](https://docs.splunk.com/observability/en/alerts-detectors-notifications/slo/burn-rate-alerts.html) for more info. + * `rule` - (Required) Set of rules used for alerting. + * `severity` - (Required) The severity of the rule, must be one of: `"Critical"`, `"Major"`, `"Minor"`, `"Warning"`, `"Info"`. + * `description` - (Optional) Description for the rule. Displays as the alert condition in the Alert Rules tab of the detector editor in the web UI. + * `disabled` - (Optional) When true, notifications and events will not be generated for the detect label. `false` by default. + * `notifications` - (Optional) List of strings specifying where notifications will be sent when an incident occurs. See [Create SLO](https://dev.splunk.com/observability/reference/api/slo/latest#endpoint-create-new-slo) for more info. + * `parameterized_body` - (Optional) Custom notification message body when an alert is triggered. See [Alert message](https://docs.splunk.com/observability/en/alerts-detectors-notifications/create-detectors-for-alerts.html#alert-messages) for more info. + * `parameterized_subject` - (Optional) Custom notification message subject when an alert is triggered. See [Alert message](https://docs.splunk.com/observability/en/alerts-detectors-notifications/create-detectors-for-alerts.html#alert-messages) for more info. + * `runbook_url` - (Optional) URL of page to consult when an alert is triggered. This can be used with custom notification messages. + * `tip` - (Optional) Plain text suggested first course of action, such as a command line to execute. This can be used with custom notification messages. + * `parameters` - (Optional) Parameters for the SLO alert rule. Each SLO alert rule type accepts different parameters. If not specified, default parameters are used. + * `fire_lasting` - (Optional) Duration that indicates how long the alert condition is met before the alert is triggered. The value must be positive and smaller than the compliance period of the SLO target. Note: `"BREACH"` and `"ERROR_BUDGET_LEFT"` alert rules use the fireLasting parameter. Default: `"5m"` + * `percent_of_lasting` - (Optional) Percentage of the `"fire_lasting"` duration that the alert condition is met before the alert is triggered. Note: `"BREACH"` and `"ERROR_BUDGET_LEFT"` alert rules use the `"percent_of_lasting"` parameter. Default: `100` + * `percent_error_budget_left` - (Optional) Error budget must be equal to or smaller than this percentage for the alert to be triggered. Note: `"ERROR_BUDGET_LEFT"` alert rules use the `"percent_error_budget_left"` parameter. Default: `100` + * `short_window_1` - (Optional) Short window 1 used in burn rate alert calculation. This value must be longer than 1/30 of `"long_window_1"`. Note: `"BURN_RATE"` alert rules use the `"short_window_1"` parameter. See [SLO alerts](https://docs.splunk.com/observability/en/alerts-detectors-notifications/slo/burn-rate-alerts.html) for more info. + * `short_window_2` - (Optional) Short window 2 used in burn rate alert calculation. This value must be longer than 1/30 of `"long_window_2"`. Note: `"BURN_RATE"` alert rules use the `"short_window_2"` parameter. See [SLO alerts](https://docs.splunk.com/observability/en/alerts-detectors-notifications/slo/burn-rate-alerts.html) for more info. + * `long_window_1` - (Optional) Long window 1 used in burn rate alert calculation. This value must be longer than `"short_window_1"` and shorter than 90 days. Note: `"BURN_RATE"` alert rules use the `"long_window_1"` parameter. See [SLO alerts](https://docs.splunk.com/observability/en/alerts-detectors-notifications/slo/burn-rate-alerts.html) for more info. + * `long_window_2` - (Optional) Long window 2 used in burn rate alert calculation. This value must be longer than `"short_window_2"` and shorter than 90 days. Note: `"BURN_RATE"` alert rules use the `"long_window_2"` parameter. See [SLO alerts](https://docs.splunk.com/observability/en/alerts-detectors-notifications/slo/burn-rate-alerts.html) for more info. + * `burn_rate_threshold_1` - (Optional) Burn rate threshold 1 used in burn rate alert calculation. This value must be between 0 and 100/(100-SLO target). Note: `"BURN_RATE"` alert rules use the `"burn_rate_threshold_1"` parameter. See [SLO alerts](https://docs.splunk.com/observability/en/alerts-detectors-notifications/slo/burn-rate-alerts.html) for more info. + * `burn_rate_threshold_2` - (Optional) Burn rate threshold 2 used in burn rate alert calculation. This value must be between 0 and 100/(100-SLO target). Note: `"BURN_RATE"` alert rules use the `"burn_rate_threshold_2"` parameter. See [SLO alerts](https://docs.splunk.com/observability/en/alerts-detectors-notifications/slo/burn-rate-alerts.html) for more info.