diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml new file mode 100644 index 000000000..25ff2dd12 --- /dev/null +++ b/.buildkite/pipeline.yml @@ -0,0 +1,15 @@ +env: + FORCE_COLOR: 1 +steps: + - label: ":golang: ci" + command: + - "go get -v -t -d ./..." + - "go build" + - "go test ./..." + plugins: + docker#v1.4.0: + image: "canvadev/ci-docker-node-yarn-chrome-xvfb:2018-09-07" + workdir: /app + agents: + - queue=ci-docker + timeout_in_minutes: 5 diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 000000000..cd41905c5 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,28 @@ +name: Go +on: [push] +jobs: + + build: + name: Build + runs-on: ubuntu-latest + steps: + + - name: Set up Go 1.13 + uses: actions/setup-go@v1 + with: + go-version: 1.13 + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Get dependencies + run: | + go get -v -t -d ./... + if [ -f Gopkg.toml ]; then + curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh + dep ensure + fi + + - name: Build + run: go build -v . diff --git a/README.md b/README.md index 76a36593e..cfef34f81 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,53 @@ The following attributes are exported: * `id` - The ID of the created plugin. +#### `sentry_rule` + +##### Example Usage + +``` +# Create a plugin +resource "sentry_rule" "default" { + organization = "my-organization" + project = "web-app" + action_match = "any" + frequency = 30 + environment = "production" + + actions = [{ + id = "sentry.rules.actions.notify_event.NotifyEventAction" + }] + + conditions = [{ + id = "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition" + }] +} +``` + +##### Argument Reference + +The following arguments are supported: + +* `organization` - (Required) The slug of the organization the plugin should be enabled for. +* `project` - (Required) The slug of the project the plugin should be enabled for. +* `action_match` - (Optional) Use `all` to trigger alerting when all conditions are met, and `any` when at least a condition is met. Defaults to `any`. +* `frequency` - (Optional) Perform actions at most once every `X` minutes for this issue. Defaults to `30`. +* `environment` - (Optional) Environment name +* `actions` - (Required) List of actions +* `conditions` - (Required) List of conditions + +##### Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the created rule. +* `name` - The name of the created rule. +* `actions` - The rule's actions. +* `conditions` - The rule's conditions. +* `frequency` - The rule's frequency. +* `environment` - The rule's environment. + + ### Data Source Configuration #### `sentry_key` diff --git a/go.mod b/go.mod index d9560949c..55b308ae3 100644 --- a/go.mod +++ b/go.mod @@ -1,33 +1,38 @@ -module github.com/jianyuan/terraform-provider-sentry +module github.com/canva/terraform-provider-sentry require ( - cloud.google.com/go/storage v1.0.0 // indirect - github.com/aws/aws-sdk-go v1.25.0 // indirect + cloud.google.com/go v0.50.0 // indirect + cloud.google.com/go/storage v1.4.0 // indirect + github.com/aws/aws-sdk-go v1.26.5 // indirect + github.com/dghubble/sling v1.3.0 github.com/go-test/deep v1.0.4 // indirect - github.com/google/go-cmp v0.3.1 // indirect + github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 // indirect + github.com/hashicorp/go-hclog v0.10.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/hashicorp/hcl2 v0.0.0-20190909202536-66c59f909e25 // indirect - github.com/hashicorp/hil v0.0.0-20190212132231-97b3a9cdfa93 // indirect - github.com/hashicorp/terraform-plugin-sdk v1.0.0 + github.com/hashicorp/terraform v0.12.19 + github.com/hashicorp/terraform-svchost v0.0.0-20191119180714-d2e4933b9136 // indirect github.com/hashicorp/yamux v0.0.0-20190923154419-df201c70410d // indirect - github.com/jianyuan/go-sentry v1.2.0 - github.com/mattn/go-colorable v0.1.2 // indirect - github.com/mattn/go-isatty v0.0.9 // indirect - github.com/stretchr/testify v1.4.0 // indirect + github.com/jstemmer/go-junit-report v0.9.1 // indirect + github.com/mattn/go-isatty v0.0.11 // indirect + github.com/mitchellh/mapstructure v1.1.2 + github.com/posener/complete v1.2.3 // indirect + github.com/stretchr/testify v1.4.0 github.com/ulikunitz/xz v0.5.6 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect - go.opencensus.io v0.22.1 // indirect - golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad // indirect - golang.org/x/exp v0.0.0-20190925190815-26a69ce95baf // indirect - golang.org/x/net v0.0.0-20190926025831-c00fd9afed17 // indirect - golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect - golang.org/x/sys v0.0.0-20190927073244-c990c680b611 // indirect - golang.org/x/tools v0.0.0-20190927052746-69890759d905 // indirect - google.golang.org/api v0.10.0 // indirect - google.golang.org/appengine v1.6.4 // indirect - google.golang.org/genproto v0.0.0-20190926190326-7ee9db18f195 // indirect - google.golang.org/grpc v1.24.0 // indirect + github.com/zclconf/go-cty v1.2.0 // indirect + go.opencensus.io v0.22.2 // indirect + golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect + golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect + golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 // indirect + golang.org/x/sys v0.0.0-20191218084908-4a24b4065292 // indirect + golang.org/x/tools v0.0.0-20191218225520-84f0c7cf60ea // indirect + google.golang.org/api v0.15.0 // indirect + google.golang.org/appengine v1.6.5 // indirect + google.golang.org/genproto v0.0.0-20191216205247-b31c10ee225f // indirect + google.golang.org/grpc v1.26.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999 // indirect + +go 1.13 diff --git a/go.sum b/go.sum index 6776c4e99..a537553fd 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxK cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3 h1:AVXDdKsrtX33oR9fbCMu/+c1o8Ofjq6Ku/MInaLVg5Y= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0 h1:0E3eE8MX426vUOs7aHfI7aN1BrIzzzf4ccKCSfSjGmc= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go/bigquery v1.0.1 h1:hL+ycaJpVE9M7nLoiXb/Pn10ENE2u+oddxbD8uu0ZVU= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/datastore v1.0.0 h1:Kt+gOPPp2LEPWp8CSfxhsM8ik9CcyE/gYu+0r+RnZvM= @@ -15,51 +17,120 @@ cloud.google.com/go/pubsub v1.0.1 h1:W9tAK3E57P75u0XLLR82LZyw8VpAnhmyTOxW9qzmyj8 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/storage v1.0.0 h1:VV2nUM3wwLLGh9lSABFgZMjInyUbJeaRSE64WuAIQ+4= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.4.0 h1:KDdqY5VTXBTqpSbctVTt0mVvfanP6JZzNzLE0qNY100= +cloud.google.com/go/storage v1.4.0/go.mod h1:ZusYJWlOshgSBGbt6K3GnB3MT3H1xs2id9+TCl4fDBA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v36.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.9.2/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.8.1-0.20191028180845-3492b2aff503/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= +github.com/Azure/go-autorest/autorest/azure/cli v0.2.0/go.mod h1:WWTbGPvkAg3I4ms2j2s+Zr5xCGwGqTQh+6M2ZqOczkE= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA= +github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= +github.com/Unknwon/com v0.0.0-20151008135407-28b053d5a292/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no= +github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/agl/ed25519 v0.0.0-20150830182803-278e1ec8e8a6/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190329064014-6e358769c32a/go.mod h1:T9M45xf79ahXVelWoOBmH0y4aC1t5kXO5BxwyakgIGA= +github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190103054945-8205d1f41e70/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/aliyun/aliyun-tablestore-go-sdk v4.1.2+incompatible/go.mod h1:LDQHRZylxvcg8H7wBIDfvO5g/cy4/sz1iucBlc2l3Jw= +github.com/antchfx/xpath v0.0.0-20190129040759-c8489ed3251e/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= +github.com/antchfx/xquery v0.0.0-20180515051857-ad5b8c7a47b0/go.mod h1:LzD22aAzDP8/dyiCKFp31He4m2GPjl0AFyzDtZzUu9M= github.com/apparentlymart/go-cidr v1.0.1 h1:NmIwLZ/KdsjIUlhf+/Np40atNXm/+lZ5txfTJ/SpF+U= github.com/apparentlymart/go-cidr v1.0.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhiM5J5RFxEaFvMZVEAM1KvT1YzbEOwB2EAGjA= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= +github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0 h1:MzVXffFUye+ZcSR6opIgz9Co7WcDx6ZcY+RjfFHoA0I= +github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= +github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= -github.com/aws/aws-sdk-go v1.19.39/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.25.0 h1:MyXUdCesJLBvSSKYcaKeeEwxNUwUpG6/uqVYeH/Zzfo= -github.com/aws/aws-sdk-go v1.25.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.25.3/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.26.5 h1:mm59kTX51f/tU8Bas3zfwdJirxVdeQ+FRTWMoTdYN0M= +github.com/aws/aws-sdk-go v1.26.5/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 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/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= +github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e/go.mod h1:N+BjUcTjSxc2mtRGSCPsat1kze3CUtvJN3/jTXlp29k= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.0/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 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= github.com/dghubble/sling v1.1.0 h1:DLu20Bq2qsB9cI5Hldaxj+TMPEaPpPE8IR2kvD22Atg= github.com/dghubble/sling v1.1.0/go.mod h1:ZcPRuLm0qrcULW2gOrjXrAWgf76sahqSyxXyVOvkunE= +github.com/dghubble/sling v1.3.0 h1:pZHjCJq4zJvc6qVQ5wN1jo5oNZlNE0+8T/h0XeXBUKU= +github.com/dghubble/sling v1.3.0/go.mod h1:XXShWaBWKzNLhu2OxikSNFrlsvowtz4kyRuXUG7oQKY= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/dnaeon/go-vcr v0.0.0-20180920040454-5637cf3d8a31/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/dylanmei/iso8601 v0.1.0/go.mod h1:w9KhXSgIyROl1DefbMYIE7UVSIvELTbMrCfx+QkYnoQ= +github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1/go.mod h1:lcy9/2gH1jn/VCLouHA6tOEwLoNVd4GW6zhuKLmHC2Y= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -71,6 +142,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -92,25 +164,51 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gophercloud/gophercloud v0.0.0-20190208042652-bc37892e1968/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= +github.com/gophercloud/utils v0.0.0-20190128072930-fbb6ab446f01/go.mod h1:wjDF8z83zTeg5eMLml5EBSlAhbF7G8DobyI1YsMuyzw= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/aws-sdk-go-base v0.4.0/go.mod h1:eRhlz3c4nhqxFZJAahJEFL7gh6Jyj5rQmQc7F9eHFyQ= +github.com/hashicorp/consul v0.0.0-20171026175957-610f3c86a089/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI= github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-azure-helpers v0.10.0/go.mod h1:YuAtHxm2v74s+IjQwUG88dHBJPd5jL+cXr5BGVzSKhE= +github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6KdvN3Gig= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-getter v1.4.0 h1:ENHNi8494porjD0ZhIrjlAHnveSFhY7hvOJrV/fsKkw= github.com/hashicorp/go-getter v1.4.0/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY= +github.com/hashicorp/go-getter v1.4.2-0.20200106182914-9813cbd4eb02 h1:l1KB3bHVdvegcIf5upQ5mjcHjs2qsWnKh4Yr9xgIuu8= +github.com/hashicorp/go-getter v1.4.2-0.20200106182914-9813cbd4eb02/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY= github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-hclog v0.0.0-20181001195459-61d530d6c27f/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.10.1 h1:uyt/l0dWjJ879yiAu+T7FG3/6QX+zwm4bQ8P7XsYt3o= +github.com/hashicorp/go-hclog v0.10.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v0.0.0-20180129170900-7f3cd4390caa/go.mod h1:6ij3Z20p+OhOkCSrA0gImAWoHYQRGbnlcuk6XYTiaRw= +github.com/hashicorp/go-msgpack v0.5.4/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-plugin v1.0.1-0.20190610192547-a1bc61569a26 h1:hRho44SAoNu1CBtn5r8Q9J3rCs4ZverWZ4R+UeeNuWM= +github.com/hashicorp/go-plugin v1.0.1-0.20190610192547-a1bc61569a26/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE= github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= +github.com/hashicorp/go-retryablehttp v0.5.2/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-slug v0.4.1/go.mod h1:I5tq5Lv0E2xcNXNkmx7BSfzi1PsJ2cNjs3cC3LwyhK8= +github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-tfe v0.3.27/go.mod h1:DVPSW2ogH+M9W1/i50ASgMht8cHP7NxxK0nrY9aFikQ= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0= @@ -122,35 +220,55 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/hcl/v2 v2.0.0 h1:efQznTz+ydmQXq3BOnRa3AXzvCeTq1P4dKj/z5GLlY8= +github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90= +github.com/hashicorp/hcl/v2 v2.2.0 h1:ZQ1eNLggMfTyFBhV8swxT081mlaRjr4EG85NEjjLB84= +github.com/hashicorp/hcl/v2 v2.2.0/go.mod h1:MD4q2LOluJ5pRwTVkCXmJOY7ODWDXVXGVB8LY0t7wig= +github.com/hashicorp/hcl2 v0.0.0-20190821123243-0c888d1241f6 h1:JImQpEeUQ+0DPFMaWzLA0GdUNPaUlCXLpfiqkSZBUfc= github.com/hashicorp/hcl2 v0.0.0-20190821123243-0c888d1241f6/go.mod h1:Cxv+IJLuBiEhQ7pBYGEuORa0nr4U994pE8mYLuFd7v0= -github.com/hashicorp/hcl2 v0.0.0-20190909202536-66c59f909e25 h1:hP3bdZpTEdjGFXQZyeeRsYH1oJgOBGx5sMLYPqZuqOU= -github.com/hashicorp/hcl2 v0.0.0-20190909202536-66c59f909e25/go.mod h1:Cxv+IJLuBiEhQ7pBYGEuORa0nr4U994pE8mYLuFd7v0= github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590 h1:2yzhWGdgQUWZUCNK+AoO35V+HTsgEmcM4J9IkArh7PI= github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590/go.mod h1:n2TSygSNwsLJ76m8qFXTSc7beTb+auJxYdqrnoqwZWE= -github.com/hashicorp/hil v0.0.0-20190212132231-97b3a9cdfa93 h1:T1Q6ag9tCwun16AW+XK3tAql24P4uTGUMIn1/92WsQQ= -github.com/hashicorp/hil v0.0.0-20190212132231-97b3a9cdfa93/go.mod h1:n2TSygSNwsLJ76m8qFXTSc7beTb+auJxYdqrnoqwZWE= 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/memberlist v0.1.0/go.mod h1:ncdBp14cuox2iFOq3kDiquKU6fqsTBc3W6JvZwjxxsE= +github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb/go.mod h1:h/Ru6tmZazX7WO/GDmwdpS975F019L4t5ng5IgwbNrE= +github.com/hashicorp/terraform v0.12.19 h1:S8pFSml4/aS3XMeCj8alrlbNDGyd25RwoD9V7oUDcnM= +github.com/hashicorp/terraform v0.12.19/go.mod h1:ocggZME4cKSKxRSSJ2r9Vza2me7V1MRF2wnElFJgO6w= github.com/hashicorp/terraform-config-inspect v0.0.0-20190821133035-82a99dc22ef4 h1:fTkL0YwjohGyN7AqsDhz6bwcGBpT+xBqi3Qhpw58Juw= github.com/hashicorp/terraform-config-inspect v0.0.0-20190821133035-82a99dc22ef4/go.mod h1:JDmizlhaP5P0rYTTZB0reDMefAiJyfWPEtugV4in1oI= -github.com/hashicorp/terraform-plugin-sdk v1.0.0 h1:3AjuuV1LJKs1NlG+heUgqWN6/QCSx2kDhyS6K7F0fTw= -github.com/hashicorp/terraform-plugin-sdk v1.0.0/go.mod h1:NuwtLpEpPsFaKJPJNGtMcn9vlhe6Ofe+Y6NqXhJgV2M= +github.com/hashicorp/terraform-config-inspect v0.0.0-20191115094559-17f92b0546e8 h1:+RyjwU+Gnd/aTJBPZVDNm903eXVjjqhbaR4Ypx3xYyY= +github.com/hashicorp/terraform-config-inspect v0.0.0-20191115094559-17f92b0546e8/go.mod h1:p+ivJws3dpqbp1iP84+npOyAmTTOLMgCzrXd3GSdn/A= +github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596 h1:hjyO2JsNZUKT1ym+FAdlBEkGPevazYsmVgIMw7dVELg= +github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg= +github.com/hashicorp/terraform-svchost v0.0.0-20191119180714-d2e4933b9136 h1:81Dg7SK6Q5vzqFItO8e1iIF2Nj8bLXV23NXjEgbev/s= +github.com/hashicorp/terraform-svchost v0.0.0-20191119180714-d2e4933b9136/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg= +github.com/hashicorp/vault v0.10.4/go.mod h1:KfSyffbKxoVyspOdlaGVjIuwLobi07qD1bAbosPMpP0= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20190923154419-df201c70410d h1:W+SIwDdl3+jXWeidYySAgzytE3piq6GumXeBjFBG67c= github.com/hashicorp/yamux v0.0.0-20190923154419-df201c70410d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jianyuan/go-sentry v1.2.0 h1:J+td84GIYoZovZYV+0B+9YuAJ2dR4SMAsky3kXZU580= -github.com/jianyuan/go-sentry v1.2.0/go.mod h1:IlgHIblLdlWCnqpIxFV4bnLb4fgg9xFfDd41p6kRc/Y= +github.com/jianyuan/go-sentry v1.2.1-0.20191201111848-212d47039109 h1:vNC97hHywvxBBDosxyYMsA2/5wW3lZ2n9NVTXSkF+Nw= +github.com/jianyuan/go-sentry v1.2.1-0.20191201111848-212d47039109/go.mod h1:IlgHIblLdlWCnqpIxFV4bnLb4fgg9xFfDd41p6kRc/Y= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/joyent/triton-go v0.0.0-20180313100802-d8f9c0314926/go.mod h1:U+RSyWxWd04xTqnuOQxnai7XGS2PrPY2cfGoDKtMHjA= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/keybase/go-crypto v0.0.0-20161004153544-93f5b35093ba/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -160,18 +278,28 @@ github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3v github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82/go.mod h1:y54tfGmO3NKssKveTEFFzH8C/akrSOy/iW9qEAUDV84= +github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= +github.com/masterzen/winrm v0.0.0-20190223112901-5e5c9a7fe54b/go.mod h1:wr1VqkwW0AB5JS0QLy5GpVMS9E3VtRoSYXUYyVk46KY= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-shellwords v1.0.4/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= @@ -182,6 +310,7 @@ github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnG github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-linereader v0.0.0-20190213213312-1b945b3263eb/go.mod h1:OaY7UOoTkkrX3wRwjpYRKafIkkyeD0UtweSHAWWiqQM= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= @@ -189,67 +318,120 @@ github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzC github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9dGS02Q3Y= +github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA= +github.com/mitchellh/prefixedio v0.0.0-20190213213902-5733675afd51/go.mod h1:kB1naBgV9ORnkiTVeyJOI1DavaJkG4oNIq0Af6ZVKUo= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/packer-community/winrmcp v0.0.0-20180102160824-81144009af58/go.mod h1:f6Izs6JvFTdnRbziASagjZ2vmf55NSIkC/weStxCHqk= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.1 h1:LrvDIY//XNo65Lq84G/akBuMGlawHvGBABv8f/ZN6DI= github.com/posener/complete v1.2.1/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M= +github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d/go.mod h1:BSTlc8jOjh0niykqEGVXOLXdi9o0r0kR8tCYiMvjFgw= +github.com/terraform-providers/terraform-provider-openstack v1.15.0/go.mod h1:2aQ6n/BtChAl1y2S60vebhyJyZXBsuAI5G4+lHrT1Ew= +github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v0.0.0-20180813092308-00b869d2f4a5/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok= github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack v4.0.1+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= +github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v1.1.0 h1:uJwc9HiBOCpoKIObTQaLR+tsEXx1HBHnOsOOpcdhZgw= github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= +github.com/zclconf/go-cty v1.1.1 h1:Shl2p9Dat0cqJfXu0DZa+cOTRPhXQjK8IYWD6GVfiqo= +github.com/zclconf/go-cty v1.1.1/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= +github.com/zclconf/go-cty v1.2.0 h1:sPHsy7ADcIZQP3vILvTjrh74ZA175TFP5vqiNK1UmlI= +github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= github.com/zclconf/go-cty-yaml v1.0.1 h1:up11wlgAaDvlAGENcFDnZgkn0qUJurso7k6EpURKNF8= github.com/zclconf/go-cty-yaml v1.0.1/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.1 h1:8dP3SGL7MPB94crU3bEPplMPe83FI4EouesJUeFHv50= -go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA= +go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad h1:5E5raQxcv+6CZ11RrBYQe5WRbUIWpScjh0kvHZkZIrQ= -golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20190925190815-26a69ce95baf h1:V3Pwu2Rbgu9M+TwZjWt4811gVksfbQRU7/rX1dUq8rI= -golang.org/x/exp v0.0.0-20190925190815-26a69ce95baf/go.mod h1:/XYaSSBNneWTe8VNa9AjSawUDfGmbpmIqqzQT33BSc0= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587 h1:5Uz0rkjCFu9BC9gCRN7EkwVvhNyQgGWb8KNJrPwBoHY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -259,32 +441,41 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac h1:8R1esu+8QioDxo4E4mX6bFztO+dMTM49DNAaWfO5OeY= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190502183928-7f726cade0ab h1:9RfW3ktsOZxgo9YNbBAjq1FWzc/igwEcUzZz8IXgSbk= golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/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= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190926025831-c00fd9afed17 h1:qPnAdmjNA41t3QBTx2mFGf/SD1IoslhYu7AmdsVzCcs= -golang.org/x/net v0.0.0-20190926025831-c00fd9afed17/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191009170851-d66e71096ffb/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -297,9 +488,13 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -309,9 +504,12 @@ golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA= -golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191218084908-4a24b4065292 h1:Y8q0zsdcgAd+JU8VUA8p8Qv2YhuY9zevDG2ORt5qBUI= +golang.org/x/sys v0.0.0-20191218084908-4a24b4065292/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -334,23 +532,29 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190925164712-ae58c0ff6b32/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190927052746-69890759d905 h1:OthNz2adqzWs1n8LWLzi4agXuDngecStsR2egYq+JnA= -golang.org/x/tools v0.0.0-20190927052746-69890759d905/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191218225520-84f0c7cf60ea h1:mtRJM/ln5qwEigajtnZtuARALEPOooGf5lwkM5a9tt4= +golang.org/x/tools v0.0.0-20191218225520-84f0c7cf60ea/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.10.0 h1:7tmAxx3oKE98VMZ+SBZzvYYWRQ9HODBxmC8mXUsraSQ= -google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.14.0 h1:uMf5uLi4eQMRrMKhCplNik4U4H8Z6C1br3zOtAa/aDE= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.4 h1:WiKh4+/eMB2HaY7QhCfW/R7MuRAoA8QMCSJA6jP5/fo= -google.golang.org/appengine v1.6.4/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -359,15 +563,18 @@ google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20190926190326-7ee9db18f195 h1:dWzgMaXfaHsnkRKZ1l3iJLDmTEB40JMl/dqRbJX4D/o= -google.golang.org/genproto v0.0.0-20190926190326-7ee9db18f195/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216205247-b31c10ee225f h1:0RYv5T9ZdroAqqfM2taEB0nJrArv0X1JpIdgUmY4xg8= +google.golang.org/genproto v0.0.0-20191216205247-b31c10ee225f/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s= -google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -376,8 +583,10 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go index 6900fb97e..d8e7eda63 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,8 @@ package main import ( - "github.com/hashicorp/terraform-plugin-sdk/plugin" - "github.com/jianyuan/terraform-provider-sentry/sentry" + "github.com/canva/terraform-provider-sentry/sentry" + "github.com/hashicorp/terraform/plugin" ) func main() { diff --git a/sentry/config.go b/sentry/config.go index 27953281e..d11013ce2 100644 --- a/sentry/config.go +++ b/sentry/config.go @@ -4,7 +4,7 @@ import ( "log" "net/url" - "github.com/jianyuan/go-sentry/sentry" + "github.com/canva/terraform-provider-sentry/sentryclient" ) // Config is the configuration structure used to instantiate the Sentry @@ -26,7 +26,7 @@ func (c *Config) Client() (interface{}, error) { } log.Printf("[INFO] Instantiating Sentry client...") - cl := sentry.NewClient(nil, baseURL, c.Token) + cl := sentryclient.NewClient(nil, baseURL, c.Token) return cl, nil } diff --git a/sentry/data_source_sentry_key.go b/sentry/data_source_sentry_key.go index 3e6fdd2c3..168815f09 100644 --- a/sentry/data_source_sentry_key.go +++ b/sentry/data_source_sentry_key.go @@ -4,8 +4,8 @@ import ( "fmt" "log" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/jianyuan/go-sentry/sentry" + "github.com/canva/terraform-provider-sentry/sentryclient" + "github.com/hashicorp/terraform/helper/schema" ) func dataSourceSentryKey() *schema.Resource { @@ -78,7 +78,7 @@ func dataSourceSentryKey() *schema.Resource { } func dataSourceSentryKeyRead(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) org := d.Get("organization").(string) project := d.Get("project").(string) @@ -114,7 +114,7 @@ func dataSourceSentryKeyRead(d *schema.ResourceData, meta interface{}) error { len(keys)) } -func sentryKeyAttributes(d *schema.ResourceData, key *sentry.ProjectKey) error { +func sentryKeyAttributes(d *schema.ResourceData, key *sentryclient.ProjectKey) error { d.SetId(key.ID) d.Set("name", key.Name) d.Set("public", key.Public) diff --git a/sentry/data_source_sentry_key_test.go b/sentry/data_source_sentry_key_test.go index 788e62fbd..c8e556463 100644 --- a/sentry/data_source_sentry_key_test.go +++ b/sentry/data_source_sentry_key_test.go @@ -5,8 +5,8 @@ import ( "regexp" "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" ) func TestAccSentryKeyDataSource_basic(t *testing.T) { diff --git a/sentry/import_sentry_key.go b/sentry/import_sentry_key.go index 3c9053057..a0d86acac 100644 --- a/sentry/import_sentry_key.go +++ b/sentry/import_sentry_key.go @@ -2,7 +2,7 @@ package sentry import ( "errors" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform/helper/schema" "log" "strings" ) diff --git a/sentry/import_sentry_project_plugin.go b/sentry/import_sentry_project_plugin.go index 4d6f3b59a..6164cf4d2 100644 --- a/sentry/import_sentry_project_plugin.go +++ b/sentry/import_sentry_project_plugin.go @@ -5,7 +5,7 @@ import ( "log" "strings" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform/helper/schema" ) func resourceSentryPluginImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { diff --git a/sentry/import_sentry_project_rule.go b/sentry/import_sentry_project_rule.go new file mode 100755 index 000000000..af3938993 --- /dev/null +++ b/sentry/import_sentry_project_rule.go @@ -0,0 +1,27 @@ +package sentry + +import ( + "errors" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceSentryRuleImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + addrID := d.Id() + + log.Printf("[DEBUG] Importing rule using ADDR ID %s", addrID) + + parts := strings.Split(addrID, "/") + + if len(parts) != 3 { + return nil, errors.New("Project import requires an ADDR ID of the following schema org-slug/project-slug/rule-id") + } + + d.Set("organization", parts[0]) + d.Set("project", parts[1]) + d.SetId(parts[2]) + + return []*schema.ResourceData{d}, nil +} diff --git a/sentry/import_sentry_team.go b/sentry/import_sentry_team.go index ee34d98a9..ea5c84992 100644 --- a/sentry/import_sentry_team.go +++ b/sentry/import_sentry_team.go @@ -5,7 +5,7 @@ import ( "log" "strings" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform/helper/schema" ) func resourceSentryTeamImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { diff --git a/sentry/provider.go b/sentry/provider.go index 477eb4483..ae11666db 100644 --- a/sentry/provider.go +++ b/sentry/provider.go @@ -1,8 +1,8 @@ package sentry import ( - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" ) // Provider returns a terraform.ResourceProvider. @@ -29,6 +29,7 @@ func Provider() terraform.ResourceProvider { "sentry_project": resourceSentryProject(), "sentry_key": resourceSentryKey(), "sentry_plugin": resourceSentryPlugin(), + "sentry_rule": resourceSentryRule(), }, DataSourcesMap: map[string]*schema.Resource{ diff --git a/sentry/provider_test.go b/sentry/provider_test.go index bb770f1f2..7f37315d8 100644 --- a/sentry/provider_test.go +++ b/sentry/provider_test.go @@ -4,8 +4,8 @@ import ( "os" "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" ) var testOrganization = os.Getenv("SENTRY_TEST_ORGANIZATION") diff --git a/sentry/resource_sentry_key.go b/sentry/resource_sentry_key.go index ef82a2fbe..94e008602 100644 --- a/sentry/resource_sentry_key.go +++ b/sentry/resource_sentry_key.go @@ -3,8 +3,8 @@ package sentry import ( "net/http" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/jianyuan/go-sentry/sentry" + "github.com/canva/terraform-provider-sentry/sentryclient" + "github.com/hashicorp/terraform/helper/schema" ) func resourceSentryKey() *schema.Resource { @@ -50,12 +50,16 @@ func resourceSentryKey() *schema.Resource { Computed: true, }, "rate_limit_window": { - Type: schema.TypeInt, - Computed: true, + Type: schema.TypeInt, + Description: "time window in second for rate_limit_count", + Optional: true, + Computed: true, }, "rate_limit_count": { - Type: schema.TypeInt, - Computed: true, + Type: schema.TypeInt, + Description: "rate limit for the key in rate_limit_window time", + Optional: true, + Computed: true, }, "dsn_secret": { Type: schema.TypeString, @@ -74,12 +78,16 @@ func resourceSentryKey() *schema.Resource { } func resourceSentryKeyCreate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) org := d.Get("organization").(string) project := d.Get("project").(string) - params := &sentry.CreateProjectKeyParams{ + params := &sentryclient.CreateProjectKeyParams{ Name: d.Get("name").(string), + RateLimit: &sentryclient.ProjectKeyRateLimit{ + Window: d.Get("rate_limit_window").(int), + Count: d.Get("rate_limit_count").(int), + }, } key, _, err := client.ProjectKeys.Create(org, project, params) @@ -88,11 +96,12 @@ func resourceSentryKeyCreate(d *schema.ResourceData, meta interface{}) error { } d.SetId(key.ID) + return resourceSentryKeyRead(d, meta) } func resourceSentryKeyRead(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) id := d.Id() org := d.Get("organization").(string) @@ -140,13 +149,17 @@ func resourceSentryKeyRead(d *schema.ResourceData, meta interface{}) error { } func resourceSentryKeyUpdate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) id := d.Id() org := d.Get("organization").(string) project := d.Get("project").(string) - params := &sentry.UpdateProjectKeyParams{ + params := &sentryclient.UpdateProjectKeyParams{ Name: d.Get("name").(string), + RateLimit: &sentryclient.ProjectKeyRateLimit{ + Window: d.Get("rate_limit_window").(int), + Count: d.Get("rate_limit_count").(int), + }, } key, _, err := client.ProjectKeys.Update(org, project, id, params) @@ -159,7 +172,7 @@ func resourceSentryKeyUpdate(d *schema.ResourceData, meta interface{}) error { } func resourceSentryKeyDelete(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) id := d.Id() org := d.Get("organization").(string) diff --git a/sentry/resource_sentry_key_test.go b/sentry/resource_sentry_key_test.go index ce03135d2..9a5467f8f 100644 --- a/sentry/resource_sentry_key_test.go +++ b/sentry/resource_sentry_key_test.go @@ -5,13 +5,15 @@ import ( "fmt" "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" - "github.com/jianyuan/go-sentry/sentry" + + "github.com/canva/terraform-provider-sentry/sentryclient" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + ) func TestAccSentryKey_basic(t *testing.T) { - var key sentry.ProjectKey + var key sentryclient.ProjectKey resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -46,8 +48,36 @@ func TestAccSentryKey_basic(t *testing.T) { }) } +func TestAccSentryKey_RateLimit(t *testing.T) { + var key sentryclient.ProjectKey + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckSentryKeyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSentryKeyConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckSentryKeyExists("sentry_key.test_key_rate_limit", &key), + resource.TestCheckResourceAttr("sentry_key.test_key_rate_limit", "rate_limit_window", "86400"), + resource.TestCheckResourceAttr("sentry_key.test_key_rate_limit", "rate_limit_count", "1000"), + ), + }, + { + Config: testAccSentryKeyUpdateConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckSentryKeyExists("sentry_key.test_key_rate_limit", &key), + resource.TestCheckResourceAttr("sentry_key.test_key_rate_limit", "rate_limit_window", "100"), + resource.TestCheckResourceAttr("sentry_key.test_key_rate_limit", "rate_limit_count", "100"), + ), + }, + }, + }) +} + func testAccCheckSentryKeyDestroy(s *terraform.State) error { - client := testAccProvider.Meta().(*sentry.Client) + client := testAccProvider.Meta().(*sentryclient.Client) for _, rs := range s.RootModule().Resources { if rs.Type != "sentry_key" { @@ -73,7 +103,7 @@ func testAccCheckSentryKeyDestroy(s *terraform.State) error { return nil } -func testAccCheckSentryKeyExists(n string, projectKey *sentry.ProjectKey) resource.TestCheckFunc { +func testAccCheckSentryKeyExists(n string, projectKey *sentryclient.ProjectKey) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -84,7 +114,7 @@ func testAccCheckSentryKeyExists(n string, projectKey *sentry.ProjectKey) resour return errors.New("No key ID is set") } - client := testAccProvider.Meta().(*sentry.Client) + client := testAccProvider.Meta().(*sentryclient.Client) keys, _, err := client.ProjectKeys.List( rs.Primary.Attributes["organization"], rs.Primary.Attributes["project"], @@ -105,38 +135,54 @@ func testAccCheckSentryKeyExists(n string, projectKey *sentry.ProjectKey) resour var testAccSentryKeyConfig = fmt.Sprintf(` resource "sentry_team" "test_team" { - organization = "%s" + organization = "%[1]s" name = "Test team" } resource "sentry_project" "test_project" { - organization = "%s" + organization = "%[1]s" team = "${sentry_team.test_team.id}" name = "Test project" } resource "sentry_key" "test_key" { - organization = "%s" + organization = "%[1]s" + project = "${sentry_project.test_project.id}" + name = "Test key" + } + + resource "sentry_key" "test_key_rate_limit" { + organization = "%[1]s" project = "${sentry_project.test_project.id}" name = "Test key" + rate_limit_window = 86400 + rate_limit_count = 1000 } -`, testOrganization, testOrganization, testOrganization) +`, testOrganization) var testAccSentryKeyUpdateConfig = fmt.Sprintf(` resource "sentry_team" "test_team" { - organization = "%s" + organization = "%[1]s" name = "Test team" } resource "sentry_project" "test_project" { - organization = "%s" + organization = "%[1]s" team = "${sentry_team.test_team.id}" name = "Test project" } resource "sentry_key" "test_key" { - organization = "%s" + organization = "%[1]s" project = "${sentry_project.test_project.id}" name = "Test key changed" } -`, testOrganization, testOrganization, testOrganization) + + resource "sentry_key" "test_key_rate_limit" { + organization = "%[1]s" + project = "${sentry_project.test_project.id}" + name = "Test key" + rate_limit_window = 100 + rate_limit_count = 100 + } +`, testOrganization) diff --git a/sentry/resource_sentry_organization.go b/sentry/resource_sentry_organization.go index d380d21da..2f1681382 100644 --- a/sentry/resource_sentry_organization.go +++ b/sentry/resource_sentry_organization.go @@ -3,8 +3,8 @@ package sentry import ( "log" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/jianyuan/go-sentry/sentry" + "github.com/canva/terraform-provider-sentry/sentryclient" + "github.com/hashicorp/terraform/helper/schema" ) func resourceSentryOrganization() *schema.Resource { @@ -39,12 +39,12 @@ func resourceSentryOrganization() *schema.Resource { } func resourceSentryOrganizationCreate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) - params := &sentry.CreateOrganizationParams{ + params := &sentryclient.CreateOrganizationParams{ Name: d.Get("name").(string), Slug: d.Get("slug").(string), - AgreeTerms: sentry.Bool(d.Get("agree_terms").(bool)), + AgreeTerms: sentryclient.Bool(d.Get("agree_terms").(bool)), } log.Printf("[DEBUG] Creating Sentry organization %s", params.Name) @@ -58,7 +58,7 @@ func resourceSentryOrganizationCreate(d *schema.ResourceData, meta interface{}) } func resourceSentryOrganizationRead(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) slug := d.Id() log.Printf("[DEBUG] Reading Sentry organization %s", slug) @@ -77,11 +77,11 @@ func resourceSentryOrganizationRead(d *schema.ResourceData, meta interface{}) er } func resourceSentryOrganizationUpdate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) slug := d.Id() log.Printf("[DEBUG] Updating Sentry organization %s", slug) - params := &sentry.UpdateOrganizationParams{ + params := &sentryclient.UpdateOrganizationParams{ Name: d.Get("name").(string), Slug: d.Get("slug").(string), } @@ -96,7 +96,7 @@ func resourceSentryOrganizationUpdate(d *schema.ResourceData, meta interface{}) } func resourceSentryOrganizationDelete(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) slug := d.Id() log.Printf("[DEBUG] Deleting Sentry organization %s", slug) diff --git a/sentry/resource_sentry_project.go b/sentry/resource_sentry_project.go index 3d98e3bf3..ab2c60322 100644 --- a/sentry/resource_sentry_project.go +++ b/sentry/resource_sentry_project.go @@ -5,8 +5,8 @@ import ( "log" "strings" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/jianyuan/go-sentry/sentry" + "github.com/canva/terraform-provider-sentry/sentryclient" + "github.com/hashicorp/terraform/helper/schema" ) func resourceSentryProject() *schema.Resource { @@ -30,6 +30,18 @@ func resourceSentryProject() *schema.Resource { Required: true, Description: "The slug of the team to create the project for", }, + "remove_default_key": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to remove the default key", + Default: false, + }, + "remove_default_rule": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to remove the default rule", + Default: false, + }, "name": { Type: schema.TypeString, Required: true, @@ -83,6 +95,15 @@ func resourceSentryProject() *schema.Resource { Type: schema.TypeInt, Computed: true, }, + "allowed_domains": { + Type: schema.TypeList, + Computed: true, + Description: "The domains allowd to be collected", + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, // TODO: Project options }, @@ -90,13 +111,14 @@ func resourceSentryProject() *schema.Resource { } func resourceSentryProjectCreate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) org := d.Get("organization").(string) team := d.Get("team").(string) - params := &sentry.CreateProjectParams{ - Name: d.Get("name").(string), - Slug: d.Get("slug").(string), + params := &sentryclient.CreateProjectParams{ + Name: d.Get("name").(string), + Slug: d.Get("slug").(string), + Platform: d.Get("platform").(string), } proj, _, err := client.Projects.Create(org, team, params) @@ -104,12 +126,27 @@ func resourceSentryProjectCreate(d *schema.ResourceData, meta interface{}) error return err } + if d.Get("remove_default_key").(bool) { + err = removeDefaultKey(client, org, proj.Slug) + if err != nil { + return err + } + } + + if d.Get("remove_default_rule").(bool) { + err = removeDefaultRule(client, org, proj.Slug) + if err != nil { + return err + } + } + d.SetId(proj.Slug) + return resourceSentryProjectRead(d, meta) } func resourceSentryProjectRead(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) slug := d.Id() org := d.Get("organization").(string) @@ -133,6 +170,7 @@ func resourceSentryProjectRead(d *schema.ResourceData, meta interface{}) error { d.Set("status", proj.Status) d.Set("digests_min_delay", proj.DigestsMinDelay) d.Set("digests_max_delay", proj.DigestsMaxDelay) + d.Set("allowed_domains", proj.AllowedDomains) // TODO: Project options @@ -140,11 +178,11 @@ func resourceSentryProjectRead(d *schema.ResourceData, meta interface{}) error { } func resourceSentryProjectUpdate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) slug := d.Id() org := d.Get("organization").(string) - params := &sentry.UpdateProjectParams{ + params := &sentryclient.UpdateProjectParams{ Name: d.Get("name").(string), Slug: d.Get("slug").(string), } @@ -162,6 +200,14 @@ func resourceSentryProjectUpdate(d *schema.ResourceData, meta interface{}) error params.DigestsMaxDelay = Int(v.(int)) } + allowed_domains := []string{} + for _, url := range d.Get("allowed_domains").([]interface{}) { + allowed_domains = append(allowed_domains, url.(string)) + } + if len(allowed_domains) > 0 { + params.AllowedDomains = allowed_domains + } + proj, _, err := client.Projects.Update(org, slug, params) if err != nil { return err @@ -172,7 +218,7 @@ func resourceSentryProjectUpdate(d *schema.ResourceData, meta interface{}) error } func resourceSentryProjectDelete(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) slug := d.Id() org := d.Get("organization").(string) @@ -197,3 +243,37 @@ func resourceSentryProjectImporter(d *schema.ResourceData, meta interface{}) ([] return []*schema.ResourceData{d}, nil } + +func removeDefaultKey(client *sentryclient.Client, org, projSlug string) error { + keys, _, err := client.ProjectKeys.List(org, projSlug) + if err != nil { + return err + } + var defaultKeyId string + for _, key := range keys { + if key.Name == "Default" { + defaultKeyId = key.ID + break + } + } + + client.ProjectKeys.Delete(org, projSlug, defaultKeyId) + return nil +} + +func removeDefaultRule(client *sentryclient.Client, org, projSlug string) error { + rules, _, err := client.Rules.List(org, projSlug) + if err != nil { + return err + } + var defaultRuleId string + for _, rule := range rules { + if rule.Name == "Send a notification for new issues" { + defaultRuleId = rule.ID + break + } + } + + client.Rules.Delete(org, projSlug, defaultRuleId) + return nil +} diff --git a/sentry/resource_sentry_project_plugin.go b/sentry/resource_sentry_project_plugin.go index 2d3a242d7..dbfb89905 100644 --- a/sentry/resource_sentry_project_plugin.go +++ b/sentry/resource_sentry_project_plugin.go @@ -3,8 +3,8 @@ package sentry import ( "log" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/jianyuan/go-sentry/sentry" + "github.com/canva/terraform-provider-sentry/sentryclient" + "github.com/hashicorp/terraform/helper/schema" ) func resourceSentryPlugin() *schema.Resource { @@ -43,7 +43,7 @@ func resourceSentryPlugin() *schema.Resource { } func resourceSentryPluginCreate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) plugin := d.Get("plugin").(string) org := d.Get("organization").(string) @@ -66,7 +66,7 @@ func resourceSentryPluginCreate(d *schema.ResourceData, meta interface{}) error } func resourceSentryPluginRead(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) id := d.Id() org := d.Get("organization").(string) @@ -98,7 +98,7 @@ func resourceSentryPluginRead(d *schema.ResourceData, meta interface{}) error { } func resourceSentryPluginUpdate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) id := d.Id() org := d.Get("organization").(string) @@ -113,7 +113,7 @@ func resourceSentryPluginUpdate(d *schema.ResourceData, meta interface{}) error } func resourceSentryPluginDelete(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) id := d.Id() org := d.Get("organization").(string) diff --git a/sentry/resource_sentry_project_rule.go b/sentry/resource_sentry_project_rule.go new file mode 100755 index 000000000..efca8ebb0 --- /dev/null +++ b/sentry/resource_sentry_project_rule.go @@ -0,0 +1,230 @@ +package sentry + +import ( + "errors" + + "github.com/canva/terraform-provider-sentry/sentryclient" + "github.com/hashicorp/terraform/helper/schema" + "github.com/mitchellh/mapstructure" +) + +const ( + defaultActionMatch = "any" + defaultFrequency = 30 +) + +func resourceSentryRule() *schema.Resource { + return &schema.Resource{ + Create: resourceSentryRuleCreate, + Read: resourceSentryRuleRead, + Update: resourceSentryRuleUpdate, + Delete: resourceSentryRuleDelete, + Importer: &schema.ResourceImporter{ + State: resourceSentryRuleImporter, + }, + + Schema: map[string]*schema.Schema{ + "organization": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the organization the project belongs to", + }, + "project": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the project to create the plugin for", + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The rule name", + }, + "action_match": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "actions": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeMap, + }, + }, + "conditions": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeMap, + }, + }, + "frequency": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Perform actions at most once every X minutes", + }, + "environment": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Perform rule in a specific environment", + }, + }, + } +} + +func resourceSentryRuleCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*sentryclient.Client) + + name := d.Get("name").(string) + org := d.Get("organization").(string) + project := d.Get("project").(string) + environment := d.Get("environment").(string) + actionMatch := d.Get("action_match").(string) + inputConditions := d.Get("conditions").([]interface{}) + inputActions := d.Get("actions").([]interface{}) + frequency := d.Get("frequency").(int) + + if actionMatch == "" { + actionMatch = defaultActionMatch + } + if frequency == 0 { + frequency = defaultFrequency + } + + conditions := make([]*sentryclient.CreateRuleConditionParams, len(inputConditions)) + for i, ic := range inputConditions { + var condition sentryclient.CreateRuleConditionParams + mapstructure.Decode(ic, &condition) + conditions[i] = &condition + } + actions := make([]*sentryclient.CreateRuleActionParams, len(inputActions)) + for i, ia := range inputActions { + var action sentryclient.CreateRuleActionParams + mapstructure.Decode(ia, &action) + actions[i] = &action + } + + params := &sentryclient.CreateRuleParams{ + ActionMatch: actionMatch, + Environment: environment, + Frequency: frequency, + Name: name, + Conditions: conditions, + Actions: actions, + } + + if environment != "" { + params.Environment = environment + } + + rule, _, err := client.Rules.Create(org, project, params) + if err != nil { + return err + } + + d.SetId(rule.ID) + + return resourceSentryRuleRead(d, meta) +} + +func resourceSentryRuleRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*sentryclient.Client) + org := d.Get("organization").(string) + project := d.Get("project").(string) + id := d.Id() + + rules, _, err := client.Rules.List(org, project) + if err != nil { + d.SetId("") + return nil + } + + var rule *sentryclient.Rule + for _, r := range rules { + if r.ID == id { + rule = &r + break + } + } + + if rule == nil { + return errors.New("Could not find rule with ID " + id) + } + + d.SetId(rule.ID) + d.Set("name", rule.Name) + d.Set("actions", rule.Actions) + d.Set("conditions", rule.Conditions) + d.Set("frequency", rule.Frequency) + d.Set("environment", rule.Environment) + + return nil +} + +func resourceSentryRuleUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*sentryclient.Client) + + id := d.Id() + name := d.Get("name").(string) + org := d.Get("organization").(string) + project := d.Get("project").(string) + environment := d.Get("environment").(string) + actionMatch := d.Get("action_match").(string) + inputConditions := d.Get("conditions").([]interface{}) + inputActions := d.Get("actions").([]interface{}) + frequency := d.Get("frequency").(int) + + if actionMatch == "" { + actionMatch = defaultActionMatch + } + if frequency == 0 { + frequency = defaultFrequency + } + + conditions := make([]sentryclient.RuleCondition, len(inputConditions)) + for i, ic := range inputConditions { + var condition sentryclient.RuleCondition + mapstructure.Decode(ic, &condition) + conditions[i] = condition + } + actions := make([]sentryclient.RuleAction, len(inputActions)) + for i, ia := range inputActions { + var action sentryclient.RuleAction + mapstructure.Decode(ia, &action) + actions[i] = action + } + + params := &sentryclient.Rule{ + ID: id, + ActionMatch: actionMatch, + Environment: environment, + Frequency: frequency, + Name: name, + Conditions: conditions, + Actions: actions, + } + + if environment != "" { + params.Environment = environment + } + + _, _, err := client.Rules.Update(org, project, id, params) + if err != nil { + return err + } + + return resourceSentryRuleRead(d, meta) +} + +func resourceSentryRuleDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*sentryclient.Client) + + id := d.Id() + org := d.Get("organization").(string) + project := d.Get("project").(string) + + _, err := client.Rules.Delete(org, project, id) + return err +} diff --git a/sentry/resource_sentry_project_test.go b/sentry/resource_sentry_project_test.go index 7d8e899f0..a5f9661e8 100644 --- a/sentry/resource_sentry_project_test.go +++ b/sentry/resource_sentry_project_test.go @@ -3,16 +3,17 @@ package sentry import ( "errors" "fmt" + "sort" "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" - "github.com/jianyuan/go-sentry/sentry" + "github.com/canva/terraform-provider-sentry/sentryclient" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" ) func TestAccSentryProject_basic(t *testing.T) { - var project sentry.Project + var project sentryclient.Project random := acctest.RandInt() newProjectSlug := fmt.Sprintf("test-project-%d", random) @@ -27,7 +28,8 @@ func TestAccSentryProject_basic(t *testing.T) { organization = "%s" team = "${sentry_team.test_team.id}" name = "Test project changed" - slug = "%s" + slug = "%s" + allowed_domains = ["www.canva.com", "www.canva.cn"] } `, testOrganization, testOrganization, newProjectSlug) @@ -41,10 +43,11 @@ func TestAccSentryProject_basic(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckSentryProjectExists("sentry_project.test_project", &project), testAccCheckSentryProjectAttributes(&project, &testAccSentryProjectExpectedAttributes{ - Name: "Test project", - Organization: testOrganization, - Team: "Test team", - SlugPresent: true, + Name: "Test project", + Organization: testOrganization, + Team: "Test team", + SlugPresent: true, + AllowedDomains: []string{"*"}, }), ), }, @@ -53,19 +56,28 @@ func TestAccSentryProject_basic(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckSentryProjectExists("sentry_project.test_project", &project), testAccCheckSentryProjectAttributes(&project, &testAccSentryProjectExpectedAttributes{ - Name: "Test project changed", - Organization: testOrganization, - Team: "Test team", - Slug: newProjectSlug, + Name: "Test project changed", + Organization: testOrganization, + Team: "Test team", + Slug: newProjectSlug, + AllowedDomains: []string{"www.canva.com", "www.canva.cn"}, }), ), }, + { + Config: testAccSentryProjectRemoveKeyConfig, + Check: testAccCheckSentryKeyRemoved("sentry_project.test_project_remove_key"), + }, + { + Config: testAccSentryProjectRemoveRuleConfig, + Check: testAccCheckSentryRuleRemoved("sentry_project.test_project_remove_rule"), + }, }, }) } func testAccCheckSentryProjectDestroy(s *terraform.State) error { - client := testAccProvider.Meta().(*sentry.Client) + client := testAccProvider.Meta().(*sentryclient.Client) for _, rs := range s.RootModule().Resources { if rs.Type != "sentry_project" { @@ -86,7 +98,7 @@ func testAccCheckSentryProjectDestroy(s *terraform.State) error { return nil } -func testAccCheckSentryProjectExists(n string, proj *sentry.Project) resource.TestCheckFunc { +func testAccCheckSentryProjectExists(n string, proj *sentryclient.Project) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -97,7 +109,7 @@ func testAccCheckSentryProjectExists(n string, proj *sentry.Project) resource.Te return errors.New("No project ID is set") } - client := testAccProvider.Meta().(*sentry.Client) + client := testAccProvider.Meta().(*sentryclient.Client) sentryProj, _, err := client.Projects.Get( rs.Primary.Attributes["organization"], rs.Primary.ID, @@ -110,16 +122,47 @@ func testAccCheckSentryProjectExists(n string, proj *sentry.Project) resource.Te } } +func testAccCheckSentryKeyRemoved(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs := s.RootModule().Resources[n] + client := testAccProvider.Meta().(*sentryclient.Client) + keys, _, err := client.ProjectKeys.List(rs.Primary.Attributes["organization"], rs.Primary.ID) + if err != nil { + return err + } + if len(keys) != 0 { + return fmt.Errorf("Default key not removed") + } + return nil + } +} + +func testAccCheckSentryRuleRemoved(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs := s.RootModule().Resources[n] + client := testAccProvider.Meta().(*sentryclient.Client) + keys, _, err := client.Rules.List(rs.Primary.Attributes["organization"], rs.Primary.ID) + if err != nil { + return err + } + if len(keys) != 0 { + return fmt.Errorf("Default rule not removed") + } + return nil + } +} + type testAccSentryProjectExpectedAttributes struct { Name string Organization string Team string - SlugPresent bool - Slug string + SlugPresent bool + Slug string + AllowedDomains []string } -func testAccCheckSentryProjectAttributes(proj *sentry.Project, want *testAccSentryProjectExpectedAttributes) resource.TestCheckFunc { +func testAccCheckSentryProjectAttributes(proj *sentryclient.Project, want *testAccSentryProjectExpectedAttributes) resource.TestCheckFunc { return func(s *terraform.State) error { if proj.Name != want.Name { return fmt.Errorf("got proj %q; want %q", proj.Name, want.Name) @@ -141,6 +184,18 @@ func testAccCheckSentryProjectAttributes(proj *sentry.Project, want *testAccSent return fmt.Errorf("got slug %q; want %q", proj.Slug, want.Slug) } + if len(want.AllowedDomains) == len(proj.AllowedDomains) { + sort.Strings(want.AllowedDomains) + sort.Strings(proj.AllowedDomains) + for index, _ := range want.AllowedDomains { + if want.AllowedDomains[index] != proj.AllowedDomains[index] { + return fmt.Errorf("want: %v, get: %v", want.AllowedDomains, proj.AllowedDomains) + } + } + } else { + return fmt.Errorf("want: %v, get: %v", want.AllowedDomains, proj.AllowedDomains) + } + return nil } } @@ -157,3 +212,32 @@ var testAccSentryProjectConfig = fmt.Sprintf(` name = "Test project" } `, testOrganization, testOrganization) + +var testAccSentryProjectRemoveKeyConfig = fmt.Sprintf(` + resource "sentry_team" "test_team" { + organization = "%s" + name = "Test team" + } + + resource "sentry_project" "test_project_remove_key" { + organization = "%s" + team = "${sentry_team.test_team.id}" + name = "Test project" + remove_default_key = true + } +`, testOrganization, testOrganization) + +var testAccSentryProjectRemoveRuleConfig = fmt.Sprintf(` + resource "sentry_team" "test_team" { + organization = "%s" + name = "Test team" + } + + resource "sentry_project" "test_project_remove_rule" { + organization = "%s" + team = "${sentry_team.test_team.id}" + name = "Test project" + remove_default_rule = true + } +`, testOrganization, testOrganization) + diff --git a/sentry/resource_sentry_team.go b/sentry/resource_sentry_team.go index ccbe5a510..d3fbe1bb0 100644 --- a/sentry/resource_sentry_team.go +++ b/sentry/resource_sentry_team.go @@ -3,8 +3,8 @@ package sentry import ( "log" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/jianyuan/go-sentry/sentry" + "github.com/canva/terraform-provider-sentry/sentryclient" + "github.com/hashicorp/terraform/helper/schema" ) func resourceSentryTeam() *schema.Resource { @@ -55,10 +55,10 @@ func resourceSentryTeam() *schema.Resource { } func resourceSentryTeamCreate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) org := d.Get("organization").(string) - params := &sentry.CreateTeamParams{ + params := &sentryclient.CreateTeamParams{ Name: d.Get("name").(string), Slug: d.Get("slug").(string), } @@ -74,7 +74,7 @@ func resourceSentryTeamCreate(d *schema.ResourceData, meta interface{}) error { } func resourceSentryTeamRead(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) slug := d.Id() org := d.Get("organization").(string) @@ -98,11 +98,11 @@ func resourceSentryTeamRead(d *schema.ResourceData, meta interface{}) error { } func resourceSentryTeamUpdate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) slug := d.Id() org := d.Get("organization").(string) - params := &sentry.UpdateTeamParams{ + params := &sentryclient.UpdateTeamParams{ Name: d.Get("name").(string), Slug: d.Get("slug").(string), } @@ -118,7 +118,7 @@ func resourceSentryTeamUpdate(d *schema.ResourceData, meta interface{}) error { } func resourceSentryTeamDelete(d *schema.ResourceData, meta interface{}) error { - client := meta.(*sentry.Client) + client := meta.(*sentryclient.Client) slug := d.Id() org := d.Get("organization").(string) diff --git a/sentry/resource_sentry_team_test.go b/sentry/resource_sentry_team_test.go index 606491906..336ce27d4 100644 --- a/sentry/resource_sentry_team_test.go +++ b/sentry/resource_sentry_team_test.go @@ -5,14 +5,14 @@ import ( "fmt" "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" - "github.com/jianyuan/go-sentry/sentry" + "github.com/canva/terraform-provider-sentry/sentryclient" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" ) func TestAccSentryTeam_basic(t *testing.T) { - var team sentry.Team + var team sentryclient.Team random := acctest.RandInt() newTeamSlug := fmt.Sprintf("test-team-changed-%d", random) @@ -55,7 +55,7 @@ func TestAccSentryTeam_basic(t *testing.T) { } func testAccCheckSentryTeamDestroy(s *terraform.State) error { - client := testAccProvider.Meta().(*sentry.Client) + client := testAccProvider.Meta().(*sentryclient.Client) for _, rs := range s.RootModule().Resources { if rs.Type != "sentry_team" { @@ -79,7 +79,7 @@ func testAccCheckSentryTeamDestroy(s *terraform.State) error { return nil } -func testAccCheckSentryTeamExists(n string, team *sentry.Team) resource.TestCheckFunc { +func testAccCheckSentryTeamExists(n string, team *sentryclient.Team) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -90,7 +90,7 @@ func testAccCheckSentryTeamExists(n string, team *sentry.Team) resource.TestChec return errors.New("No team ID is set") } - client := testAccProvider.Meta().(*sentry.Client) + client := testAccProvider.Meta().(*sentryclient.Client) sentryTeam, _, err := client.Teams.Get( rs.Primary.Attributes["organization"], rs.Primary.ID, @@ -110,7 +110,7 @@ type testAccSentryTeamExpectedAttributes struct { Slug string } -func testAccCheckSentryTeamAttributes(team *sentry.Team, want *testAccSentryTeamExpectedAttributes) resource.TestCheckFunc { +func testAccCheckSentryTeamAttributes(team *sentryclient.Team, want *testAccSentryTeamExpectedAttributes) resource.TestCheckFunc { return func(s *terraform.State) error { if team.Name != want.Name { return fmt.Errorf("got team %q; want %q", team.Name, want.Name) diff --git a/sentryclient/errors.go b/sentryclient/errors.go new file mode 100644 index 000000000..b640db288 --- /dev/null +++ b/sentryclient/errors.go @@ -0,0 +1,36 @@ +package sentryclient + +import "fmt" + +// APIError represents a Sentry API Error response +type APIError map[string]interface{} + +// TODO: use this instead +// type apiError struct { +// Detail string `json:"detail"` +// } + +func (e APIError) Error() string { + if len(e) == 1 { + if detail, ok := e["detail"].(string); ok { + return fmt.Sprintf("sentry: %s", detail) + } + } + + return fmt.Sprintf("sentry: %v", map[string]interface{}(e)) +} + +// Empty returns true if empty. +func (e APIError) Empty() bool { + return len(e) == 0 +} + +func relevantError(httpError error, apiError APIError) error { + if httpError != nil { + return httpError + } + if !apiError.Empty() { + return apiError + } + return nil +} diff --git a/sentryclient/helpers.go b/sentryclient/helpers.go new file mode 100644 index 000000000..4b8b3a2b4 --- /dev/null +++ b/sentryclient/helpers.go @@ -0,0 +1,6 @@ +package sentryclient + +// Bool returns a pointer to the bool value. +func Bool(v bool) *bool { + return &v +} diff --git a/sentryclient/organizations.go b/sentryclient/organizations.go new file mode 100644 index 000000000..2c2069e25 --- /dev/null +++ b/sentryclient/organizations.go @@ -0,0 +1,139 @@ +package sentryclient + +import ( + "net/http" + "time" + + "github.com/dghubble/sling" +) + +// OrganizationStatus represents a Sentry organization's status. +type OrganizationStatus struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// OrganizationQuota represents a Sentry organization's quota. +type OrganizationQuota struct { + MaxRate int `json:"maxRate"` + MaxRateInterval int `json:"maxRateInterval"` + AccountLimit int `json:"accountLimit"` + ProjectLimit int `json:"projectLimit"` +} + +// OrganizationAvailableRole represents a Sentry organization's available role. +type OrganizationAvailableRole struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// Organization represents a Sentry organization. +// Based on https://github.com/getsentry/sentry/blob/9.0.0/src/sentry/api/serializers/models/organization.py +type Organization struct { + ID string `json:"id"` + Slug string `json:"slug"` + Status OrganizationStatus `json:"status"` + Name string `json:"name"` + DateCreated time.Time `json:"dateCreated"` + IsEarlyAdopter bool `json:"isEarlyAdopter"` + Avatar Avatar `json:"avatar"` + + Quota OrganizationQuota `json:"quota"` + + IsDefault bool `json:"isDefault"` + DefaultRole string `json:"defaultRole"` + AvailableRoles []OrganizationAvailableRole `json:"availableRoles"` + OpenMembership bool `json:"openMembership"` + Require2FA bool `json:"require2FA"` + AllowSharedIssues bool `json:"allowSharedIssues"` + EnhancedPrivacy bool `json:"enhancedPrivacy"` + DataScrubber bool `json:"dataScrubber"` + DataScrubberDefaults bool `json:"dataScrubberDefaults"` + SensitiveFields []string `json:"sensitiveFields"` + SafeFields []string `json:"safeFields"` + ScrubIPAddresses bool `json:"scrubIPAddresses"` + + Access []string `json:"access"` + Features []string `json:"features"` + PendingAccessRequests int `json:"pendingAccessRequests"` + + AccountRateLimit int `json:"accountRateLimit"` + ProjectRateLimit int `json:"projectRateLimit"` + + Teams []Team `json:"teams"` + Projects []ProjectSummary `json:"projects"` + // TODO: onboardingTasks +} + +// OrganizationService provides methods for accessing Sentry organization API endpoints. +// https://docs.sentry.io/api/organizations/ +type OrganizationService struct { + sling *sling.Sling +} + +func newOrganizationService(sling *sling.Sling) *OrganizationService { + return &OrganizationService{ + sling: sling.Path("organizations/"), + } +} + +// ListOrganizationParams are the parameters for OrganizationService.List. +type ListOrganizationParams struct { + Cursor string `url:"cursor,omitempty"` +} + +// List organizations available to the authenticated session. +// https://docs.sentry.io/api/organizations/get-organization-index/ +func (s *OrganizationService) List(params *ListOrganizationParams) ([]Organization, *http.Response, error) { + organizations := new([]Organization) + apiError := new(APIError) + resp, err := s.sling.New().Get("").QueryStruct(params).Receive(organizations, apiError) + return *organizations, resp, relevantError(err, *apiError) +} + +// CreateOrganizationParams are the parameters for OrganizationService.Create. +type CreateOrganizationParams struct { + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` + AgreeTerms *bool `json:"agreeTerms,omitempty"` +} + +// Get a Sentry organization. +// https://docs.sentry.io/api/organizations/get-organization-details/ +func (s *OrganizationService) Get(slug string) (*Organization, *http.Response, error) { + org := new(Organization) + apiError := new(APIError) + resp, err := s.sling.New().Get(slug+"/").Receive(org, apiError) + return org, resp, relevantError(err, *apiError) +} + +// Create a new Sentry organization. +// https://docs.sentry.io/api/organizations/post-organization-index/ +func (s *OrganizationService) Create(params *CreateOrganizationParams) (*Organization, *http.Response, error) { + org := new(Organization) + apiError := new(APIError) + resp, err := s.sling.New().Post("").BodyJSON(params).Receive(org, apiError) + return org, resp, relevantError(err, *apiError) +} + +// UpdateOrganizationParams are the parameters for OrganizationService.Update. +type UpdateOrganizationParams struct { + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` +} + +// Update a Sentry organization. +// https://docs.sentry.io/api/organizations/put-organization-details/ +func (s *OrganizationService) Update(slug string, params *UpdateOrganizationParams) (*Organization, *http.Response, error) { + org := new(Organization) + apiError := new(APIError) + resp, err := s.sling.New().Put(slug+"/").BodyJSON(params).Receive(org, apiError) + return org, resp, relevantError(err, *apiError) +} + +// Delete a Sentry organization. +func (s *OrganizationService) Delete(slug string) (*http.Response, error) { + apiError := new(APIError) + resp, err := s.sling.New().Delete(slug+"/").Receive(nil, apiError) + return resp, relevantError(err, *apiError) +} diff --git a/sentryclient/organizations_test.go b/sentryclient/organizations_test.go new file mode 100644 index 000000000..8de4b8c94 --- /dev/null +++ b/sentryclient/organizations_test.go @@ -0,0 +1,523 @@ +package sentryclient + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestOrganizationService_List(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/organizations/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "GET", r) + assertQuery(t, map[string]string{"cursor": "1500300636142:0:1"}, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `[ + { + "name": "The Interstellar Jurisdiction", + "slug": "the-interstellar-jurisdiction", + "avatar": { + "avatarUuid": null, + "avatarType": "letter_avatar" + }, + "dateCreated": "2017-07-17T14:10:36.141Z", + "id": "2", + "isEarlyAdopter": false + } + ]`) + }) + + client := NewClient(httpClient, nil, "") + organizations, _, err := client.Organizations.List(&ListOrganizationParams{ + Cursor: "1500300636142:0:1", + }) + assert.NoError(t, err) + expected := []Organization{ + { + ID: "2", + Slug: "the-interstellar-jurisdiction", + Name: "The Interstellar Jurisdiction", + DateCreated: mustParseTime("2017-07-17T14:10:36.141Z"), + IsEarlyAdopter: false, + Avatar: Avatar{ + UUID: nil, + Type: "letter_avatar", + }, + }, + } + assert.Equal(t, expected, organizations) +} + +func TestOrganizationService_Get(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/organizations/the-interstellar-jurisdiction/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "GET", r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "access": [], + "allowSharedIssues": true, + "availableRoles": [{ + "id": "member", + "name": "Member" + }, + { + "id": "admin", + "name": "Admin" + }, + { + "id": "manager", + "name": "Manager" + }, + { + "id": "owner", + "name": "Owner" + } + ], + "avatar": { + "avatarType": "letter_avatar", + "avatarUuid": null + }, + "dataScrubber": false, + "dataScrubberDefaults": false, + "dateCreated": "2018-09-20T15:47:52.908Z", + "defaultRole": "member", + "enhancedPrivacy": false, + "experiments": {}, + "features": [ + "sso", + "api-keys", + "github-apps", + "repos", + "new-issue-ui", + "github-enterprise", + "bitbucket-integration", + "jira-integration", + "vsts-integration", + "suggested-commits", + "new-teams", + "open-membership", + "shared-issues" + ], + "id": "2", + "isDefault": false, + "isEarlyAdopter": false, + "name": "The Interstellar Jurisdiction", + "onboardingTasks": [], + "openMembership": true, + "pendingAccessRequests": 0, + "projects": [{ + "dateCreated": "2018-09-20T15:47:56.723Z", + "firstEvent": null, + "hasAccess": true, + "id": "3", + "isBookmarked": false, + "isMember": false, + "latestDeploys": null, + "name": "Prime Mover", + "platform": null, + "platforms": [], + "slug": "prime-mover", + "team": { + "id": "2", + "name": "Powerful Abolitionist", + "slug": "powerful-abolitionist" + }, + "teams": [{ + "id": "2", + "name": "Powerful Abolitionist", + "slug": "powerful-abolitionist" + }] + }, + { + "dateCreated": "2018-09-20T15:47:52.926Z", + "firstEvent": null, + "hasAccess": true, + "id": "2", + "isBookmarked": false, + "isMember": false, + "latestDeploys": null, + "name": "Pump Station", + "platform": null, + "platforms": [], + "slug": "pump-station", + "team": { + "id": "2", + "name": "Powerful Abolitionist", + "slug": "powerful-abolitionist" + }, + "teams": [{ + "id": "2", + "name": "Powerful Abolitionist", + "slug": "powerful-abolitionist" + }] + }, + { + "dateCreated": "2018-09-20T15:48:07.592Z", + "firstEvent": null, + "hasAccess": true, + "id": "4", + "isBookmarked": false, + "isMember": false, + "latestDeploys": null, + "name": "The Spoiled Yoghurt", + "platform": null, + "platforms": [], + "slug": "the-spoiled-yoghurt", + "team": { + "id": "2", + "name": "Powerful Abolitionist", + "slug": "powerful-abolitionist" + }, + "teams": [{ + "id": "2", + "name": "Powerful Abolitionist", + "slug": "powerful-abolitionist" + }] + } + ], + "quota": { + "accountLimit": 0, + "maxRate": 0, + "maxRateInterval": 60, + "projectLimit": 100 + }, + "require2FA": false, + "safeFields": [], + "scrapeJavaScript": true, + "scrubIPAddresses": false, + "sensitiveFields": [], + "slug": "the-interstellar-jurisdiction", + "status": { + "id": "active", + "name": "active" + }, + "storeCrashReports": false, + "teams": [{ + "avatar": { + "avatarType": "letter_avatar", + "avatarUuid": null + }, + "dateCreated": "2018-09-20T15:48:07.803Z", + "hasAccess": true, + "id": "3", + "isMember": false, + "isPending": false, + "name": "Ancient Gabelers", + "slug": "ancient-gabelers" + }, + { + "avatar": { + "avatarType": "letter_avatar", + "avatarUuid": null + }, + "dateCreated": "2018-09-20T15:47:52.922Z", + "hasAccess": true, + "id": "2", + "isMember": false, + "isPending": false, + "name": "Powerful Abolitionist", + "slug": "powerful-abolitionist" + } + ] + }`) + }) + + client := NewClient(httpClient, nil, "") + organization, _, err := client.Organizations.Get("the-interstellar-jurisdiction") + assert.NoError(t, err) + expected := &Organization{ + ID: "2", + Slug: "the-interstellar-jurisdiction", + Status: OrganizationStatus{ + ID: "active", + Name: "active", + }, + Name: "The Interstellar Jurisdiction", + DateCreated: mustParseTime("2018-09-20T15:47:52.908Z"), + IsEarlyAdopter: false, + Avatar: Avatar{ + Type: "letter_avatar", + }, + + Quota: OrganizationQuota{ + MaxRate: 0, + MaxRateInterval: 60, + AccountLimit: 0, + ProjectLimit: 100, + }, + + IsDefault: false, + DefaultRole: "member", + AvailableRoles: []OrganizationAvailableRole{ + { + ID: "member", + Name: "Member", + }, + { + ID: "admin", + Name: "Admin", + }, + { + ID: "manager", + Name: "Manager", + }, + { + ID: "owner", + Name: "Owner", + }, + }, + OpenMembership: true, + Require2FA: false, + AllowSharedIssues: true, + EnhancedPrivacy: false, + DataScrubber: false, + DataScrubberDefaults: false, + SensitiveFields: []string{}, + SafeFields: []string{}, + ScrubIPAddresses: false, + + Access: []string{}, + Features: []string{ + "sso", + "api-keys", + "github-apps", + "repos", + "new-issue-ui", + "github-enterprise", + "bitbucket-integration", + "jira-integration", + "vsts-integration", + "suggested-commits", + "new-teams", + "open-membership", + "shared-issues", + }, + PendingAccessRequests: 0, + + AccountRateLimit: 0, + ProjectRateLimit: 0, + + Teams: []Team{ + { + ID: "3", + Slug: "ancient-gabelers", + Name: "Ancient Gabelers", + DateCreated: mustParseTime("2018-09-20T15:48:07.803Z"), + IsMember: false, + HasAccess: true, + IsPending: false, + Avatar: Avatar{ + Type: "letter_avatar", + }, + }, + { + ID: "2", + Slug: "powerful-abolitionist", + Name: "Powerful Abolitionist", + DateCreated: mustParseTime("2018-09-20T15:47:52.922Z"), + IsMember: false, + HasAccess: true, + IsPending: false, + Avatar: Avatar{ + Type: "letter_avatar", + }, + }, + }, + Projects: []ProjectSummary{ + { + ID: "3", + Name: "Prime Mover", + Slug: "prime-mover", + IsBookmarked: false, + IsMember: false, + HasAccess: true, + DateCreated: mustParseTime("2018-09-20T15:47:56.723Z"), + FirstEvent: time.Time{}, + Platform: nil, + Platforms: []string{}, + Team: &ProjectSummaryTeam{ + ID: "2", + Name: "Powerful Abolitionist", + Slug: "powerful-abolitionist", + }, + Teams: []ProjectSummaryTeam{ + { + ID: "2", + Name: "Powerful Abolitionist", + Slug: "powerful-abolitionist", + }, + }, + }, + { + ID: "2", + Name: "Pump Station", + Slug: "pump-station", + IsBookmarked: false, + IsMember: false, + HasAccess: true, + DateCreated: mustParseTime("2018-09-20T15:47:52.926Z"), + FirstEvent: time.Time{}, + Platform: nil, + Platforms: []string{}, + Team: &ProjectSummaryTeam{ + ID: "2", + Name: "Powerful Abolitionist", + Slug: "powerful-abolitionist", + }, + Teams: []ProjectSummaryTeam{ + { + ID: "2", + Name: "Powerful Abolitionist", + Slug: "powerful-abolitionist", + }, + }, + }, + { + ID: "4", + Name: "The Spoiled Yoghurt", + Slug: "the-spoiled-yoghurt", + IsBookmarked: false, + IsMember: false, + HasAccess: true, + DateCreated: mustParseTime("2018-09-20T15:48:07.592Z"), + FirstEvent: time.Time{}, + Platform: nil, + Platforms: []string{}, + Team: &ProjectSummaryTeam{ + ID: "2", + Name: "Powerful Abolitionist", + Slug: "powerful-abolitionist", + }, + Teams: []ProjectSummaryTeam{ + { + ID: "2", + Name: "Powerful Abolitionist", + Slug: "powerful-abolitionist", + }, + }, + }, + }, + } + assert.Equal(t, expected, organization) +} + +func TestOrganizationService_Create(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/organizations/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "POST", r) + assertPostJSON(t, map[string]interface{}{ + "name": "The Interstellar Jurisdiction", + "slug": "the-interstellar-jurisdiction", + }, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "name": "The Interstellar Jurisdiction", + "slug": "the-interstellar-jurisdiction", + "id": "2" + }`) + }) + + client := NewClient(httpClient, nil, "") + params := &CreateOrganizationParams{ + Name: "The Interstellar Jurisdiction", + Slug: "the-interstellar-jurisdiction", + } + organization, _, err := client.Organizations.Create(params) + assert.NoError(t, err) + expected := &Organization{ + ID: "2", + Name: "The Interstellar Jurisdiction", + Slug: "the-interstellar-jurisdiction", + } + assert.Equal(t, expected, organization) +} + +func TestOrganizationService_Create_AgreeTerms(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/organizations/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "POST", r) + assertPostJSON(t, map[string]interface{}{ + "name": "The Interstellar Jurisdiction", + "slug": "the-interstellar-jurisdiction", + "agreeTerms": true, + }, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "name": "The Interstellar Jurisdiction", + "slug": "the-interstellar-jurisdiction", + "id": "2" + }`) + }) + + client := NewClient(httpClient, nil, "") + params := &CreateOrganizationParams{ + Name: "The Interstellar Jurisdiction", + Slug: "the-interstellar-jurisdiction", + AgreeTerms: Bool(true), + } + organization, _, err := client.Organizations.Create(params) + assert.NoError(t, err) + expected := &Organization{ + ID: "2", + Name: "The Interstellar Jurisdiction", + Slug: "the-interstellar-jurisdiction", + } + assert.Equal(t, expected, organization) +} + +func TestOrganizationService_Update(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/organizations/badly-misnamed/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "PUT", r) + assertPostJSON(t, map[string]interface{}{ + "name": "Impeccably Designated", + "slug": "impeccably-designated", + }, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "name": "Impeccably Designated", + "slug": "impeccably-designated", + "id": "2" + }`) + }) + + client := NewClient(httpClient, nil, "") + params := &UpdateOrganizationParams{ + Name: "Impeccably Designated", + Slug: "impeccably-designated", + } + organization, _, err := client.Organizations.Update("badly-misnamed", params) + assert.NoError(t, err) + expected := &Organization{ + ID: "2", + Name: "Impeccably Designated", + Slug: "impeccably-designated", + } + assert.Equal(t, expected, organization) +} + +func TestOrganizationService_Delete(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/organizations/the-interstellar-jurisdiction/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "DELETE", r) + }) + + client := NewClient(httpClient, nil, "") + _, err := client.Organizations.Delete("the-interstellar-jurisdiction") + assert.NoError(t, err) +} diff --git a/sentryclient/project_keys.go b/sentryclient/project_keys.go new file mode 100644 index 000000000..4afbfebaf --- /dev/null +++ b/sentryclient/project_keys.go @@ -0,0 +1,113 @@ +package sentryclient + +import ( + "net/http" + "time" + + "github.com/dghubble/sling" +) + +// ProjectKeyRateLimit represents a project key's rate limit. +type ProjectKeyRateLimit struct { + Window int `json:"window"` + Count int `json:"count"` +} + +// ProjectKeyDSN represents a project key's DSN. +type ProjectKeyDSN struct { + Secret string `json:"secret"` + Public string `json:"public"` + CSP string `json:"csp"` + Security string `json:"security"` + Minidump string `json:"minidump"` + CDN string `json:"cdn"` +} + +// ProjectKey represents a client key bound to a project. +// https://github.com/getsentry/sentry/blob/9.0.0/src/sentry/api/serializers/models/project_key.py +type ProjectKey struct { + ID string `json:"id"` + Name string `json:"name"` + Label string `json:"label"` + Public string `json:"public"` + Secret string `json:"secret"` + ProjectID int `json:"projectId"` + IsActive bool `json:"isActive"` + RateLimit *ProjectKeyRateLimit `json:"rateLimit"` + DSN ProjectKeyDSN `json:"dsn"` + DateCreated time.Time `json:"dateCreated"` +} + +// ProjectKeyService provides methods for accessing Sentry project +// client key API endpoints. +// https://docs.sentry.io/api/projects/ +type ProjectKeyService struct { + sling *sling.Sling +} + +func newProjectKeyService(sling *sling.Sling) *ProjectKeyService { + return &ProjectKeyService{ + sling: sling, + } +} + +// List client keys bound to a project. +// https://docs.sentry.io/api/projects/get-project-keys/ +func (s *ProjectKeyService) List(organizationSlug string, projectSlug string) ([]ProjectKey, *http.Response, error) { + projectKeys := new([]ProjectKey) + apiError := new(APIError) + resp, err := s.sling.New().Get("projects/"+organizationSlug+"/"+projectSlug+"/keys/").Receive(projectKeys, apiError) + return *projectKeys, resp, relevantError(err, *apiError) +} + +// CreateProjectKeyParams are the parameters for ProjectKeyService.Create. +type CreateProjectKeyParams struct { + Name string `json:"name,omitempty"` + RateLimit *ProjectKeyRateLimit `json:"rateLimit,omitempty"` +} + +// Create a new client key bound to a project. +// https://docs.sentry.io/api/projects/post-project-keys/ +func (s *ProjectKeyService) Create(organizationSlug string, projectSlug string, params *CreateProjectKeyParams) (*ProjectKey, *http.Response, error) { + projectKey := new(ProjectKey) + apiError := new(APIError) + resp, err := s.sling.New().Post("projects/"+organizationSlug+"/"+projectSlug+"/keys/").BodyJSON(params).Receive(projectKey, apiError) + + if err != nil { + return projectKey, resp, relevantError(err, *apiError) + } + + // Hack as currently the API does not support setting rate limits on Create + if params.RateLimit != nil { + updateParams := &UpdateProjectKeyParams{ + Name: params.Name, + RateLimit: params.RateLimit, + } + projectKey, resp, err = s.Update(organizationSlug, projectSlug, projectKey.ID, updateParams) + } + + return projectKey, resp, relevantError(err, *apiError) +} + +// UpdateProjectKeyParams are the parameters for ProjectKeyService.Update. +type UpdateProjectKeyParams struct { + Name string `json:"name,omitempty"` + RateLimit *ProjectKeyRateLimit `json:"rateLimit,omitempty"` +} + +// Update a client key. +// https://docs.sentry.io/api/projects/put-project-key-details/ +func (s *ProjectKeyService) Update(organizationSlug string, projectSlug string, keyID string, params *UpdateProjectKeyParams) (*ProjectKey, *http.Response, error) { + projectKey := new(ProjectKey) + apiError := new(APIError) + resp, err := s.sling.New().Put("projects/"+organizationSlug+"/"+projectSlug+"/keys/"+keyID+"/").BodyJSON(params).Receive(projectKey, apiError) + return projectKey, resp, relevantError(err, *apiError) +} + +// Delete a project. +// https://docs.sentry.io/api/projects/delete-project-details/ +func (s *ProjectKeyService) Delete(organizationSlug string, projectSlug string, keyID string) (*http.Response, error) { + apiError := new(APIError) + resp, err := s.sling.New().Delete("projects/"+organizationSlug+"/"+projectSlug+"/keys/"+keyID+"/").Receive(nil, apiError) + return resp, relevantError(err, *apiError) +} diff --git a/sentryclient/project_keys_test.go b/sentryclient/project_keys_test.go new file mode 100644 index 000000000..dd5f37d86 --- /dev/null +++ b/sentryclient/project_keys_test.go @@ -0,0 +1,448 @@ +package sentryclient + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestProjectKeyService_List(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/pump-station/keys/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "GET", r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `[{ + "browserSdk": { + "choices": [ + [ + "latest", + "latest" + ], + [ + "4.x", + "4.x" + ] + ] + }, + "browserSdkVersion": "4.x", + "dateCreated": "2018-09-20T15:48:07.397Z", + "dsn": { + "cdn": "https://sentry.io/js-sdk-loader/cfc7b0341c6e4f6ea1a9d256a30dba00.min.js", + "csp": "https://sentry.io/api/2/csp-report/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + "minidump": "https://sentry.io/api/2/minidump/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + "public": "https://cfc7b0341c6e4f6ea1a9d256a30dba00@sentry.io/2", + "secret": "https://cfc7b0341c6e4f6ea1a9d256a30dba00:a07dcd97aa56481f82aeabaed43ca448@sentry.io/2", + "security": "https://sentry.io/api/2/security/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00" + }, + "id": "cfc7b0341c6e4f6ea1a9d256a30dba00", + "isActive": true, + "label": "Fabulous Key", + "name": "Fabulous Key", + "projectId": 2, + "public": "cfc7b0341c6e4f6ea1a9d256a30dba00", + "rateLimit": null, + "secret": "a07dcd97aa56481f82aeabaed43ca448" + }]`) + }) + + client := NewClient(httpClient, nil, "") + projectKeys, _, err := client.ProjectKeys.List("the-interstellar-jurisdiction", "pump-station") + assert.NoError(t, err) + + expected := []ProjectKey{ + { + ID: "cfc7b0341c6e4f6ea1a9d256a30dba00", + Name: "Fabulous Key", + Label: "Fabulous Key", + Public: "cfc7b0341c6e4f6ea1a9d256a30dba00", + Secret: "a07dcd97aa56481f82aeabaed43ca448", + ProjectID: 2, + IsActive: true, + DSN: ProjectKeyDSN{ + Secret: "https://cfc7b0341c6e4f6ea1a9d256a30dba00:a07dcd97aa56481f82aeabaed43ca448@sentry.io/2", + Public: "https://cfc7b0341c6e4f6ea1a9d256a30dba00@sentry.io/2", + CSP: "https://sentry.io/api/2/csp-report/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + Security: "https://sentry.io/api/2/security/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + Minidump: "https://sentry.io/api/2/minidump/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + CDN: "https://sentry.io/js-sdk-loader/cfc7b0341c6e4f6ea1a9d256a30dba00.min.js", + }, + DateCreated: mustParseTime("2018-09-20T15:48:07.397Z"), + }, + } + assert.Equal(t, expected, projectKeys) +} + +func TestProjectKeyService_Create(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/pump-station/keys/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "POST", r) + assertPostJSON(t, map[string]interface{}{ + "name": "Fabulous Key", + }, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "browserSdk": { + "choices": [ + [ + "latest", + "latest" + ], + [ + "4.x", + "4.x" + ] + ] + }, + "browserSdkVersion": "4.x", + "dateCreated": "2018-09-20T15:48:07.397Z", + "dsn": { + "cdn": "https://sentry.io/js-sdk-loader/cfc7b0341c6e4f6ea1a9d256a30dba00.min.js", + "csp": "https://sentry.io/api/2/csp-report/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + "minidump": "https://sentry.io/api/2/minidump/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + "public": "https://cfc7b0341c6e4f6ea1a9d256a30dba00@sentry.io/2", + "secret": "https://cfc7b0341c6e4f6ea1a9d256a30dba00:a07dcd97aa56481f82aeabaed43ca448@sentry.io/2", + "security": "https://sentry.io/api/2/security/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00" + }, + "id": "cfc7b0341c6e4f6ea1a9d256a30dba00", + "isActive": true, + "label": "Fabulous Key", + "name": "Fabulous Key", + "projectId": 2, + "public": "cfc7b0341c6e4f6ea1a9d256a30dba00", + "rateLimit": null, + "secret": "a07dcd97aa56481f82aeabaed43ca448" + }`) + }) + + client := NewClient(httpClient, nil, "") + params := &CreateProjectKeyParams{ + Name: "Fabulous Key", + } + projectKey, _, err := client.ProjectKeys.Create("the-interstellar-jurisdiction", "pump-station", params) + assert.NoError(t, err) + expected := &ProjectKey{ + ID: "cfc7b0341c6e4f6ea1a9d256a30dba00", + Name: "Fabulous Key", + Label: "Fabulous Key", + Public: "cfc7b0341c6e4f6ea1a9d256a30dba00", + Secret: "a07dcd97aa56481f82aeabaed43ca448", + ProjectID: 2, + IsActive: true, + DSN: ProjectKeyDSN{ + Secret: "https://cfc7b0341c6e4f6ea1a9d256a30dba00:a07dcd97aa56481f82aeabaed43ca448@sentry.io/2", + Public: "https://cfc7b0341c6e4f6ea1a9d256a30dba00@sentry.io/2", + CSP: "https://sentry.io/api/2/csp-report/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + Security: "https://sentry.io/api/2/security/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + Minidump: "https://sentry.io/api/2/minidump/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + CDN: "https://sentry.io/js-sdk-loader/cfc7b0341c6e4f6ea1a9d256a30dba00.min.js", + }, + DateCreated: mustParseTime("2018-09-20T15:48:07.397Z"), + } + assert.Equal(t, expected, projectKey) +} + +func TestProjectKeyService_Create_RateLimit(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/pump-station/keys/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "POST", r) + assertPostJSON(t, map[string]interface{}{ + "name": "Fabulous Key", + "rateLimit": map[string]interface{}{ + "window": json.Number("86400"), + "count": json.Number("1000"), + }, + }, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "browserSdk": { + "choices": [ + [ + "latest", + "latest" + ], + [ + "4.x", + "4.x" + ] + ] + }, + "browserSdkVersion": "4.x", + "dateCreated": "2018-09-20T15:48:07.397Z", + "dsn": { + "cdn": "https://sentry.io/js-sdk-loader/cfc7b0341c6e4f6ea1a9d256a30dba00.min.js", + "csp": "https://sentry.io/api/2/csp-report/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + "minidump": "https://sentry.io/api/2/minidump/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + "public": "https://cfc7b0341c6e4f6ea1a9d256a30dba00@sentry.io/2", + "secret": "https://cfc7b0341c6e4f6ea1a9d256a30dba00:a07dcd97aa56481f82aeabaed43ca448@sentry.io/2", + "security": "https://sentry.io/api/2/security/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00" + }, + "id": "cfc7b0341c6e4f6ea1a9d256a30dba00", + "isActive": true, + "label": "Fabulous Key", + "name": "Fabulous Key", + "projectId": 2, + "public": "cfc7b0341c6e4f6ea1a9d256a30dba00", + "rateLimit": null, + "secret": "a07dcd97aa56481f82aeabaed43ca448" + }`) + }) + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/pump-station/keys/cfc7b0341c6e4f6ea1a9d256a30dba00/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "PUT", r) + assertPostJSON(t, map[string]interface{}{ + "name": "Fabulous Key", + "rateLimit": map[string]interface{}{ + "window": json.Number("86400"), + "count": json.Number("1000"), + }, + }, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "browserSdk": { + "choices": [ + [ + "latest", + "latest" + ], + [ + "4.x", + "4.x" + ] + ] + }, + "browserSdkVersion": "4.x", + "dateCreated": "2018-09-20T15:48:07.397Z", + "dsn": { + "cdn": "https://sentry.io/js-sdk-loader/cfc7b0341c6e4f6ea1a9d256a30dba00.min.js", + "csp": "https://sentry.io/api/2/csp-report/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + "minidump": "https://sentry.io/api/2/minidump/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + "public": "https://cfc7b0341c6e4f6ea1a9d256a30dba00@sentry.io/2", + "secret": "https://cfc7b0341c6e4f6ea1a9d256a30dba00:a07dcd97aa56481f82aeabaed43ca448@sentry.io/2", + "security": "https://sentry.io/api/2/security/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00" + }, + "id": "cfc7b0341c6e4f6ea1a9d256a30dba00", + "isActive": true, + "label": "Fabulous Key", + "name": "Fabulous Key", + "projectId": 2, + "public": "cfc7b0341c6e4f6ea1a9d256a30dba00", + "rateLimit": { + "count": 1000, + "window": 86400 + }, + "secret": "a07dcd97aa56481f82aeabaed43ca448" + }`) + }) + + rateLimit := ProjectKeyRateLimit{ + Count: 1000, + Window: 86400, + } + + client := NewClient(httpClient, nil, "") + params := &CreateProjectKeyParams{ + Name: "Fabulous Key", + RateLimit: &rateLimit, + } + projectKey, _, err := client.ProjectKeys.Create("the-interstellar-jurisdiction", "pump-station", params) + assert.NoError(t, err) + expected := &ProjectKey{ + ID: "cfc7b0341c6e4f6ea1a9d256a30dba00", + Name: "Fabulous Key", + Label: "Fabulous Key", + Public: "cfc7b0341c6e4f6ea1a9d256a30dba00", + Secret: "a07dcd97aa56481f82aeabaed43ca448", + ProjectID: 2, + IsActive: true, + RateLimit: &rateLimit, + DSN: ProjectKeyDSN{ + Secret: "https://cfc7b0341c6e4f6ea1a9d256a30dba00:a07dcd97aa56481f82aeabaed43ca448@sentry.io/2", + Public: "https://cfc7b0341c6e4f6ea1a9d256a30dba00@sentry.io/2", + CSP: "https://sentry.io/api/2/csp-report/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + Security: "https://sentry.io/api/2/security/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + Minidump: "https://sentry.io/api/2/minidump/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + CDN: "https://sentry.io/js-sdk-loader/cfc7b0341c6e4f6ea1a9d256a30dba00.min.js", + }, + DateCreated: mustParseTime("2018-09-20T15:48:07.397Z"), + } + assert.Equal(t, expected, projectKey) +} + +func TestProjectKeyService_Update(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/pump-station/keys/befdbf32724c4ae0a3d286717b1f8127/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "PUT", r) + assertPostJSON(t, map[string]interface{}{ + "name": "Fabulous Key", + }, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "browserSdk": { + "choices": [ + [ + "latest", + "latest" + ], + [ + "4.x", + "4.x" + ] + ] + }, + "browserSdkVersion": "4.x", + "dateCreated": "2018-09-20T15:48:07.397Z", + "dsn": { + "cdn": "https://sentry.io/js-sdk-loader/cfc7b0341c6e4f6ea1a9d256a30dba00.min.js", + "csp": "https://sentry.io/api/2/csp-report/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + "minidump": "https://sentry.io/api/2/minidump/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + "public": "https://cfc7b0341c6e4f6ea1a9d256a30dba00@sentry.io/2", + "secret": "https://cfc7b0341c6e4f6ea1a9d256a30dba00:a07dcd97aa56481f82aeabaed43ca448@sentry.io/2", + "security": "https://sentry.io/api/2/security/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00" + }, + "id": "cfc7b0341c6e4f6ea1a9d256a30dba00", + "isActive": true, + "label": "Fabulous Key", + "name": "Fabulous Key", + "projectId": 2, + "public": "cfc7b0341c6e4f6ea1a9d256a30dba00", + "rateLimit": null, + "secret": "a07dcd97aa56481f82aeabaed43ca448" + }`) + }) + + client := NewClient(httpClient, nil, "") + params := &UpdateProjectKeyParams{ + Name: "Fabulous Key", + } + projectKey, _, err := client.ProjectKeys.Update("the-interstellar-jurisdiction", "pump-station", "befdbf32724c4ae0a3d286717b1f8127", params) + assert.NoError(t, err) + expected := &ProjectKey{ + ID: "cfc7b0341c6e4f6ea1a9d256a30dba00", + Name: "Fabulous Key", + Label: "Fabulous Key", + Public: "cfc7b0341c6e4f6ea1a9d256a30dba00", + Secret: "a07dcd97aa56481f82aeabaed43ca448", + ProjectID: 2, + IsActive: true, + DSN: ProjectKeyDSN{ + Secret: "https://cfc7b0341c6e4f6ea1a9d256a30dba00:a07dcd97aa56481f82aeabaed43ca448@sentry.io/2", + Public: "https://cfc7b0341c6e4f6ea1a9d256a30dba00@sentry.io/2", + CSP: "https://sentry.io/api/2/csp-report/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + Security: "https://sentry.io/api/2/security/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + Minidump: "https://sentry.io/api/2/minidump/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + CDN: "https://sentry.io/js-sdk-loader/cfc7b0341c6e4f6ea1a9d256a30dba00.min.js", + }, + DateCreated: mustParseTime("2018-09-20T15:48:07.397Z"), + } + assert.Equal(t, expected, projectKey) +} + +func TestProjectKeyService_Update_RateLimit(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/pump-station/keys/befdbf32724c4ae0a3d286717b1f8127/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "PUT", r) + assertPostJSON(t, map[string]interface{}{ + "name": "Fabulous Key", + "rateLimit": map[string]interface{}{ + "window": json.Number("86400"), + "count": json.Number("1000"), + }, + }, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "browserSdk": { + "choices": [ + [ + "latest", + "latest" + ], + [ + "4.x", + "4.x" + ] + ] + }, + "browserSdkVersion": "4.x", + "dateCreated": "2018-09-20T15:48:07.397Z", + "dsn": { + "cdn": "https://sentry.io/js-sdk-loader/cfc7b0341c6e4f6ea1a9d256a30dba00.min.js", + "csp": "https://sentry.io/api/2/csp-report/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + "minidump": "https://sentry.io/api/2/minidump/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + "public": "https://cfc7b0341c6e4f6ea1a9d256a30dba00@sentry.io/2", + "secret": "https://cfc7b0341c6e4f6ea1a9d256a30dba00:a07dcd97aa56481f82aeabaed43ca448@sentry.io/2", + "security": "https://sentry.io/api/2/security/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00" + }, + "id": "cfc7b0341c6e4f6ea1a9d256a30dba00", + "isActive": true, + "label": "Fabulous Key", + "name": "Fabulous Key", + "projectId": 2, + "public": "cfc7b0341c6e4f6ea1a9d256a30dba00", + "rateLimit": { + "count": 1000, + "window": 86400 + }, + "secret": "a07dcd97aa56481f82aeabaed43ca448" + }`) + }) + + rateLimit := ProjectKeyRateLimit{ + Count: 1000, + Window: 86400, + } + + client := NewClient(httpClient, nil, "") + params := &UpdateProjectKeyParams{ + Name: "Fabulous Key", + RateLimit: &rateLimit, + } + projectKey, _, err := client.ProjectKeys.Update("the-interstellar-jurisdiction", "pump-station", "befdbf32724c4ae0a3d286717b1f8127", params) + assert.NoError(t, err) + expected := &ProjectKey{ + ID: "cfc7b0341c6e4f6ea1a9d256a30dba00", + Name: "Fabulous Key", + Label: "Fabulous Key", + Public: "cfc7b0341c6e4f6ea1a9d256a30dba00", + Secret: "a07dcd97aa56481f82aeabaed43ca448", + ProjectID: 2, + IsActive: true, + RateLimit: &rateLimit, + DSN: ProjectKeyDSN{ + Secret: "https://cfc7b0341c6e4f6ea1a9d256a30dba00:a07dcd97aa56481f82aeabaed43ca448@sentry.io/2", + Public: "https://cfc7b0341c6e4f6ea1a9d256a30dba00@sentry.io/2", + CSP: "https://sentry.io/api/2/csp-report/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + Security: "https://sentry.io/api/2/security/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + Minidump: "https://sentry.io/api/2/minidump/?sentry_key=cfc7b0341c6e4f6ea1a9d256a30dba00", + CDN: "https://sentry.io/js-sdk-loader/cfc7b0341c6e4f6ea1a9d256a30dba00.min.js", + }, + DateCreated: mustParseTime("2018-09-20T15:48:07.397Z"), + } + assert.Equal(t, expected, projectKey) +} + +func TestProjectKeyService_Delete(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/pump-station/keys/befdbf32724c4ae0a3d286717b1f8127/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "DELETE", r) + }) + + client := NewClient(httpClient, nil, "") + _, err := client.ProjectKeys.Delete("the-interstellar-jurisdiction", "pump-station", "befdbf32724c4ae0a3d286717b1f8127") + assert.NoError(t, err) + +} diff --git a/sentryclient/project_plugins.go b/sentryclient/project_plugins.go new file mode 100644 index 000000000..34b56f71f --- /dev/null +++ b/sentryclient/project_plugins.go @@ -0,0 +1,98 @@ +package sentryclient + +import ( + "encoding/json" + "net/http" + + "github.com/dghubble/sling" +) + +// ProjectPlugin represents an asset of a plugin. +type ProjectPluginAsset struct { + URL string `json:"url"` +} + +// ProjectPluginConfig represents the configuration of a plugin. +// Based on https://github.com/getsentry/sentry/blob/96bc1c63df5ec73fe12c136ada11561bf52f1ec9/src/sentry/api/serializers/models/plugin.py#L62-L94. +type ProjectPluginConfig struct { + Name string `json:"name"` + Label string `json:"label"` + Type string `json:"type"` + Required bool `json:"required"` + Help string `json:"help"` + Placeholder string `json:"placeholder"` + Choices json.RawMessage `json:"choices"` + ReadOnly bool `json:"readonly"` + DefaultValue interface{} `json:"defaultValue"` + Value interface{} `json:"value"` +} + +// ProjectPlugin represents a plugin bound to a project. +// Based on https://github.com/getsentry/sentry/blob/96bc1c63df5ec73fe12c136ada11561bf52f1ec9/src/sentry/api/serializers/models/plugin.py#L11. +type ProjectPlugin struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + CanDisable bool `json:"canDisable"` + IsTestable bool `json:"isTestable"` + Metadata map[string]interface{} `json:"metadata"` + Contexts []string `json:"contexts"` + Status string `json:"status"` + Assets []ProjectPluginAsset `json:"assets"` + Doc string `json:"doc"` + Config []ProjectPluginConfig `json:"config"` +} + +// ProjectPluginService provides methods for accessing Sentry project +// plugin API endpoints. +type ProjectPluginService struct { + sling *sling.Sling +} + +func newProjectPluginService(sling *sling.Sling) *ProjectPluginService { + return &ProjectPluginService{ + sling: sling, + } +} + +// List plugins bound to a project. +func (s *ProjectPluginService) List(organizationSlug string, projectSlug string) ([]ProjectPlugin, *http.Response, error) { + projectPlugins := new([]ProjectPlugin) + apiError := new(APIError) + resp, err := s.sling.New().Get("projects/"+organizationSlug+"/"+projectSlug+"/plugins/").Receive(projectPlugins, apiError) + return *projectPlugins, resp, relevantError(err, *apiError) +} + +// Get details of a project plugin. +func (s *ProjectPluginService) Get(organizationSlug string, projectSlug string, id string) (*ProjectPlugin, *http.Response, error) { + projectPlugin := new(ProjectPlugin) + apiError := new(APIError) + resp, err := s.sling.New().Get("projects/"+organizationSlug+"/"+projectSlug+"/plugins/"+id+"/").Receive(projectPlugin, apiError) + return projectPlugin, resp, relevantError(err, *apiError) +} + +// UpdateTeamParams are the parameters for TeamService.Update. +type UpdateProjectPluginParams map[string]interface{} + +// Update settings for a given team. +// https://docs.sentry.io/api/teams/put-team-details/ +func (s *ProjectPluginService) Update(organizationSlug string, projectSlug string, id string, params UpdateProjectPluginParams) (*ProjectPlugin, *http.Response, error) { + projectPlugin := new(ProjectPlugin) + apiError := new(APIError) + resp, err := s.sling.New().Put("projects/"+organizationSlug+"/"+projectSlug+"/plugins/"+id+"/").BodyJSON(params).Receive(projectPlugin, apiError) + return projectPlugin, resp, relevantError(err, *apiError) +} + +// Enable a project plugin. +func (s *ProjectPluginService) Enable(organizationSlug string, projectSlug string, id string) (*http.Response, error) { + apiError := new(APIError) + resp, err := s.sling.New().Post("projects/"+organizationSlug+"/"+projectSlug+"/plugins/"+id+"/").Receive(nil, apiError) + return resp, relevantError(err, *apiError) +} + +// Disable a project plugin. +func (s *ProjectPluginService) Disable(organizationSlug string, projectSlug string, id string) (*http.Response, error) { + apiError := new(APIError) + resp, err := s.sling.New().Delete("projects/"+organizationSlug+"/"+projectSlug+"/plugins/"+id+"/").Receive(nil, apiError) + return resp, relevantError(err, *apiError) +} diff --git a/sentryclient/projects.go b/sentryclient/projects.go new file mode 100644 index 000000000..35086b9f2 --- /dev/null +++ b/sentryclient/projects.go @@ -0,0 +1,165 @@ +package sentryclient + +import ( + "net/http" + "time" + + "github.com/dghubble/sling" +) + +// Project represents a Sentry project. +// https://github.com/getsentry/sentry/blob/9.0.0/src/sentry/api/serializers/models/project.py +type Project struct { + ID string `json:"id"` + Slug string `json:"slug"` + Name string `json:"name"` + + IsPublic bool `json:"isPublic"` + IsBookmarked bool `json:"isBookmarked"` + Color string `json:"color"` + + DateCreated time.Time `json:"dateCreated"` + FirstEvent time.Time `json:"firstEvent"` + + Features []string `json:"features"` + Status string `json:"status"` + Platform string `json:"platform"` + + IsInternal bool `json:"isInternal"` + IsMember bool `json:"isMember"` + HasAccess bool `json:"hasAccess"` + + Avatar Avatar `json:"avatar"` + + // TODO: latestRelease + Options map[string]interface{} `json:"options"` + + DigestsMinDelay int `json:"digestsMinDelay"` + DigestsMaxDelay int `json:"digestsMaxDelay"` + SubjectPrefix string `json:"subjectPrefix"` + AllowedDomains []string `json:"allowedDomains"` + ResolveAge int `json:"resolveAge"` + DataScrubber bool `json:"dataScrubber"` + DataScrubberDefaults bool `json:"dataScrubberDefaults"` + SafeFields []string `json:"safeFields"` + SensitiveFields []string `json:"sensitiveFields"` + SubjectTemplate string `json:"subjectTemplate"` + SecurityToken string `json:"securityToken"` + SecurityTokenHeader *string `json:"securityTokenHeader"` + VerifySSL bool `json:"verifySSL"` + ScrubIPAddresses bool `json:"scrubIPAddresses"` + ScrapeJavaScript bool `json:"scrapeJavaScript"` + + Organization Organization `json:"organization"` + // TODO: plugins + // TODO: platforms + ProcessingIssues int `json:"processingIssues"` + // TODO: defaultEnvironment + + Team Team `json:"team"` + Teams []Team `json:"teams"` +} + +// ProjectSummary represents the summary of a Sentry project. +// https://github.com/getsentry/sentry/blob/9.0.0/src/sentry/api/serializers/models/project.py#L258 +type ProjectSummary struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + IsBookmarked bool `json:"isBookmarked"` + IsMember bool `json:"isMember"` + HasAccess bool `json:"hasAccess"` + + DateCreated time.Time `json:"dateCreated"` + FirstEvent time.Time `json:"firstEvent"` + + Platform *string `json:"platform"` + Platforms []string `json:"platforms"` + + Team *ProjectSummaryTeam `json:"team"` + Teams []ProjectSummaryTeam `json:"teams"` + // TODO: deploys +} + +// ProjectSummaryTeam represents a team in a ProjectSummary. +// https://github.com/getsentry/sentry/blob/9.0.0/src/sentry/api/serializers/models/project.py#L223 +type ProjectSummaryTeam struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` +} + +// ProjectService provides methods for accessing Sentry project API endpoints. +// https://docs.sentry.io/api/projects/ +type ProjectService struct { + sling *sling.Sling +} + +func newProjectService(sling *sling.Sling) *ProjectService { + return &ProjectService{ + sling: sling, + } +} + +// List projects available. +// https://docs.sentry.io/api/projects/get-project-index/ +func (s *ProjectService) List() ([]Project, *http.Response, error) { + projects := new([]Project) + apiError := new(APIError) + resp, err := s.sling.New().Get("projects/").Receive(projects, apiError) + return *projects, resp, relevantError(err, *apiError) +} + +// Get details on an individual project. +// https://docs.sentry.io/api/projects/get-project-details/ +func (s *ProjectService) Get(organizationSlug string, slug string) (*Project, *http.Response, error) { + project := new(Project) + apiError := new(APIError) + resp, err := s.sling.New().Get("projects/"+organizationSlug+"/"+slug+"/").Receive(project, apiError) + return project, resp, relevantError(err, *apiError) +} + +// CreateProjectParams are the parameters for ProjectService.Create. +type CreateProjectParams struct { + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` + Platform string `json:"platform,omitempty"` +} + +// Create a new project bound to a team. +// https://docs.sentry.io/api/teams/post-team-project-index/ +func (s *ProjectService) Create(organizationSlug string, teamSlug string, params *CreateProjectParams) (*Project, *http.Response, error) { + project := new(Project) + apiError := new(APIError) + resp, err := s.sling.New().Post("teams/"+organizationSlug+"/"+teamSlug+"/projects/").BodyJSON(params).Receive(project, apiError) + return project, resp, relevantError(err, *apiError) +} + +// UpdateProjectParams are the parameters for ProjectService.Update. +type UpdateProjectParams struct { + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` + Platform string `json:"platform,omitempty"` + IsBookmarked *bool `json:"isBookmarked,omitempty"` + DigestsMinDelay *int `json:"digestsMinDelay,omitempty"` + DigestsMaxDelay *int `json:"digestsMaxDelay,omitempty"` + Options map[string]interface{} `json:"options,omitempty"` + AllowedDomains []string `json:"allowedDomains,omitempty"` +} + +// Update various attributes and configurable settings for a given project. +// https://docs.sentry.io/api/projects/put-project-details/ +func (s *ProjectService) Update(organizationSlug string, slug string, params *UpdateProjectParams) (*Project, *http.Response, error) { + project := new(Project) + apiError := new(APIError) + resp, err := s.sling.New().Put("projects/"+organizationSlug+"/"+slug+"/").BodyJSON(params).Receive(project, apiError) + return project, resp, relevantError(err, *apiError) +} + +// Delete a project. +// https://docs.sentry.io/api/projects/delete-project-details/ +func (s *ProjectService) Delete(organizationSlug string, slug string) (*http.Response, error) { + apiError := new(APIError) + resp, err := s.sling.New().Delete("projects/"+organizationSlug+"/"+slug+"/").Receive(nil, apiError) + return resp, relevantError(err, *apiError) +} diff --git a/sentryclient/projects_test.go b/sentryclient/projects_test.go new file mode 100644 index 000000000..437e2103d --- /dev/null +++ b/sentryclient/projects_test.go @@ -0,0 +1,578 @@ +package sentryclient + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestProjectService_List(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/projects/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "GET", r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `[{ + "avatar": { + "avatarType": "letter_avatar", + "avatarUuid": null + }, + "color": "#bf6e3f", + "dateCreated": "2018-09-20T15:48:07.592Z", + "features": [ + "data-forwarding", + "rate-limits" + ], + "firstEvent": null, + "hasAccess": true, + "id": "4", + "isBookmarked": false, + "isInternal": false, + "isMember": false, + "isPublic": false, + "name": "The Spoiled Yoghurt", + "organization": { + "avatar": { + "avatarType": "letter_avatar", + "avatarUuid": null + }, + "dateCreated": "2018-09-20T15:47:52.908Z", + "id": "2", + "isEarlyAdopter": false, + "name": "The Interstellar Jurisdiction", + "require2FA": false, + "slug": "the-interstellar-jurisdiction", + "status": { + "id": "active", + "name": "active" + } + }, + "platform": null, + "slug": "the-spoiled-yoghurt", + "status": "active" + }, + { + "avatar": { + "avatarType": "letter_avatar", + "avatarUuid": null + }, + "color": "#bf5b3f", + "dateCreated": "2018-09-20T15:47:56.723Z", + "features": [ + "data-forwarding", + "rate-limits", + "releases" + ], + "firstEvent": null, + "hasAccess": true, + "id": "3", + "isBookmarked": false, + "isInternal": false, + "isMember": false, + "isPublic": false, + "name": "Prime Mover", + "organization": { + "avatar": { + "avatarType": "letter_avatar", + "avatarUuid": null + }, + "dateCreated": "2018-09-20T15:47:52.908Z", + "id": "2", + "isEarlyAdopter": false, + "name": "The Interstellar Jurisdiction", + "require2FA": false, + "slug": "the-interstellar-jurisdiction", + "status": { + "id": "active", + "name": "active" + } + }, + "platform": null, + "slug": "prime-mover", + "status": "active" + }, + { + "avatar": { + "avatarType": "letter_avatar", + "avatarUuid": null + }, + "color": "#3fbf7f", + "dateCreated": "2018-09-20T15:47:52.926Z", + "features": [ + "data-forwarding", + "rate-limits", + "releases" + ], + "firstEvent": null, + "hasAccess": true, + "id": "2", + "isBookmarked": false, + "isInternal": false, + "isMember": false, + "isPublic": false, + "name": "Pump Station", + "organization": { + "avatar": { + "avatarType": "letter_avatar", + "avatarUuid": null + }, + "dateCreated": "2018-09-20T15:47:52.908Z", + "id": "2", + "isEarlyAdopter": false, + "name": "The Interstellar Jurisdiction", + "require2FA": false, + "slug": "the-interstellar-jurisdiction", + "status": { + "id": "active", + "name": "active" + } + }, + "platform": null, + "slug": "pump-station", + "status": "active" + } + ]`) + }) + + client := NewClient(httpClient, nil, "") + projects, _, err := client.Projects.List() + assert.NoError(t, err) + + expectedOrganization := Organization{ + ID: "2", + Slug: "the-interstellar-jurisdiction", + Status: OrganizationStatus{ + ID: "active", + Name: "active", + }, + Name: "The Interstellar Jurisdiction", + DateCreated: mustParseTime("2018-09-20T15:47:52.908Z"), + Avatar: Avatar{ + Type: "letter_avatar", + }, + } + expected := []Project{ + { + ID: "4", + Slug: "the-spoiled-yoghurt", + Name: "The Spoiled Yoghurt", + Color: "#bf6e3f", + DateCreated: mustParseTime("2018-09-20T15:48:07.592Z"), + Features: []string{ + "data-forwarding", + "rate-limits", + }, + Status: "active", + HasAccess: true, + Avatar: Avatar{ + Type: "letter_avatar", + }, + Organization: expectedOrganization, + }, + { + ID: "3", + Slug: "prime-mover", + Name: "Prime Mover", + DateCreated: mustParseTime("2018-09-20T15:47:56.723Z"), + IsPublic: false, + IsBookmarked: false, + Color: "#bf5b3f", + Features: []string{ + "data-forwarding", + "rate-limits", + "releases", + }, + Status: "active", + HasAccess: true, + Avatar: Avatar{ + Type: "letter_avatar", + }, + Organization: expectedOrganization, + }, + { + ID: "2", + Slug: "pump-station", + Name: "Pump Station", + DateCreated: mustParseTime("2018-09-20T15:47:52.926Z"), + IsPublic: false, + IsBookmarked: false, + Color: "#3fbf7f", + Features: []string{ + "data-forwarding", + "rate-limits", + "releases", + }, + Status: "active", + HasAccess: true, + Avatar: Avatar{ + Type: "letter_avatar", + }, + Organization: expectedOrganization, + }, + } + assert.Equal(t, expected, projects) +} + +func TestProjectService_Get(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/pump-station/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "GET", r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "allowedDomains": [ + "*" + ], + "avatar": { + "avatarType": "letter_avatar", + "avatarUuid": null + }, + "color": "#3fbf7f", + "dataScrubber": true, + "dataScrubberDefaults": true, + "dateCreated": "2018-10-02T14:19:09.864Z", + "defaultEnvironment": null, + "digestsMaxDelay": 1800, + "digestsMinDelay": 300, + "features": [ + "data-forwarding", + "rate-limits", + "releases" + ], + "firstEvent": null, + "hasAccess": true, + "id": "2", + "isBookmarked": false, + "isInternal": false, + "isMember": false, + "isPublic": false, + "latestRelease": { + "authors": [], + "commitCount": 0, + "data": {}, + "dateCreated": "2018-10-02T14:19:25.397Z", + "dateReleased": null, + "deployCount": 0, + "firstEvent": null, + "lastCommit": null, + "lastDeploy": null, + "lastEvent": null, + "newGroups": 0, + "owner": null, + "projects": [{ + "name": "Pump Station", + "slug": "pump-station" + }], + "ref": "6ba09a7c53235ee8a8fa5ee4c1ca8ca886e7fdbb", + "shortVersion": "2.0rc2", + "url": null, + "version": "2.0rc2" + }, + "name": "Pump Station", + "options": { + "feedback:branding": true, + "filters:blacklisted_ips": "", + "filters:error_messages": "", + "filters:releases": "", + "sentry:csp_ignored_sources": "", + "sentry:csp_ignored_sources_defaults": true, + "sentry:reprocessing_active": false + }, + "organization": { + "avatar": { + "avatarType": "letter_avatar", + "avatarUuid": null + }, + "dateCreated": "2018-10-02T14:19:09.817Z", + "id": "2", + "isEarlyAdopter": false, + "name": "The Interstellar Jurisdiction", + "require2FA": false, + "slug": "the-interstellar-jurisdiction", + "status": { + "id": "active", + "name": "active" + } + }, + "platform": null, + "platforms": [], + "plugins": [{ + "assets": [], + "author": { + "name": "Sentry Team", + "url": "https://github.com/getsentry/sentry" + }, + "canDisable": true, + "contexts": [], + "description": "Integrates web hooks.", + "doc": "", + "enabled": false, + "hasConfiguration": true, + "id": "webhooks", + "isTestable": true, + "metadata": {}, + "name": "WebHooks", + "resourceLinks": [{ + "title": "Bug Tracker", + "url": "https://github.com/getsentry/sentry/issues" + }, + { + "title": "Source", + "url": "https://github.com/getsentry/sentry" + } + ], + "shortName": "WebHooks", + "slug": "webhooks", + "status": "unknown", + "type": "notification", + "version": "9.1.0.dev0" + }], + "processingIssues": 0, + "relayPiiConfig": null, + "resolveAge": 0, + "safeFields": [], + "scrapeJavaScript": true, + "scrubIPAddresses": false, + "securityToken": "320e3180c64e11e8b61e0242ac110002", + "securityTokenHeader": null, + "sensitiveFields": [], + "slug": "pump-station", + "status": "active", + "storeCrashReports": false, + "subjectPrefix": "[Sentry] ", + "subjectTemplate": "$shortID - $title", + "team": { + "id": "2", + "name": "Powerful Abolitionist", + "slug": "powerful-abolitionist" + }, + "teams": [{ + "id": "2", + "name": "Powerful Abolitionist", + "slug": "powerful-abolitionist" + }], + "verifySSL": false + }`) + }) + + client := NewClient(httpClient, nil, "") + project, _, err := client.Projects.Get("the-interstellar-jurisdiction", "pump-station") + assert.NoError(t, err) + expected := &Project{ + ID: "2", + Slug: "pump-station", + Name: "Pump Station", + Color: "#3fbf7f", + DateCreated: mustParseTime("2018-10-02T14:19:09.864Z"), + Features: []string{ + "data-forwarding", + "rate-limits", + "releases", + }, + Status: "active", + HasAccess: true, + Avatar: Avatar{ + Type: "letter_avatar", + }, + Options: map[string]interface{}{ + "feedback:branding": true, + "filters:blacklisted_ips": "", + "filters:error_messages": "", + "filters:releases": "", + "sentry:csp_ignored_sources": "", + "sentry:csp_ignored_sources_defaults": true, + "sentry:reprocessing_active": false, + }, + DigestsMinDelay: 300, + DigestsMaxDelay: 1800, + SubjectPrefix: "[Sentry] ", + AllowedDomains: []string{"*"}, + DataScrubber: true, + DataScrubberDefaults: true, + SafeFields: []string{}, + SensitiveFields: []string{}, + SubjectTemplate: "$shortID - $title", + SecurityToken: "320e3180c64e11e8b61e0242ac110002", + ScrapeJavaScript: true, + Organization: Organization{ + ID: "2", + Slug: "the-interstellar-jurisdiction", + Status: OrganizationStatus{ + ID: "active", + Name: "active", + }, + Name: "The Interstellar Jurisdiction", + DateCreated: mustParseTime("2018-10-02T14:19:09.817Z"), + Avatar: Avatar{ + Type: "letter_avatar", + }, + }, + Team: Team{ + ID: "2", + Slug: "powerful-abolitionist", + Name: "Powerful Abolitionist", + }, + Teams: []Team{ + { + ID: "2", + Slug: "powerful-abolitionist", + Name: "Powerful Abolitionist", + }, + }, + } + assert.Equal(t, expected, project) +} + +func TestProjectService_Create(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/teams/the-interstellar-jurisdiction/powerful-abolitionist/projects/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "POST", r) + assertPostJSON(t, map[string]interface{}{ + "name": "The Spoiled Yoghurt", + }, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "status": "active", + "slug": "the-spoiled-yoghurt", + "defaultEnvironment": null, + "features": [ + "data-forwarding", + "rate-limits" + ], + "color": "#bf6e3f", + "isPublic": false, + "dateCreated": "2017-07-18T19:29:44.996Z", + "platforms": [], + "callSign": "THE-SPOILED-YOGHURT", + "firstEvent": null, + "processingIssues": 0, + "isBookmarked": false, + "callSignReviewed": false, + "id": "4", + "name": "The Spoiled Yoghurt" + }`) + }) + + client := NewClient(httpClient, nil, "") + params := &CreateProjectParams{ + Name: "The Spoiled Yoghurt", + } + project, _, err := client.Projects.Create("the-interstellar-jurisdiction", "powerful-abolitionist", params) + assert.NoError(t, err) + + expected := &Project{ + ID: "4", + Slug: "the-spoiled-yoghurt", + Name: "The Spoiled Yoghurt", + DateCreated: mustParseTime("2017-07-18T19:29:44.996Z"), + IsPublic: false, + IsBookmarked: false, + Color: "#bf6e3f", + Features: []string{ + "data-forwarding", + "rate-limits", + }, + Status: "active", + } + assert.Equal(t, expected, project) +} + +func TestProjectService_Update(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/plain-proxy/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "PUT", r) + assertPostJSON(t, map[string]interface{}{ + "name": "Plane Proxy", + "slug": "plane-proxy", + "options": map[string]interface{}{ + "sentry:origins": "http://example.com\nhttp://example.invalid", + }, + }, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "status": "active", + "digestsMinDelay": 300, + "options": { + "sentry:origins": "http://example.com\nhttp://example.invalid", + "sentry:resolve_age": 0 + }, + "defaultEnvironment": null, + "features": [ + "data-forwarding", + "rate-limits", + "releases" + ], + "subjectPrefix": null, + "color": "#bf803f", + "slug": "plane-proxy", + "isPublic": false, + "dateCreated": "2017-07-18T19:30:09.751Z", + "platforms": [], + "callSign": "PLANE-PROXY", + "firstEvent": null, + "digestsMaxDelay": 1800, + "processingIssues": 0, + "isBookmarked": false, + "callSignReviewed": false, + "id": "5", + "subjectTemplate": "[$project] ${tag:level}: $title", + "name": "Plane Proxy" + }`) + }) + + client := NewClient(httpClient, nil, "") + params := &UpdateProjectParams{ + Name: "Plane Proxy", + Slug: "plane-proxy", + Options: map[string]interface{}{ + "sentry:origins": "http://example.com\nhttp://example.invalid", + }, + } + project, _, err := client.Projects.Update("the-interstellar-jurisdiction", "plain-proxy", params) + assert.NoError(t, err) + expected := &Project{ + ID: "5", + Slug: "plane-proxy", + Name: "Plane Proxy", + DateCreated: mustParseTime("2017-07-18T19:30:09.751Z"), + IsPublic: false, + IsBookmarked: false, + Color: "#bf803f", + Features: []string{ + "data-forwarding", + "rate-limits", + "releases", + }, + Status: "active", + Options: map[string]interface{}{ + "sentry:origins": "http://example.com\nhttp://example.invalid", + "sentry:resolve_age": float64(0), + }, + DigestsMinDelay: 300, + DigestsMaxDelay: 1800, + SubjectTemplate: "[$project] ${tag:level}: $title", + } + assert.Equal(t, expected, project) +} + +func TestProjectService_Delete(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/plain-proxy/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "DELETE", r) + }) + + client := NewClient(httpClient, nil, "") + _, err := client.Projects.Delete("the-interstellar-jurisdiction", "plain-proxy") + assert.NoError(t, err) + +} diff --git a/sentryclient/rules.go b/sentryclient/rules.go new file mode 100644 index 000000000..a54a3db45 --- /dev/null +++ b/sentryclient/rules.go @@ -0,0 +1,113 @@ +package sentryclient + +import ( + "net/http" + "time" + + "github.com/dghubble/sling" +) + +// Rule represents an alert rule configured for this project. +// https://github.com/getsentry/sentry/blob/9.0.0/src/sentry/api/serializers/models/rule.py +type Rule struct { + ID string `json:"id"` + ActionMatch string `json:"actionMatch"` + Environment string `json:"environment"` + Frequency int `json:"frequency"` + Name string `json:"name"` + Conditions []RuleCondition `json:"conditions"` + Actions []RuleAction `json:"actions"` + Created time.Time `json:"dateCreated"` +} + +// RuleCondition represents the conditions for each rule. +// https://github.com/getsentry/sentry/blob/9.0.0/src/sentry/api/serializers/models/rule.py +type RuleCondition struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// RuleAction represents the actions will be taken for each rule based on its conditions. +// https://github.com/getsentry/sentry/blob/9.0.0/src/sentry/api/serializers/models/rule.py +type RuleAction struct { + ID string `json:"id"` + Name string `json:"name"` + Tags string `json:"tags"` + ChannelID string `json:"channel_id"` + Channel string `json:"channel"` + Workspace string `json:"workspace"` +} + +// ProjectKeyService provides methods for accessing Sentry project +// client key API endpoints. +// https://docs.sentry.io/api/projects/ +type RuleService struct { + sling *sling.Sling +} + +func newRuleService(sling *sling.Sling) *RuleService { + return &RuleService{ + sling: sling, + } +} + +// List alert rules configured for a project. +func (s *RuleService) List(organizationSlug string, projectSlug string) ([]Rule, *http.Response, error) { + rules := new([]Rule) + apiError := new(APIError) + resp, err := s.sling.New().Get("projects/"+organizationSlug+"/"+projectSlug+"/rules/").Receive(rules, apiError) + return *rules, resp, relevantError(err, *apiError) +} + +// CreateRuleParams are the parameters for RuleService.Create. +type CreateRuleParams struct { + ActionMatch string `json:"actionMatch"` + Environment string `json:"environment,omitempty"` + Frequency int `json:"frequency"` + Name string `json:"name"` + Conditions []*CreateRuleConditionParams `json:"conditions"` + Actions []*CreateRuleActionParams `json:"actions"` +} + +// CreateRuleActionParams models the actions when creating the action for the rule. +type CreateRuleActionParams struct { + ID string `json:"id"` + Tags string `json:"tags"` + Channel string `json:"channel"` + Workspace string `json:"workspace"` + Action string `json:"action,omitempty"` + Service string `json:"service,omitempty"` +} + +// CreateRuleConditionParams models the conditions when creating the action for the rule. +type CreateRuleConditionParams struct { + ID string `json:"id"` + Attribute string `json:"attribute,omitempty"` + Match string `json:"match,omitempty"` + Value string `json:"value,omitempty"` + Key string `json:"key,omitempty"` + Interval string `json:"interval,omitempty"` +} + +// Create a new alert rule bound to a project. +func (s *RuleService) Create(organizationSlug string, projectSlug string, params *CreateRuleParams) (*Rule, *http.Response, error) { + rule := new(Rule) + apiError := new(APIError) + resp, err := s.sling.New().Post("projects/"+organizationSlug+"/"+projectSlug+"/rules/").BodyJSON(params).Receive(rule, apiError) + return rule, resp, relevantError(err, *apiError) +} + +// Update a rule. +func (s *RuleService) Update(organizationSlug string, projectSlug string, ruleID string, params *Rule) (*Rule, *http.Response, error) { + rule := new(Rule) + apiError := new(APIError) + resp, err := s.sling.New().Put("projects/"+organizationSlug+"/"+projectSlug+"/rules/"+ruleID+"/").BodyJSON(params).Receive(rule, apiError) + return rule, resp, relevantError(err, *apiError) +} + +// Delete a rule. +func (s *RuleService) Delete(organizationSlug string, projectSlug string, ruleID string) (*http.Response, error) { + apiError := new(APIError) + resp, err := s.sling.New().Delete("projects/"+organizationSlug+"/"+projectSlug+"/rules/"+ruleID+"/").Receive(nil, apiError) + return resp, relevantError(err, *apiError) +} diff --git a/sentryclient/rules_test.go b/sentryclient/rules_test.go new file mode 100644 index 000000000..634343c7e --- /dev/null +++ b/sentryclient/rules_test.go @@ -0,0 +1,297 @@ +package sentryclient + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRulesService_List(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/pump-station/rules/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "GET", r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `[ + { + "environment": "production", + "actionMatch": "any", + "frequency": 30, + "name": "Notify errors", + "conditions": [ + { + "id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition", + "name": "An issue is first seen" + } + ], + "id": "123456", + "actions": [ + { + "name": "Send a notification to the Dummy Slack workspace to #dummy-channel and show tags [environment] in notification", + "tags": "environment", + "channel_id": "XX00X0X0X", + "workspace": "1234", + "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", + "channel": "#dummy-channel" + } + ], + "dateCreated": "2019-08-24T18:12:16.321Z" + } + ]`) + + client := NewClient(httpClient, nil, "") + rules, _, err := client.Rules.List("the-interstellar-jurisdiction", "pump-station") + assert.NoError(t, err) + + expected := []Rule{ + { + ID: "123456", + ActionMatch: "all", + Environment: "production", + Frequency: 30, + Name: "Notify errors", + Conditions: []RuleCondition{ + { + ID: "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition", + Name: "An issue is first seen", + }, + }, + Actions: []RuleAction{ + { + ID: "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", + Name: "Send a notification to the Dummy Slack workspace to #dummy-channel and show tags [environment] in notification", + Tags: "environment", + ChannelID: "XX00X0X0X", + Channel: "#dummy-channel", + Workspace: "1234", + }, + }, + Created: mustParseTime("2019-08-24T18:12:16.321Z"), + }, + } + assert.Equal(t, expected, rules) + }) +} + +func TestRulesService_Create(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/pump-station/rules/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "POST", r) + assertPostJSON(t, map[string]interface{}{ + "actionMatch": "all", + "environment": "production", + "frequency": 30, + "name": "Notify errors", + "conditions": []map[string]interface{}{ + {"ID": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"}, + }, + "actions": []map[string]interface{}{ + { + "ID": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", + "Tags": "environment", + "Channel": "#dummy-channel", + "Workspace": "1234", + }, + }, + }, r) + + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "environment": "production", + "actionMatch": "any", + "frequency": 30, + "name": "Notify errors", + "conditions": [ + { + "id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition", + "name": "An issue is first seen" + } + ], + "id": "123456", + "actions": [ + { + "name": "Send a notification to the Dummy Slack workspace to #dummy-channel and show tags [environment] in notification", + "tags": "environment", + "channel_id": "XX00X0X0X", + "workspace": "1234", + "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", + "channel": "#dummy-channel" + } + ], + "dateCreated": "2019-08-24T18:12:16.321Z" + }`) + + params := &CreateRuleParams{ + ActionMatch: "all", + Environment: "production", + Frequency: 30, + Name: "Notify errors", + Conditions: []*CreateRuleConditionParams{ + {ID: "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"}, + }, + Actions: []*CreateRuleActionParams{ + { + ID: "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", + Tags: "environment", + Channel: "#dummy-channel", + Workspace: "1234", + }, + }, + } + + client := NewClient(httpClient, nil, "") + rules, _, err := client.Rules.Create("the-interstellar-jurisdiction", "pump-station", params) + assert.NoError(t, err) + + expected := Rule{ + ID: "123456", + ActionMatch: "all", + Environment: "production", + Frequency: 30, + Name: "Notify errors", + Conditions: []RuleCondition{ + { + ID: "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition", + Name: "An issue is first seen", + }, + }, + Actions: []RuleAction{ + { + ID: "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", + Name: "Send a notification to the Dummy Slack workspace to #dummy-channel and show tags [environment] in notification", + Tags: "environment", + ChannelID: "XX00X0X0X", + Channel: "#dummy-channel", + Workspace: "1234", + }, + }, + Created: mustParseTime("2019-08-24T18:12:16.321Z"), + } + assert.Equal(t, expected, rules) + }) +} + +func TestRulesService_Update(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/pump-station/rules/12345/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "POST", r) + assertPostJSON(t, map[string]interface{}{ + "actionMatch": "all", + "environment": "staging", + "frequency": 30, + "name": "Notify errors", + "conditions": []map[string]interface{}{ + {"ID": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"}, + }, + "actions": []map[string]interface{}{ + { + "ID": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", + "Tags": "environment", + "Channel": "#dummy-channel", + "Workspace": "1234", + }, + }, + }, r) + + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "environment": "staging", + "actionMatch": "any", + "frequency": 30, + "name": "Notify errors", + "conditions": [ + { + "id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition", + "name": "An issue is first seen" + } + ], + "id": "123456", + "actions": [ + { + "name": "Send a notification to the Dummy Slack workspace to #dummy-channel and show tags [environment] in notification", + "tags": "environment", + "channel_id": "XX00X0X0X", + "workspace": "1234", + "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", + "channel": "#dummy-channel" + } + ], + "dateCreated": "2019-08-24T18:12:16.321Z" + }`) + + params := &Rule{ + ID: "123456", + ActionMatch: "all", + Environment: "staging", + Frequency: 30, + Name: "Notify errors", + Conditions: []RuleCondition{ + { + ID: "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition", + Name: "An issue is first seen", + }, + }, + Actions: []RuleAction{ + { + ID: "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", + Name: "Send a notification to the Dummy Slack workspace to #dummy-channel and show tags [environment] in notification", + Tags: "environment", + ChannelID: "XX00X0X0X", + Channel: "#dummy-channel", + Workspace: "1234", + }, + }, + Created: mustParseTime("2019-08-24T18:12:16.321Z"), + } + + client := NewClient(httpClient, nil, "") + rules, _, err := client.Rules.Update("the-interstellar-jurisdiction", "pump-station", "12345", params) + assert.NoError(t, err) + + expected := Rule{ + ID: "123456", + ActionMatch: "all", + Environment: "production", + Frequency: 30, + Name: "Notify errors", + Conditions: []RuleCondition{ + { + ID: "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition", + Name: "An issue is first seen", + }, + }, + Actions: []RuleAction{ + { + ID: "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", + Name: "Send a notification to the Dummy Slack workspace to #dummy-channel and show tags [environment] in notification", + Tags: "environment", + ChannelID: "XX00X0X0X", + Channel: "#dummy-channel", + Workspace: "1234", + }, + }, + Created: mustParseTime("2019-08-24T18:12:16.321Z"), + } + assert.Equal(t, expected, rules) + }) +} + +func TestRuleService_Delete(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/pump-station/rules/12345/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "DELETE", r) + }) + + client := NewClient(httpClient, nil, "") + _, err := client.Rules.Delete("the-interstellar-jurisdiction", "pump-station", "12345") + assert.NoError(t, err) +} diff --git a/sentryclient/sentry.go b/sentryclient/sentry.go new file mode 100644 index 000000000..c8af35e33 --- /dev/null +++ b/sentryclient/sentry.go @@ -0,0 +1,61 @@ +package sentryclient + +import ( + "net/http" + "net/url" + "path" + + "github.com/dghubble/sling" +) + +const ( + DefaultBaseURL = "https://sentry.io/api/" + APIVersion = "0" +) + +type Client struct { + sling *sling.Sling + Organizations *OrganizationService + Teams *TeamService + Projects *ProjectService + ProjectKeys *ProjectKeyService + ProjectPlugins *ProjectPluginService + Rules *RuleService +} + +// NewClient returns a new Sentry API client. +// If a nil httpClient is given, the http.DefaultClient will be used. +// If a nil baseURL is given, the DefaultBaseURL will be used. +func NewClient(httpClient *http.Client, baseURL *url.URL, token string) *Client { + if httpClient == nil { + httpClient = http.DefaultClient + } + + if baseURL == nil { + baseURL, _ = url.Parse(DefaultBaseURL) + } + baseURL.Path = path.Join(baseURL.Path, APIVersion) + "/" + + base := sling.New().Base(baseURL.String()).Client(httpClient) + + if token != "" { + base.Add("Authorization", "Bearer "+token) + } + + c := &Client{ + sling: base, + Organizations: newOrganizationService(base.New()), + Teams: newTeamService(base.New()), + Projects: newProjectService(base.New()), + ProjectKeys: newProjectKeyService(base.New()), + ProjectPlugins: newProjectPluginService(base.New()), + Rules: newRuleService(base.New()), + } + return c +} + +// Avatar represents an avatar. +type Avatar struct { + UUID *string `json:"avatarUuid"` + Type string `json:"avatarType"` +} diff --git a/sentryclient/sentry_test.go b/sentryclient/sentry_test.go new file mode 100644 index 000000000..3727c3e97 --- /dev/null +++ b/sentryclient/sentry_test.go @@ -0,0 +1,79 @@ +package sentryclient + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "encoding/json" + + "github.com/stretchr/testify/assert" +) + +// testServer returns an http Client, ServeMux, and Server. The client proxies +// requests to the server and handlers can be registered on the mux to handle +// requests. The caller must close the test server. +func testServer() (*http.Client, *http.ServeMux, *httptest.Server) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + transport := &RewriteTransport{&http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + }} + client := &http.Client{Transport: transport} + return client, mux, server +} + +// RewriteTransport rewrites https requests to http to avoid TLS cert issues +// during testing. +type RewriteTransport struct { + Transport http.RoundTripper +} + +// RoundTrip rewrites the request scheme to http and calls through to the +// composed RoundTripper or if it is nil, to the http.DefaultTransport. +func (t *RewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req.URL.Scheme = "http" + if t.Transport == nil { + return http.DefaultTransport.RoundTrip(req) + } + return t.Transport.RoundTrip(req) +} + +func assertMethod(t *testing.T, expectedMethod string, req *http.Request) { + assert.Equal(t, expectedMethod, req.Method) +} + +// assertQuery tests that the Request has the expected url query key/val pairs +func assertQuery(t *testing.T, expected map[string]string, req *http.Request) { + queryValues := req.URL.Query() + expectedValues := url.Values{} + for key, value := range expected { + expectedValues.Add(key, value) + } + assert.Equal(t, expectedValues, queryValues) +} + +// assertPostJSON tests that the Request has the expected JSON in its Body +func assertPostJSON(t *testing.T, expected interface{}, req *http.Request) { + var actual interface{} + + d := json.NewDecoder(req.Body) + d.UseNumber() + + err := d.Decode(&actual) + assert.NoError(t, err) + assert.EqualValues(t, expected, actual) +} + +func mustParseTime(value string) time.Time { + t, err := time.Parse(time.RFC3339, value) + if err != nil { + panic(fmt.Sprintf("mustParseTime: %s", err)) + } + return t +} diff --git a/sentryclient/teams.go b/sentryclient/teams.go new file mode 100644 index 000000000..10d2c7d24 --- /dev/null +++ b/sentryclient/teams.go @@ -0,0 +1,89 @@ +package sentryclient + +import ( + "net/http" + "time" + + "github.com/dghubble/sling" +) + +// Team represents a Sentry team that is bound to an organization. +// https://github.com/getsentry/sentry/blob/9.0.0/src/sentry/api/serializers/models/team.py#L48 +type Team struct { + ID string `json:"id"` + Slug string `json:"slug"` + Name string `json:"name"` + DateCreated time.Time `json:"dateCreated"` + IsMember bool `json:"isMember"` + HasAccess bool `json:"hasAccess"` + IsPending bool `json:"isPending"` + Avatar Avatar `json:"avatar"` +} + +// TeamService provides methods for accessing Sentry team API endpoints. +// https://docs.sentry.io/api/teams/ +type TeamService struct { + sling *sling.Sling +} + +func newTeamService(sling *sling.Sling) *TeamService { + return &TeamService{ + sling: sling, + } +} + +// List returns a list of teams bound to an organization. +// https://docs.sentry.io/api/teams/get-organization-teams/ +func (s *TeamService) List(organizationSlug string) ([]Team, *http.Response, error) { + teams := new([]Team) + apiError := new(APIError) + resp, err := s.sling.New().Get("organizations/"+organizationSlug+"/teams/").Receive(teams, apiError) + return *teams, resp, relevantError(err, *apiError) +} + +// Get details on an individual team of an organization. +// https://docs.sentry.io/api/teams/get-team-details/ +func (s *TeamService) Get(organizationSlug string, slug string) (*Team, *http.Response, error) { + team := new(Team) + apiError := new(APIError) + resp, err := s.sling.New().Get("teams/"+organizationSlug+"/"+slug+"/").Receive(team, apiError) + return team, resp, relevantError(err, *apiError) +} + +// CreateTeamParams are the parameters for TeamService.Create. +type CreateTeamParams struct { + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` +} + +// Create a new Sentry team bound to an organization. +// https://docs.sentry.io/api/teams/post-organization-teams/ +func (s *TeamService) Create(organizationSlug string, params *CreateTeamParams) (*Team, *http.Response, error) { + team := new(Team) + apiError := new(APIError) + resp, err := s.sling.New().Post("organizations/"+organizationSlug+"/teams/").BodyJSON(params).Receive(team, apiError) + return team, resp, relevantError(err, *apiError) +} + +// UpdateTeamParams are the parameters for TeamService.Update. +type UpdateTeamParams struct { + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` +} + +// Update settings for a given team. +// https://docs.sentry.io/api/teams/put-team-details/ +func (s *TeamService) Update(organizationSlug string, slug string, params *UpdateTeamParams) (*Team, *http.Response, error) { + team := new(Team) + apiError := new(APIError) + resp, err := s.sling.New().Put("teams/"+organizationSlug+"/"+slug+"/").BodyJSON(params).Receive(team, apiError) + return team, resp, relevantError(err, *apiError) +} + +// Delete a team. +// https://docs.sentry.io/api/teams/delete-team-details/ +func (s *TeamService) Delete(organizationSlug string, slug string) (*http.Response, error) { + apiError := new(APIError) + resp, err := s.sling.New().Delete("teams/"+organizationSlug+"/"+slug+"/").Receive(nil, apiError) + return resp, relevantError(err, *apiError) +} diff --git a/sentryclient/teams_test.go b/sentryclient/teams_test.go new file mode 100644 index 000000000..2c22cc877 --- /dev/null +++ b/sentryclient/teams_test.go @@ -0,0 +1,268 @@ +package sentryclient + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTeamService_List(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/organizations/the-interstellar-jurisdiction/teams/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "GET", r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `[ + { + "slug": "ancient-gabelers", + "name": "Ancient Gabelers", + "hasAccess": true, + "isPending": false, + "dateCreated": "2017-07-18T19:29:46.305Z", + "isMember": false, + "id": "3", + "projects": [] + }, + { + "slug": "powerful-abolitionist", + "name": "Powerful Abolitionist", + "hasAccess": true, + "isPending": false, + "dateCreated": "2017-07-18T19:29:24.743Z", + "isMember": false, + "id": "2", + "projects": [ + { + "status": "active", + "slug": "prime-mover", + "defaultEnvironment": null, + "features": [ + "data-forwarding", + "rate-limits", + "releases" + ], + "color": "#bf5b3f", + "isPublic": false, + "dateCreated": "2017-07-18T19:29:30.063Z", + "platforms": [], + "callSign": "PRIME-MOVER", + "firstEvent": null, + "processingIssues": 0, + "isBookmarked": false, + "callSignReviewed": false, + "id": "3", + "name": "Prime Mover" + }, + { + "status": "active", + "slug": "pump-station", + "defaultEnvironment": null, + "features": [ + "data-forwarding", + "rate-limits", + "releases" + ], + "color": "#3fbf7f", + "isPublic": false, + "dateCreated": "2017-07-18T19:29:24.793Z", + "platforms": [], + "callSign": "PUMP-STATION", + "firstEvent": null, + "processingIssues": 0, + "isBookmarked": false, + "callSignReviewed": false, + "id": "2", + "name": "Pump Station" + }, + { + "status": "active", + "slug": "the-spoiled-yoghurt", + "defaultEnvironment": null, + "features": [ + "data-forwarding", + "rate-limits" + ], + "color": "#bf6e3f", + "isPublic": false, + "dateCreated": "2017-07-18T19:29:44.996Z", + "platforms": [], + "callSign": "THE-SPOILED-YOGHURT", + "firstEvent": null, + "processingIssues": 0, + "isBookmarked": false, + "callSignReviewed": false, + "id": "4", + "name": "The Spoiled Yoghurt" + } + ] + } + ]`) + }) + + client := NewClient(httpClient, nil, "") + teams, _, err := client.Teams.List("the-interstellar-jurisdiction") + assert.NoError(t, err) + + expected := []Team{ + { + ID: "3", + Slug: "ancient-gabelers", + Name: "Ancient Gabelers", + DateCreated: mustParseTime("2017-07-18T19:29:46.305Z"), + HasAccess: true, + IsPending: false, + IsMember: false, + }, + { + ID: "2", + Slug: "powerful-abolitionist", + Name: "Powerful Abolitionist", + DateCreated: mustParseTime("2017-07-18T19:29:24.743Z"), + HasAccess: true, + IsPending: false, + IsMember: false, + }, + } + assert.Equal(t, expected, teams) +} + +func TestTeamService_Get(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/teams/the-interstellar-jurisdiction/powerful-abolitionist/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "GET", r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "slug": "powerful-abolitionist", + "name": "Powerful Abolitionist", + "hasAccess": true, + "isPending": false, + "dateCreated": "2017-07-18T19:29:24.743Z", + "isMember": false, + "organization": { + "name": "The Interstellar Jurisdiction", + "slug": "the-interstellar-jurisdiction", + "avatar": { + "avatarUuid": null, + "avatarType": "letter_avatar" + }, + "dateCreated": "2017-07-18T19:29:24.565Z", + "id": "2", + "isEarlyAdopter": false + }, + "id": "2" + }`) + }) + + client := NewClient(httpClient, nil, "") + team, _, err := client.Teams.Get("the-interstellar-jurisdiction", "powerful-abolitionist") + assert.NoError(t, err) + + expected := &Team{ + ID: "2", + Slug: "powerful-abolitionist", + Name: "Powerful Abolitionist", + DateCreated: mustParseTime("2017-07-18T19:29:24.743Z"), + HasAccess: true, + IsPending: false, + IsMember: false, + } + assert.Equal(t, expected, team) +} + +func TestTeamService_Create(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/organizations/the-interstellar-jurisdiction/teams/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "POST", r) + assertPostJSON(t, map[string]interface{}{ + "name": "Ancient Gabelers", + }, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "slug": "ancient-gabelers", + "name": "Ancient Gabelers", + "hasAccess": true, + "isPending": false, + "dateCreated": "2017-07-18T19:29:46.305Z", + "isMember": false, + "id": "3" + }`) + }) + + client := NewClient(httpClient, nil, "") + params := &CreateTeamParams{ + Name: "Ancient Gabelers", + } + team, _, err := client.Teams.Create("the-interstellar-jurisdiction", params) + assert.NoError(t, err) + + expected := &Team{ + ID: "3", + Slug: "ancient-gabelers", + Name: "Ancient Gabelers", + DateCreated: mustParseTime("2017-07-18T19:29:46.305Z"), + HasAccess: true, + IsPending: false, + IsMember: false, + } + assert.Equal(t, expected, team) +} + +func TestTeamService_Update(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/teams/the-interstellar-jurisdiction/the-obese-philosophers/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "PUT", r) + assertPostJSON(t, map[string]interface{}{ + "name": "The Inflated Philosophers", + }, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "slug": "the-obese-philosophers", + "name": "The Inflated Philosophers", + "hasAccess": true, + "isPending": false, + "dateCreated": "2017-07-18T19:30:14.736Z", + "isMember": false, + "id": "4" + }`) + }) + + client := NewClient(httpClient, nil, "") + params := &UpdateTeamParams{ + Name: "The Inflated Philosophers", + } + team, _, err := client.Teams.Update("the-interstellar-jurisdiction", "the-obese-philosophers", params) + assert.NoError(t, err) + expected := &Team{ + ID: "4", + Slug: "the-obese-philosophers", + Name: "The Inflated Philosophers", + DateCreated: mustParseTime("2017-07-18T19:30:14.736Z"), + HasAccess: true, + IsPending: false, + IsMember: false, + } + assert.Equal(t, expected, team) +} + +func TestTeamService_Delete(t *testing.T) { + httpClient, mux, server := testServer() + defer server.Close() + + mux.HandleFunc("/api/0/teams/the-interstellar-jurisdiction/the-obese-philosophers/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "DELETE", r) + }) + + client := NewClient(httpClient, nil, "") + _, err := client.Teams.Delete("the-interstellar-jurisdiction", "the-obese-philosophers") + assert.NoError(t, err) + +}