depencies: update `go-tfe`
This commit is contained in:
parent
8c54da0ad2
commit
d0c320f148
10
go.mod
10
go.mod
|
@ -45,7 +45,6 @@ require (
|
||||||
github.com/golang/protobuf v1.2.0
|
github.com/golang/protobuf v1.2.0
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
|
||||||
github.com/google/go-cmp v0.2.0
|
github.com/google/go-cmp v0.2.0
|
||||||
github.com/google/go-querystring v1.0.0 // indirect
|
|
||||||
github.com/googleapis/gax-go v0.0.0-20161107002406-da06d194a00e // indirect
|
github.com/googleapis/gax-go v0.0.0-20161107002406-da06d194a00e // indirect
|
||||||
github.com/gophercloud/gophercloud v0.0.0-20170524130959-3027adb1ce72
|
github.com/gophercloud/gophercloud v0.0.0-20170524130959-3027adb1ce72
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181004151105-1babbf986f6f // indirect
|
github.com/gopherjs/gopherjs v0.0.0-20181004151105-1babbf986f6f // indirect
|
||||||
|
@ -56,19 +55,18 @@ require (
|
||||||
github.com/hashicorp/consul v0.0.0-20171026175957-610f3c86a089
|
github.com/hashicorp/consul v0.0.0-20171026175957-610f3c86a089
|
||||||
github.com/hashicorp/errwrap v1.0.0
|
github.com/hashicorp/errwrap v1.0.0
|
||||||
github.com/hashicorp/go-checkpoint v0.0.0-20171009173528-1545e56e46de
|
github.com/hashicorp/go-checkpoint v0.0.0-20171009173528-1545e56e46de
|
||||||
github.com/hashicorp/go-cleanhttp v0.0.0-20171130225243-06c9ea3a335b
|
github.com/hashicorp/go-cleanhttp v0.5.0
|
||||||
github.com/hashicorp/go-getter v0.0.0-20180327010114-90bb99a48d86
|
github.com/hashicorp/go-getter v0.0.0-20180327010114-90bb99a48d86
|
||||||
github.com/hashicorp/go-hclog v0.0.0-20181001195459-61d530d6c27f
|
github.com/hashicorp/go-hclog v0.0.0-20181001195459-61d530d6c27f
|
||||||
github.com/hashicorp/go-immutable-radix v0.0.0-20180129170900-7f3cd4390caa // indirect
|
github.com/hashicorp/go-immutable-radix v0.0.0-20180129170900-7f3cd4390caa // indirect
|
||||||
github.com/hashicorp/go-msgpack v0.0.0-20150518234257-fa3f63826f7c // indirect
|
github.com/hashicorp/go-msgpack v0.0.0-20150518234257-fa3f63826f7c // indirect
|
||||||
github.com/hashicorp/go-multierror v1.0.0
|
github.com/hashicorp/go-multierror v1.0.0
|
||||||
github.com/hashicorp/go-plugin v0.0.0-20181002195811-1faddcf740b6
|
github.com/hashicorp/go-plugin v0.0.0-20181002195811-1faddcf740b6
|
||||||
github.com/hashicorp/go-retryablehttp v0.0.0-20160930035102-6e85be8fee1d
|
github.com/hashicorp/go-retryablehttp v0.0.0-20180718195005-e651d75abec6
|
||||||
github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90
|
github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90
|
||||||
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc // indirect
|
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc // indirect
|
||||||
github.com/hashicorp/go-slug v0.1.0 // indirect
|
|
||||||
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 // indirect
|
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 // indirect
|
||||||
github.com/hashicorp/go-tfe v0.2.6
|
github.com/hashicorp/go-tfe v0.2.9
|
||||||
github.com/hashicorp/go-uuid v1.0.0
|
github.com/hashicorp/go-uuid v1.0.0
|
||||||
github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577
|
github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577
|
||||||
github.com/hashicorp/golang-lru v0.5.0 // indirect
|
github.com/hashicorp/golang-lru v0.5.0 // indirect
|
||||||
|
@ -121,7 +119,6 @@ require (
|
||||||
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect
|
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect
|
||||||
github.com/soheilhy/cmux v0.1.4 // indirect
|
github.com/soheilhy/cmux v0.1.4 // indirect
|
||||||
github.com/spf13/afero v1.0.2
|
github.com/spf13/afero v1.0.2
|
||||||
github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d // indirect
|
|
||||||
github.com/terraform-providers/terraform-provider-aws v1.41.0
|
github.com/terraform-providers/terraform-provider-aws v1.41.0
|
||||||
github.com/terraform-providers/terraform-provider-openstack v0.0.0-20170616075611-4080a521c6ea
|
github.com/terraform-providers/terraform-provider-openstack v0.0.0-20170616075611-4080a521c6ea
|
||||||
github.com/terraform-providers/terraform-provider-template v1.0.0 // indirect
|
github.com/terraform-providers/terraform-provider-template v1.0.0 // indirect
|
||||||
|
@ -142,7 +139,6 @@ require (
|
||||||
golang.org/x/net v0.0.0-20181017193950-04a2e542c03f
|
golang.org/x/net v0.0.0-20181017193950-04a2e542c03f
|
||||||
golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced
|
golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced
|
||||||
golang.org/x/sys v0.0.0-20180925112736-b09afc3d579e // indirect
|
golang.org/x/sys v0.0.0-20180925112736-b09afc3d579e // indirect
|
||||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect
|
|
||||||
google.golang.org/api v0.0.0-20181015145326-625cd1887957
|
google.golang.org/api v0.0.0-20181015145326-625cd1887957
|
||||||
google.golang.org/appengine v1.2.0 // indirect
|
google.golang.org/appengine v1.2.0 // indirect
|
||||||
google.golang.org/grpc v1.14.0
|
google.golang.org/grpc v1.14.0
|
||||||
|
|
18
go.sum
18
go.sum
|
@ -121,8 +121,8 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/go-checkpoint v0.0.0-20171009173528-1545e56e46de h1:XDCSythtg8aWSRSO29uwhgh7b127fWr+m5SemqjSUL8=
|
github.com/hashicorp/go-checkpoint v0.0.0-20171009173528-1545e56e46de h1:XDCSythtg8aWSRSO29uwhgh7b127fWr+m5SemqjSUL8=
|
||||||
github.com/hashicorp/go-checkpoint v0.0.0-20171009173528-1545e56e46de/go.mod h1:xIwEieBHERyEvaeKF/TcHh1Hu+lxPM+n2vT1+g9I4m4=
|
github.com/hashicorp/go-checkpoint v0.0.0-20171009173528-1545e56e46de/go.mod h1:xIwEieBHERyEvaeKF/TcHh1Hu+lxPM+n2vT1+g9I4m4=
|
||||||
github.com/hashicorp/go-cleanhttp v0.0.0-20171130225243-06c9ea3a335b h1:xrvnoavY7pMnMB/4x+cSAMgkzwjiSyilS55LZ14Ko7o=
|
github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6KdvN3Gig=
|
||||||
github.com/hashicorp/go-cleanhttp v0.0.0-20171130225243-06c9ea3a335b/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||||
github.com/hashicorp/go-getter v0.0.0-20180327010114-90bb99a48d86 h1:mv3oKLM8sTaxmU/PrT39T35HRnUfchK+vtzXw6Ci9lY=
|
github.com/hashicorp/go-getter v0.0.0-20180327010114-90bb99a48d86 h1:mv3oKLM8sTaxmU/PrT39T35HRnUfchK+vtzXw6Ci9lY=
|
||||||
github.com/hashicorp/go-getter v0.0.0-20180327010114-90bb99a48d86/go.mod h1:6rdJFnhkXnzGOJbvkrdv4t9nLwKcVA+tmbQeUlkIzrU=
|
github.com/hashicorp/go-getter v0.0.0-20180327010114-90bb99a48d86/go.mod h1:6rdJFnhkXnzGOJbvkrdv4t9nLwKcVA+tmbQeUlkIzrU=
|
||||||
github.com/hashicorp/go-hclog v0.0.0-20181001195459-61d530d6c27f h1:Yv9YzBlAETjy6AOX9eLBZ3nshNVRREgerT/3nvxlGho=
|
github.com/hashicorp/go-hclog v0.0.0-20181001195459-61d530d6c27f h1:Yv9YzBlAETjy6AOX9eLBZ3nshNVRREgerT/3nvxlGho=
|
||||||
|
@ -137,8 +137,8 @@ github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uP
|
||||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||||
github.com/hashicorp/go-plugin v0.0.0-20181002195811-1faddcf740b6 h1:czAJ5CXRPr+6vd6RGdJelApnxNbK3dAkakgBwLEWfrc=
|
github.com/hashicorp/go-plugin v0.0.0-20181002195811-1faddcf740b6 h1:czAJ5CXRPr+6vd6RGdJelApnxNbK3dAkakgBwLEWfrc=
|
||||||
github.com/hashicorp/go-plugin v0.0.0-20181002195811-1faddcf740b6/go.mod h1:JSqWYsict+jzcj0+xElxyrBQRPNoiWQuddnxArJ7XHQ=
|
github.com/hashicorp/go-plugin v0.0.0-20181002195811-1faddcf740b6/go.mod h1:JSqWYsict+jzcj0+xElxyrBQRPNoiWQuddnxArJ7XHQ=
|
||||||
github.com/hashicorp/go-retryablehttp v0.0.0-20160930035102-6e85be8fee1d h1:/T1aqTlRV/71ER/wHvhqTZaXGQW7XSO+F16mIIHw7zc=
|
github.com/hashicorp/go-retryablehttp v0.0.0-20180718195005-e651d75abec6 h1:qCv4319q2q7XKn0MQbi8p37hsJ+9Xo8e6yojA73JVxk=
|
||||||
github.com/hashicorp/go-retryablehttp v0.0.0-20160930035102-6e85be8fee1d/go.mod h1:fXcdFsQoipQa7mwORhKad5jmDCeSy/RCGzWA08PO0lM=
|
github.com/hashicorp/go-retryablehttp v0.0.0-20180718195005-e651d75abec6/go.mod h1:fXcdFsQoipQa7mwORhKad5jmDCeSy/RCGzWA08PO0lM=
|
||||||
github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90 h1:9HVkPxOpo+yO93Ah4yrO67d/qh0fbLLWbKqhYjyHq9A=
|
github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90 h1:9HVkPxOpo+yO93Ah4yrO67d/qh0fbLLWbKqhYjyHq9A=
|
||||||
github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90/go.mod h1:o4zcYY1e0GEZI6eSEr+43QDYmuGglw1qSO6qdHUHCgg=
|
github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90/go.mod h1:o4zcYY1e0GEZI6eSEr+43QDYmuGglw1qSO6qdHUHCgg=
|
||||||
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc h1:wAa9fGALVHfjYxZuXRnmuJG2CnwRpJYOTvY6YdErAh0=
|
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc h1:wAa9fGALVHfjYxZuXRnmuJG2CnwRpJYOTvY6YdErAh0=
|
||||||
|
@ -147,8 +147,10 @@ github.com/hashicorp/go-slug v0.1.0 h1:MJGEiOwRGrQCBmMMZABHqIESySFJ4ajrsjgDI4/aF
|
||||||
github.com/hashicorp/go-slug v0.1.0/go.mod h1:+zDycQOzGqOqMW7Kn2fp9vz/NtqpMLQlgb9JUF+0km4=
|
github.com/hashicorp/go-slug v0.1.0/go.mod h1:+zDycQOzGqOqMW7Kn2fp9vz/NtqpMLQlgb9JUF+0km4=
|
||||||
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 h1:7YOlAIO2YWnJZkQp7B5eFykaIY7C9JndqAFQyVV5BhM=
|
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 h1:7YOlAIO2YWnJZkQp7B5eFykaIY7C9JndqAFQyVV5BhM=
|
||||||
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||||
github.com/hashicorp/go-tfe v0.2.6 h1:o2ryV7ZS0BgaLfNvzWz+A/6J70UETMy+wFL+DQlUy/M=
|
github.com/hashicorp/go-tfe v0.2.7 h1:Cy0irO9Qfgdn7FmvxSoXIQrRa3iM/kFmp/c0oCboCow=
|
||||||
github.com/hashicorp/go-tfe v0.2.6/go.mod h1:nJs7lSMcNPGQQtjyPG6en099CQ/f83+hfeeSqehl2Fg=
|
github.com/hashicorp/go-tfe v0.2.7/go.mod h1:WJgjAJVdnXYPOWF6j66VI20djUGfeFjeayIgUDhohsU=
|
||||||
|
github.com/hashicorp/go-tfe v0.2.9 h1:CmxjF5zBKh5XBf2fMseJPaSKxKIauIIS4r+6+hNX8JM=
|
||||||
|
github.com/hashicorp/go-tfe v0.2.9/go.mod h1:WJgjAJVdnXYPOWF6j66VI20djUGfeFjeayIgUDhohsU=
|
||||||
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
||||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577 h1:at4+18LrM8myamuV7/vT6x2s1JNXp2k4PsSbt4I02X4=
|
github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577 h1:at4+18LrM8myamuV7/vT6x2s1JNXp2k4PsSbt4I02X4=
|
||||||
|
@ -343,8 +345,8 @@ golang.org/x/sys v0.0.0-20180925112736-b09afc3d579e h1:LSlw/Dbj0MkNvPYAAkGinYmGl
|
||||||
golang.org/x/sys v0.0.0-20180925112736-b09afc3d579e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180925112736-b09afc3d579e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
|
||||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||||
google.golang.org/api v0.0.0-20181015145326-625cd1887957 h1:jwCmWUTrTFfjsobRuGurnCQeW4NZKijaIf6yAXwLR0E=
|
google.golang.org/api v0.0.0-20181015145326-625cd1887957 h1:jwCmWUTrTFfjsobRuGurnCQeW4NZKijaIf6yAXwLR0E=
|
||||||
google.golang.org/api v0.0.0-20181015145326-625cd1887957/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
google.golang.org/api v0.0.0-20181015145326-625cd1887957/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
module github.com/hashicorp/go-cleanhttp
|
|
@ -0,0 +1,43 @@
|
||||||
|
package cleanhttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandlerInput provides input options to cleanhttp's handlers
|
||||||
|
type HandlerInput struct {
|
||||||
|
ErrStatus int
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintablePathCheckHandler is a middleware that ensures the request path
|
||||||
|
// contains only printable runes.
|
||||||
|
func PrintablePathCheckHandler(next http.Handler, input *HandlerInput) http.Handler {
|
||||||
|
// Nil-check on input to make it optional
|
||||||
|
if input == nil {
|
||||||
|
input = &HandlerInput{
|
||||||
|
ErrStatus: http.StatusBadRequest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to http.StatusBadRequest on error
|
||||||
|
if input.ErrStatus == 0 {
|
||||||
|
input.ErrStatus = http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check URL path for non-printable characters
|
||||||
|
idx := strings.IndexFunc(r.URL.Path, func(c rune) bool {
|
||||||
|
return !unicode.IsPrint(c)
|
||||||
|
})
|
||||||
|
|
||||||
|
if idx != -1 {
|
||||||
|
w.WriteHeader(input.ErrStatus)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ sudo: false
|
||||||
language: go
|
language: go
|
||||||
|
|
||||||
go:
|
go:
|
||||||
- 1.6.3
|
- 1.8.1
|
||||||
|
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
|
|
|
@ -14,13 +14,16 @@ makes `retryablehttp` very easy to drop into existing programs.
|
||||||
|
|
||||||
`retryablehttp` performs automatic retries under certain conditions. Mainly, if
|
`retryablehttp` performs automatic retries under certain conditions. Mainly, if
|
||||||
an error is returned by the client (connection errors, etc.), or if a 500-range
|
an error is returned by the client (connection errors, etc.), or if a 500-range
|
||||||
response code is received, then a retry is invoked after a wait period.
|
response code is received (except 501), then a retry is invoked after a wait
|
||||||
Otherwise, the response is returned and left to the caller to interpret.
|
period. Otherwise, the response is returned and left to the caller to
|
||||||
|
interpret.
|
||||||
|
|
||||||
The main difference from `net/http` is that requests which take a request body
|
The main difference from `net/http` is that requests which take a request body
|
||||||
(POST/PUT et. al) require an `io.ReadSeeker` to be provided. This enables the
|
(POST/PUT et. al) can have the body provided in a number of ways (some more or
|
||||||
request body to be "rewound" if the initial request fails so that the full
|
less efficient) that allow "rewinding" the request body if the initial request
|
||||||
request can be attempted again.
|
fails so that the full request can be attempted again. See the
|
||||||
|
[godoc](http://godoc.org/github.com/hashicorp/go-retryablehttp) for more
|
||||||
|
details.
|
||||||
|
|
||||||
Example Use
|
Example Use
|
||||||
===========
|
===========
|
||||||
|
|
|
@ -8,18 +8,28 @@
|
||||||
// response is received, then a retry is invoked. Otherwise, the response is
|
// response is received, then a retry is invoked. Otherwise, the response is
|
||||||
// returned and left to the caller to interpret.
|
// returned and left to the caller to interpret.
|
||||||
//
|
//
|
||||||
// The main difference from net/http is that requests which take a request body
|
// Requests which take a request body should provide a non-nil function
|
||||||
// (POST/PUT et. al) require an io.ReadSeeker to be provided. This enables the
|
// parameter. The best choice is to provide either a function satisfying
|
||||||
// request body to be "rewound" if the initial request fails so that the full
|
// ReaderFunc which provides multiple io.Readers in an efficient manner, a
|
||||||
// request can be attempted again.
|
// *bytes.Buffer (the underlying raw byte slice will be used) or a raw byte
|
||||||
|
// slice. As it is a reference type, and we will wrap it as needed by readers,
|
||||||
|
// we can efficiently re-use the request body without needing to copy it. If an
|
||||||
|
// io.Reader (such as a *bytes.Reader) is provided, the full body will be read
|
||||||
|
// prior to the first request, and will be efficiently re-used for any retries.
|
||||||
|
// ReadSeeker can be used, but some users have observed occasional data races
|
||||||
|
// between the net/http library and the Seek functionality of some
|
||||||
|
// implementations of ReadSeeker, so should be avoided if possible.
|
||||||
package retryablehttp
|
package retryablehttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
@ -44,6 +54,9 @@ var (
|
||||||
respReadLimit = int64(4096)
|
respReadLimit = int64(4096)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ReaderFunc is the type of function that can be given natively to NewRequest
|
||||||
|
type ReaderFunc func() (io.Reader, error)
|
||||||
|
|
||||||
// LenReader is an interface implemented by many in-memory io.Reader's. Used
|
// LenReader is an interface implemented by many in-memory io.Reader's. Used
|
||||||
// for automatically sending the right Content-Length header when possible.
|
// for automatically sending the right Content-Length header when possible.
|
||||||
type LenReader interface {
|
type LenReader interface {
|
||||||
|
@ -54,32 +67,118 @@ type LenReader interface {
|
||||||
type Request struct {
|
type Request struct {
|
||||||
// body is a seekable reader over the request body payload. This is
|
// body is a seekable reader over the request body payload. This is
|
||||||
// used to rewind the request data in between retries.
|
// used to rewind the request data in between retries.
|
||||||
body io.ReadSeeker
|
body ReaderFunc
|
||||||
|
|
||||||
// Embed an HTTP request directly. This makes a *Request act exactly
|
// Embed an HTTP request directly. This makes a *Request act exactly
|
||||||
// like an *http.Request so that all meta methods are supported.
|
// like an *http.Request so that all meta methods are supported.
|
||||||
*http.Request
|
*http.Request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithContext returns wrapped Request with a shallow copy of underlying *http.Request
|
||||||
|
// with its context changed to ctx. The provided ctx must be non-nil.
|
||||||
|
func (r *Request) WithContext(ctx context.Context) *Request {
|
||||||
|
r.Request = r.Request.WithContext(ctx)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
// NewRequest creates a new wrapped request.
|
// NewRequest creates a new wrapped request.
|
||||||
func NewRequest(method, url string, body io.ReadSeeker) (*Request, error) {
|
func NewRequest(method, url string, rawBody interface{}) (*Request, error) {
|
||||||
// Wrap the body in a noop ReadCloser if non-nil. This prevents the
|
var err error
|
||||||
// reader from being closed by the HTTP client.
|
var body ReaderFunc
|
||||||
var rcBody io.ReadCloser
|
var contentLength int64
|
||||||
if body != nil {
|
|
||||||
rcBody = ioutil.NopCloser(body)
|
if rawBody != nil {
|
||||||
|
switch rawBody.(type) {
|
||||||
|
// If they gave us a function already, great! Use it.
|
||||||
|
case ReaderFunc:
|
||||||
|
body = rawBody.(ReaderFunc)
|
||||||
|
tmp, err := body()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if lr, ok := tmp.(LenReader); ok {
|
||||||
|
contentLength = int64(lr.Len())
|
||||||
|
}
|
||||||
|
if c, ok := tmp.(io.Closer); ok {
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
case func() (io.Reader, error):
|
||||||
|
body = rawBody.(func() (io.Reader, error))
|
||||||
|
tmp, err := body()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if lr, ok := tmp.(LenReader); ok {
|
||||||
|
contentLength = int64(lr.Len())
|
||||||
|
}
|
||||||
|
if c, ok := tmp.(io.Closer); ok {
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a regular byte slice, we can read it over and over via new
|
||||||
|
// readers
|
||||||
|
case []byte:
|
||||||
|
buf := rawBody.([]byte)
|
||||||
|
body = func() (io.Reader, error) {
|
||||||
|
return bytes.NewReader(buf), nil
|
||||||
|
}
|
||||||
|
contentLength = int64(len(buf))
|
||||||
|
|
||||||
|
// If a bytes.Buffer we can read the underlying byte slice over and
|
||||||
|
// over
|
||||||
|
case *bytes.Buffer:
|
||||||
|
buf := rawBody.(*bytes.Buffer)
|
||||||
|
body = func() (io.Reader, error) {
|
||||||
|
return bytes.NewReader(buf.Bytes()), nil
|
||||||
|
}
|
||||||
|
contentLength = int64(buf.Len())
|
||||||
|
|
||||||
|
// We prioritize *bytes.Reader here because we don't really want to
|
||||||
|
// deal with it seeking so want it to match here instead of the
|
||||||
|
// io.ReadSeeker case.
|
||||||
|
case *bytes.Reader:
|
||||||
|
buf, err := ioutil.ReadAll(rawBody.(*bytes.Reader))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
body = func() (io.Reader, error) {
|
||||||
|
return bytes.NewReader(buf), nil
|
||||||
|
}
|
||||||
|
contentLength = int64(len(buf))
|
||||||
|
|
||||||
|
// Compat case
|
||||||
|
case io.ReadSeeker:
|
||||||
|
raw := rawBody.(io.ReadSeeker)
|
||||||
|
body = func() (io.Reader, error) {
|
||||||
|
raw.Seek(0, 0)
|
||||||
|
return ioutil.NopCloser(raw), nil
|
||||||
|
}
|
||||||
|
if lr, ok := raw.(LenReader); ok {
|
||||||
|
contentLength = int64(lr.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read all in so we can reset
|
||||||
|
case io.Reader:
|
||||||
|
buf, err := ioutil.ReadAll(rawBody.(io.Reader))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
body = func() (io.Reader, error) {
|
||||||
|
return bytes.NewReader(buf), nil
|
||||||
|
}
|
||||||
|
contentLength = int64(len(buf))
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("cannot handle type %T", rawBody)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the request with the noop-closer for the body.
|
httpReq, err := http.NewRequest(method, url, nil)
|
||||||
httpReq, err := http.NewRequest(method, url, rcBody)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
httpReq.ContentLength = contentLength
|
||||||
// Check if we can set the Content-Length automatically.
|
|
||||||
if lr, ok := body.(LenReader); ok {
|
|
||||||
httpReq.ContentLength = int64(lr.Len())
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Request{body, httpReq}, nil
|
return &Request{body, httpReq}, nil
|
||||||
}
|
}
|
||||||
|
@ -105,7 +204,18 @@ type ResponseLogHook func(*log.Logger, *http.Response)
|
||||||
// Client will close any response body when retrying, but if the retry is
|
// Client will close any response body when retrying, but if the retry is
|
||||||
// aborted it is up to the CheckResponse callback to properly close any
|
// aborted it is up to the CheckResponse callback to properly close any
|
||||||
// response body before returning.
|
// response body before returning.
|
||||||
type CheckRetry func(resp *http.Response, err error) (bool, error)
|
type CheckRetry func(ctx context.Context, resp *http.Response, err error) (bool, error)
|
||||||
|
|
||||||
|
// Backoff specifies a policy for how long to wait between retries.
|
||||||
|
// It is called after a failing request to determine the amount of time
|
||||||
|
// that should pass before trying again.
|
||||||
|
type Backoff func(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration
|
||||||
|
|
||||||
|
// ErrorHandler is called if retries are expired, containing the last status
|
||||||
|
// from the http library. If not specified, default behavior for the library is
|
||||||
|
// to close the body and return an error indicating how many tries were
|
||||||
|
// attempted. If overriding this, be sure to close the body if needed.
|
||||||
|
type ErrorHandler func(resp *http.Response, err error, numTries int) (*http.Response, error)
|
||||||
|
|
||||||
// Client is used to make HTTP requests. It adds additional functionality
|
// Client is used to make HTTP requests. It adds additional functionality
|
||||||
// like automatic retries to tolerate minor outages.
|
// like automatic retries to tolerate minor outages.
|
||||||
|
@ -128,6 +238,12 @@ type Client struct {
|
||||||
// CheckRetry specifies the policy for handling retries, and is called
|
// CheckRetry specifies the policy for handling retries, and is called
|
||||||
// after each request. The default policy is DefaultRetryPolicy.
|
// after each request. The default policy is DefaultRetryPolicy.
|
||||||
CheckRetry CheckRetry
|
CheckRetry CheckRetry
|
||||||
|
|
||||||
|
// Backoff specifies the policy for how long to wait between retries
|
||||||
|
Backoff Backoff
|
||||||
|
|
||||||
|
// ErrorHandler specifies the custom error handler to use, if any
|
||||||
|
ErrorHandler ErrorHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient creates a new Client with default settings.
|
// NewClient creates a new Client with default settings.
|
||||||
|
@ -139,12 +255,18 @@ func NewClient() *Client {
|
||||||
RetryWaitMax: defaultRetryWaitMax,
|
RetryWaitMax: defaultRetryWaitMax,
|
||||||
RetryMax: defaultRetryMax,
|
RetryMax: defaultRetryMax,
|
||||||
CheckRetry: DefaultRetryPolicy,
|
CheckRetry: DefaultRetryPolicy,
|
||||||
|
Backoff: DefaultBackoff,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultRetryPolicy provides a default callback for Client.CheckRetry, which
|
// DefaultRetryPolicy provides a default callback for Client.CheckRetry, which
|
||||||
// will retry on connection errors and server errors.
|
// will retry on connection errors and server errors.
|
||||||
func DefaultRetryPolicy(resp *http.Response, err error) (bool, error) {
|
func DefaultRetryPolicy(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
||||||
|
// do not retry on context.Canceled or context.DeadlineExceeded
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return false, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
|
@ -152,24 +274,92 @@ func DefaultRetryPolicy(resp *http.Response, err error) (bool, error) {
|
||||||
// the server time to recover, as 500's are typically not permanent
|
// the server time to recover, as 500's are typically not permanent
|
||||||
// errors and may relate to outages on the server side. This will catch
|
// errors and may relate to outages on the server side. This will catch
|
||||||
// invalid response codes as well, like 0 and 999.
|
// invalid response codes as well, like 0 and 999.
|
||||||
if resp.StatusCode == 0 || resp.StatusCode >= 500 {
|
if resp.StatusCode == 0 || (resp.StatusCode >= 500 && resp.StatusCode != 501) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultBackoff provides a default callback for Client.Backoff which
|
||||||
|
// will perform exponential backoff based on the attempt number and limited
|
||||||
|
// by the provided minimum and maximum durations.
|
||||||
|
func DefaultBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
|
||||||
|
mult := math.Pow(2, float64(attemptNum)) * float64(min)
|
||||||
|
sleep := time.Duration(mult)
|
||||||
|
if float64(sleep) != mult || sleep > max {
|
||||||
|
sleep = max
|
||||||
|
}
|
||||||
|
return sleep
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinearJitterBackoff provides a callback for Client.Backoff which will
|
||||||
|
// perform linear backoff based on the attempt number and with jitter to
|
||||||
|
// prevent a thundering herd.
|
||||||
|
//
|
||||||
|
// min and max here are *not* absolute values. The number to be multipled by
|
||||||
|
// the attempt number will be chosen at random from between them, thus they are
|
||||||
|
// bounding the jitter.
|
||||||
|
//
|
||||||
|
// For instance:
|
||||||
|
// * To get strictly linear backoff of one second increasing each retry, set
|
||||||
|
// both to one second (1s, 2s, 3s, 4s, ...)
|
||||||
|
// * To get a small amount of jitter centered around one second increasing each
|
||||||
|
// retry, set to around one second, such as a min of 800ms and max of 1200ms
|
||||||
|
// (892ms, 2102ms, 2945ms, 4312ms, ...)
|
||||||
|
// * To get extreme jitter, set to a very wide spread, such as a min of 100ms
|
||||||
|
// and a max of 20s (15382ms, 292ms, 51321ms, 35234ms, ...)
|
||||||
|
func LinearJitterBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
|
||||||
|
// attemptNum always starts at zero but we want to start at 1 for multiplication
|
||||||
|
attemptNum++
|
||||||
|
|
||||||
|
if max <= min {
|
||||||
|
// Unclear what to do here, or they are the same, so return min *
|
||||||
|
// attemptNum
|
||||||
|
return min * time.Duration(attemptNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed rand; doing this every time is fine
|
||||||
|
rand := rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
|
||||||
|
|
||||||
|
// Pick a random number that lies somewhere between the min and max and
|
||||||
|
// multiply by the attemptNum. attemptNum starts at zero so we always
|
||||||
|
// increment here. We first get a random percentage, then apply that to the
|
||||||
|
// difference between min and max, and add to min.
|
||||||
|
jitter := rand.Float64() * float64(max-min)
|
||||||
|
jitterMin := int64(jitter) + int64(min)
|
||||||
|
return time.Duration(jitterMin * int64(attemptNum))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PassthroughErrorHandler is an ErrorHandler that directly passes through the
|
||||||
|
// values from the net/http library for the final request. The body is not
|
||||||
|
// closed.
|
||||||
|
func PassthroughErrorHandler(resp *http.Response, err error, _ int) (*http.Response, error) {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
// Do wraps calling an HTTP method with retries.
|
// Do wraps calling an HTTP method with retries.
|
||||||
func (c *Client) Do(req *Request) (*http.Response, error) {
|
func (c *Client) Do(req *Request) (*http.Response, error) {
|
||||||
c.Logger.Printf("[DEBUG] %s %s", req.Method, req.URL)
|
if c.Logger != nil {
|
||||||
|
c.Logger.Printf("[DEBUG] %s %s", req.Method, req.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp *http.Response
|
||||||
|
var err error
|
||||||
|
|
||||||
for i := 0; ; i++ {
|
for i := 0; ; i++ {
|
||||||
var code int // HTTP response code
|
var code int // HTTP response code
|
||||||
|
|
||||||
// Always rewind the request body when non-nil.
|
// Always rewind the request body when non-nil.
|
||||||
if req.body != nil {
|
if req.body != nil {
|
||||||
if _, err := req.body.Seek(0, 0); err != nil {
|
body, err := req.body()
|
||||||
return nil, fmt.Errorf("failed to seek body: %v", err)
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
if c, ok := body.(io.ReadCloser); ok {
|
||||||
|
req.Request.Body = c
|
||||||
|
} else {
|
||||||
|
req.Request.Body = ioutil.NopCloser(body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,13 +368,18 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt the request
|
// Attempt the request
|
||||||
resp, err := c.HTTPClient.Do(req.Request)
|
resp, err = c.HTTPClient.Do(req.Request)
|
||||||
|
if resp != nil {
|
||||||
|
code = resp.StatusCode
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we should continue with retries.
|
// Check if we should continue with retries.
|
||||||
checkOK, checkErr := c.CheckRetry(resp, err)
|
checkOK, checkErr := c.CheckRetry(req.Request.Context(), resp, err)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Logger.Printf("[ERR] %s %s request failed: %v", req.Method, req.URL, err)
|
if c.Logger != nil {
|
||||||
|
c.Logger.Printf("[ERR] %s %s request failed: %v", req.Method, req.URL, err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Call this here to maintain the behavior of logging all requests,
|
// Call this here to maintain the behavior of logging all requests,
|
||||||
// even if CheckRetry signals to stop.
|
// even if CheckRetry signals to stop.
|
||||||
|
@ -202,25 +397,38 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We do this before drainBody beause there's no need for the I/O if
|
||||||
|
// we're breaking out
|
||||||
|
remain := c.RetryMax - i
|
||||||
|
if remain <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
// We're going to retry, consume any response to reuse the connection.
|
// We're going to retry, consume any response to reuse the connection.
|
||||||
if err == nil {
|
if err == nil && resp != nil {
|
||||||
c.drainBody(resp.Body)
|
c.drainBody(resp.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
remain := c.RetryMax - i
|
wait := c.Backoff(c.RetryWaitMin, c.RetryWaitMax, i, resp)
|
||||||
if remain == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
wait := backoff(c.RetryWaitMin, c.RetryWaitMax, i)
|
|
||||||
desc := fmt.Sprintf("%s %s", req.Method, req.URL)
|
desc := fmt.Sprintf("%s %s", req.Method, req.URL)
|
||||||
if code > 0 {
|
if code > 0 {
|
||||||
desc = fmt.Sprintf("%s (status: %d)", desc, code)
|
desc = fmt.Sprintf("%s (status: %d)", desc, code)
|
||||||
}
|
}
|
||||||
c.Logger.Printf("[DEBUG] %s: retrying in %s (%d left)", desc, wait, remain)
|
if c.Logger != nil {
|
||||||
|
c.Logger.Printf("[DEBUG] %s: retrying in %s (%d left)", desc, wait, remain)
|
||||||
|
}
|
||||||
time.Sleep(wait)
|
time.Sleep(wait)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return an error if we fall out of the retry loop
|
if c.ErrorHandler != nil {
|
||||||
|
return c.ErrorHandler(resp, err, c.RetryMax+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default, we close the response body and return an error without
|
||||||
|
// returning the response
|
||||||
|
if resp != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("%s %s giving up after %d attempts",
|
return nil, fmt.Errorf("%s %s giving up after %d attempts",
|
||||||
req.Method, req.URL, c.RetryMax+1)
|
req.Method, req.URL, c.RetryMax+1)
|
||||||
}
|
}
|
||||||
|
@ -230,7 +438,9 @@ func (c *Client) drainBody(body io.ReadCloser) {
|
||||||
defer body.Close()
|
defer body.Close()
|
||||||
_, err := io.Copy(ioutil.Discard, io.LimitReader(body, respReadLimit))
|
_, err := io.Copy(ioutil.Discard, io.LimitReader(body, respReadLimit))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Logger.Printf("[ERR] error reading response body: %v", err)
|
if c.Logger != nil {
|
||||||
|
c.Logger.Printf("[ERR] error reading response body: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,12 +473,12 @@ func (c *Client) Head(url string) (*http.Response, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post is a shortcut for doing a POST request without making a new client.
|
// Post is a shortcut for doing a POST request without making a new client.
|
||||||
func Post(url, bodyType string, body io.ReadSeeker) (*http.Response, error) {
|
func Post(url, bodyType string, body interface{}) (*http.Response, error) {
|
||||||
return defaultClient.Post(url, bodyType, body)
|
return defaultClient.Post(url, bodyType, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post is a convenience method for doing simple POST requests.
|
// Post is a convenience method for doing simple POST requests.
|
||||||
func (c *Client) Post(url, bodyType string, body io.ReadSeeker) (*http.Response, error) {
|
func (c *Client) Post(url, bodyType string, body interface{}) (*http.Response, error) {
|
||||||
req, err := NewRequest("POST", url, body)
|
req, err := NewRequest("POST", url, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -288,15 +498,3 @@ func PostForm(url string, data url.Values) (*http.Response, error) {
|
||||||
func (c *Client) PostForm(url string, data url.Values) (*http.Response, error) {
|
func (c *Client) PostForm(url string, data url.Values) (*http.Response, error) {
|
||||||
return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
|
return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// backoff is used to calculate how long to sleep before retrying
|
|
||||||
// after observing failures. It takes the minimum/maximum wait time and
|
|
||||||
// iteration, and returns the duration to wait.
|
|
||||||
func backoff(min, max time.Duration, iter int) time.Duration {
|
|
||||||
mult := math.Pow(2, float64(iter)) * float64(min)
|
|
||||||
sleep := time.Duration(mult)
|
|
||||||
if float64(sleep) != mult || sleep > max {
|
|
||||||
sleep = max
|
|
||||||
}
|
|
||||||
return sleep
|
|
||||||
}
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ Currently the following endpoints are supported:
|
||||||
- [x] [Organizations](https://www.terraform.io/docs/enterprise/api/organizations.html)
|
- [x] [Organizations](https://www.terraform.io/docs/enterprise/api/organizations.html)
|
||||||
- [x] [Organization Tokens](https://www.terraform.io/docs/enterprise/api/organization-tokens.html)
|
- [x] [Organization Tokens](https://www.terraform.io/docs/enterprise/api/organization-tokens.html)
|
||||||
- [x] [Policies](https://www.terraform.io/docs/enterprise/api/policies.html)
|
- [x] [Policies](https://www.terraform.io/docs/enterprise/api/policies.html)
|
||||||
|
- [x] [Policy Sets](https://www.terraform.io/docs/enterprise/api/policy-sets.html)
|
||||||
- [x] [Policy Checks](https://www.terraform.io/docs/enterprise/api/policy-checks.html)
|
- [x] [Policy Checks](https://www.terraform.io/docs/enterprise/api/policy-checks.html)
|
||||||
- [ ] [Registry Modules](https://www.terraform.io/docs/enterprise/api/modules.html)
|
- [ ] [Registry Modules](https://www.terraform.io/docs/enterprise/api/modules.html)
|
||||||
- [x] [Runs](https://www.terraform.io/docs/enterprise/api/run.html)
|
- [x] [Runs](https://www.terraform.io/docs/enterprise/api/run.html)
|
||||||
|
|
|
@ -69,7 +69,7 @@ type ApplyStatusTimestamps struct {
|
||||||
// Read an apply by its ID.
|
// Read an apply by its ID.
|
||||||
func (s *applies) Read(ctx context.Context, applyID string) (*Apply, error) {
|
func (s *applies) Read(ctx context.Context, applyID string) (*Apply, error) {
|
||||||
if !validStringID(&applyID) {
|
if !validStringID(&applyID) {
|
||||||
return nil, errors.New("Invalid value for apply ID")
|
return nil, errors.New("invalid value for apply ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("applies/%s", url.QueryEscape(applyID))
|
u := fmt.Sprintf("applies/%s", url.QueryEscape(applyID))
|
||||||
|
@ -90,7 +90,7 @@ func (s *applies) Read(ctx context.Context, applyID string) (*Apply, error) {
|
||||||
// Logs retrieves the logs of an apply.
|
// Logs retrieves the logs of an apply.
|
||||||
func (s *applies) Logs(ctx context.Context, applyID string) (io.Reader, error) {
|
func (s *applies) Logs(ctx context.Context, applyID string) (io.Reader, error) {
|
||||||
if !validStringID(&applyID) {
|
if !validStringID(&applyID) {
|
||||||
return nil, errors.New("Invalid value for apply ID")
|
return nil, errors.New("invalid value for apply ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the apply to make sure it exists.
|
// Get the apply to make sure it exists.
|
||||||
|
@ -101,12 +101,12 @@ func (s *applies) Logs(ctx context.Context, applyID string) (io.Reader, error) {
|
||||||
|
|
||||||
// Return an error if the log URL is empty.
|
// Return an error if the log URL is empty.
|
||||||
if a.LogReadURL == "" {
|
if a.LogReadURL == "" {
|
||||||
return nil, fmt.Errorf("Apply %s does not have a log URL", applyID)
|
return nil, fmt.Errorf("apply %s does not have a log URL", applyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.Parse(a.LogReadURL)
|
u, err := url.Parse(a.LogReadURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Invalid log URL: %v", err)
|
return nil, fmt.Errorf("invalid log URL: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
done := func() (bool, error) {
|
done := func() (bool, error) {
|
||||||
|
|
|
@ -101,7 +101,7 @@ type ConfigurationVersionListOptions struct {
|
||||||
// List returns all configuration versions of a workspace.
|
// List returns all configuration versions of a workspace.
|
||||||
func (s *configurationVersions) List(ctx context.Context, workspaceID string, options ConfigurationVersionListOptions) (*ConfigurationVersionList, error) {
|
func (s *configurationVersions) List(ctx context.Context, workspaceID string, options ConfigurationVersionListOptions) (*ConfigurationVersionList, error) {
|
||||||
if !validStringID(&workspaceID) {
|
if !validStringID(&workspaceID) {
|
||||||
return nil, errors.New("Invalid value for workspace ID")
|
return nil, errors.New("invalid value for workspace ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("workspaces/%s/configuration-versions", url.QueryEscape(workspaceID))
|
u := fmt.Sprintf("workspaces/%s/configuration-versions", url.QueryEscape(workspaceID))
|
||||||
|
@ -137,7 +137,7 @@ type ConfigurationVersionCreateOptions struct {
|
||||||
// configuration version will be usable once data is uploaded to it.
|
// configuration version will be usable once data is uploaded to it.
|
||||||
func (s *configurationVersions) Create(ctx context.Context, workspaceID string, options ConfigurationVersionCreateOptions) (*ConfigurationVersion, error) {
|
func (s *configurationVersions) Create(ctx context.Context, workspaceID string, options ConfigurationVersionCreateOptions) (*ConfigurationVersion, error) {
|
||||||
if !validStringID(&workspaceID) {
|
if !validStringID(&workspaceID) {
|
||||||
return nil, errors.New("Invalid value for workspace ID")
|
return nil, errors.New("invalid value for workspace ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we don't send a user provided ID.
|
// Make sure we don't send a user provided ID.
|
||||||
|
@ -161,7 +161,7 @@ func (s *configurationVersions) Create(ctx context.Context, workspaceID string,
|
||||||
// Read a configuration version by its ID.
|
// Read a configuration version by its ID.
|
||||||
func (s *configurationVersions) Read(ctx context.Context, cvID string) (*ConfigurationVersion, error) {
|
func (s *configurationVersions) Read(ctx context.Context, cvID string) (*ConfigurationVersion, error) {
|
||||||
if !validStringID(&cvID) {
|
if !validStringID(&cvID) {
|
||||||
return nil, errors.New("Invalid value for configuration version ID")
|
return nil, errors.New("invalid value for configuration version ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("configuration-versions/%s", url.QueryEscape(cvID))
|
u := fmt.Sprintf("configuration-versions/%s", url.QueryEscape(cvID))
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
module github.com/hashicorp/go-tfe
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/google/go-querystring v1.0.0
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.0
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.0.0-20180718195005-e651d75abec6
|
||||||
|
github.com/hashicorp/go-slug v0.1.0
|
||||||
|
github.com/hashicorp/go-uuid v1.0.0
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.2.2
|
||||||
|
github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d
|
||||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c
|
||||||
|
)
|
|
@ -0,0 +1,20 @@
|
||||||
|
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/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||||
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
|
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-retryablehttp v0.0.0-20180718195005-e651d75abec6 h1:qCv4319q2q7XKn0MQbi8p37hsJ+9Xo8e6yojA73JVxk=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.0.0-20180718195005-e651d75abec6/go.mod h1:fXcdFsQoipQa7mwORhKad5jmDCeSy/RCGzWA08PO0lM=
|
||||||
|
github.com/hashicorp/go-slug v0.1.0 h1:MJGEiOwRGrQCBmMMZABHqIESySFJ4ajrsjgDI4/aFI0=
|
||||||
|
github.com/hashicorp/go-slug v0.1.0/go.mod h1:+zDycQOzGqOqMW7Kn2fp9vz/NtqpMLQlgb9JUF+0km4=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
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/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d h1:Z4EH+5EffvBEhh37F0C0DnpklTMh00JOkjW5zK3ofBI=
|
||||||
|
github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d/go.mod h1:BSTlc8jOjh0niykqEGVXOLXdi9o0r0kR8tCYiMvjFgw=
|
||||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
|
||||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
@ -63,8 +63,13 @@ func (r *LogReader) read(l []byte) (int, error) {
|
||||||
}
|
}
|
||||||
req = req.WithContext(r.ctx)
|
req = req.WithContext(r.ctx)
|
||||||
|
|
||||||
|
// Attach the default headers.
|
||||||
|
for k, v := range r.client.headers {
|
||||||
|
req.Header[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve the next chunk.
|
// Retrieve the next chunk.
|
||||||
resp, err := r.client.http.Do(req)
|
resp, err := r.client.http.HTTPClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,7 +83,7 @@ type OAuthClientListOptions struct {
|
||||||
// List all the OAuth clients for a given organization.
|
// List all the OAuth clients for a given organization.
|
||||||
func (s *oAuthClients) List(ctx context.Context, organization string, options OAuthClientListOptions) (*OAuthClientList, error) {
|
func (s *oAuthClients) List(ctx context.Context, organization string, options OAuthClientListOptions) (*OAuthClientList, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("organizations/%s/oauth-clients", url.QueryEscape(organization))
|
u := fmt.Sprintf("organizations/%s/oauth-clients", url.QueryEscape(organization))
|
||||||
|
@ -121,16 +121,16 @@ type OAuthClientCreateOptions struct {
|
||||||
|
|
||||||
func (o OAuthClientCreateOptions) valid() error {
|
func (o OAuthClientCreateOptions) valid() error {
|
||||||
if !validString(o.APIURL) {
|
if !validString(o.APIURL) {
|
||||||
return errors.New("APIURL is required")
|
return errors.New("API URL is required")
|
||||||
}
|
}
|
||||||
if !validString(o.HTTPURL) {
|
if !validString(o.HTTPURL) {
|
||||||
return errors.New("HTTPURL is required")
|
return errors.New("HTTP URL is required")
|
||||||
}
|
}
|
||||||
if !validString(o.OAuthToken) {
|
if !validString(o.OAuthToken) {
|
||||||
return errors.New("OAuthToken is required")
|
return errors.New("OAuth token is required")
|
||||||
}
|
}
|
||||||
if o.ServiceProvider == nil {
|
if o.ServiceProvider == nil {
|
||||||
return errors.New("ServiceProvider is required")
|
return errors.New("service provider is required")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -138,7 +138,7 @@ func (o OAuthClientCreateOptions) valid() error {
|
||||||
// Create an OAuth client to connect an organization and a VCS provider.
|
// Create an OAuth client to connect an organization and a VCS provider.
|
||||||
func (s *oAuthClients) Create(ctx context.Context, organization string, options OAuthClientCreateOptions) (*OAuthClient, error) {
|
func (s *oAuthClients) Create(ctx context.Context, organization string, options OAuthClientCreateOptions) (*OAuthClient, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
if err := options.valid(); err != nil {
|
if err := options.valid(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -165,7 +165,7 @@ func (s *oAuthClients) Create(ctx context.Context, organization string, options
|
||||||
// Read an OAuth client by its ID.
|
// Read an OAuth client by its ID.
|
||||||
func (s *oAuthClients) Read(ctx context.Context, oAuthClientID string) (*OAuthClient, error) {
|
func (s *oAuthClients) Read(ctx context.Context, oAuthClientID string) (*OAuthClient, error) {
|
||||||
if !validStringID(&oAuthClientID) {
|
if !validStringID(&oAuthClientID) {
|
||||||
return nil, errors.New("Invalid value for OAuth client ID")
|
return nil, errors.New("invalid value for OAuth client ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("oauth-clients/%s", url.QueryEscape(oAuthClientID))
|
u := fmt.Sprintf("oauth-clients/%s", url.QueryEscape(oAuthClientID))
|
||||||
|
@ -186,7 +186,7 @@ func (s *oAuthClients) Read(ctx context.Context, oAuthClientID string) (*OAuthCl
|
||||||
// Delete an OAuth client by its ID.
|
// Delete an OAuth client by its ID.
|
||||||
func (s *oAuthClients) Delete(ctx context.Context, oAuthClientID string) error {
|
func (s *oAuthClients) Delete(ctx context.Context, oAuthClientID string) error {
|
||||||
if !validStringID(&oAuthClientID) {
|
if !validStringID(&oAuthClientID) {
|
||||||
return errors.New("Invalid value for OAuth client ID")
|
return errors.New("invalid value for OAuth client ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("oauth-clients/%s", url.QueryEscape(oAuthClientID))
|
u := fmt.Sprintf("oauth-clients/%s", url.QueryEscape(oAuthClientID))
|
||||||
|
|
|
@ -62,7 +62,7 @@ type OAuthTokenListOptions struct {
|
||||||
// List all the OAuth tokens for a given organization.
|
// List all the OAuth tokens for a given organization.
|
||||||
func (s *oAuthTokens) List(ctx context.Context, organization string, options OAuthTokenListOptions) (*OAuthTokenList, error) {
|
func (s *oAuthTokens) List(ctx context.Context, organization string, options OAuthTokenListOptions) (*OAuthTokenList, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("organizations/%s/oauth-tokens", url.QueryEscape(organization))
|
u := fmt.Sprintf("organizations/%s/oauth-tokens", url.QueryEscape(organization))
|
||||||
|
@ -83,7 +83,7 @@ func (s *oAuthTokens) List(ctx context.Context, organization string, options OAu
|
||||||
// Read an OAuth token by its ID.
|
// Read an OAuth token by its ID.
|
||||||
func (s *oAuthTokens) Read(ctx context.Context, oAuthTokenID string) (*OAuthToken, error) {
|
func (s *oAuthTokens) Read(ctx context.Context, oAuthTokenID string) (*OAuthToken, error) {
|
||||||
if !validStringID(&oAuthTokenID) {
|
if !validStringID(&oAuthTokenID) {
|
||||||
return nil, errors.New("Invalid value for OAuth token ID")
|
return nil, errors.New("invalid value for OAuth token ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("oauth-tokens/%s", url.QueryEscape(oAuthTokenID))
|
u := fmt.Sprintf("oauth-tokens/%s", url.QueryEscape(oAuthTokenID))
|
||||||
|
@ -113,7 +113,7 @@ type OAuthTokenUpdateOptions struct {
|
||||||
// Update an existing OAuth token.
|
// Update an existing OAuth token.
|
||||||
func (s *oAuthTokens) Update(ctx context.Context, oAuthTokenID string, options OAuthTokenUpdateOptions) (*OAuthToken, error) {
|
func (s *oAuthTokens) Update(ctx context.Context, oAuthTokenID string, options OAuthTokenUpdateOptions) (*OAuthToken, error) {
|
||||||
if !validStringID(&oAuthTokenID) {
|
if !validStringID(&oAuthTokenID) {
|
||||||
return nil, errors.New("Invalid value for OAuth token ID")
|
return nil, errors.New("invalid value for OAuth token ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we don't send a user provided ID.
|
// Make sure we don't send a user provided ID.
|
||||||
|
@ -137,7 +137,7 @@ func (s *oAuthTokens) Update(ctx context.Context, oAuthTokenID string, options O
|
||||||
// Delete an OAuth token by its ID.
|
// Delete an OAuth token by its ID.
|
||||||
func (s *oAuthTokens) Delete(ctx context.Context, oAuthTokenID string) error {
|
func (s *oAuthTokens) Delete(ctx context.Context, oAuthTokenID string) error {
|
||||||
if !validStringID(&oAuthTokenID) {
|
if !validStringID(&oAuthTokenID) {
|
||||||
return errors.New("Invalid value for OAuth token ID")
|
return errors.New("invalid value for OAuth token ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("oauth-tokens/%s", url.QueryEscape(oAuthTokenID))
|
u := fmt.Sprintf("oauth-tokens/%s", url.QueryEscape(oAuthTokenID))
|
||||||
|
|
|
@ -147,13 +147,13 @@ type OrganizationCreateOptions struct {
|
||||||
|
|
||||||
func (o OrganizationCreateOptions) valid() error {
|
func (o OrganizationCreateOptions) valid() error {
|
||||||
if !validString(o.Name) {
|
if !validString(o.Name) {
|
||||||
return errors.New("Name is required")
|
return errors.New("name is required")
|
||||||
}
|
}
|
||||||
if !validStringID(o.Name) {
|
if !validStringID(o.Name) {
|
||||||
return errors.New("Invalid value for name")
|
return errors.New("invalid value for name")
|
||||||
}
|
}
|
||||||
if !validString(o.Email) {
|
if !validString(o.Email) {
|
||||||
return errors.New("Email is required")
|
return errors.New("email is required")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -184,7 +184,7 @@ func (s *organizations) Create(ctx context.Context, options OrganizationCreateOp
|
||||||
// Read an organization by its name.
|
// Read an organization by its name.
|
||||||
func (s *organizations) Read(ctx context.Context, organization string) (*Organization, error) {
|
func (s *organizations) Read(ctx context.Context, organization string) (*Organization, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("organizations/%s", url.QueryEscape(organization))
|
u := fmt.Sprintf("organizations/%s", url.QueryEscape(organization))
|
||||||
|
@ -226,7 +226,7 @@ type OrganizationUpdateOptions struct {
|
||||||
// Update attributes of an existing organization.
|
// Update attributes of an existing organization.
|
||||||
func (s *organizations) Update(ctx context.Context, organization string, options OrganizationUpdateOptions) (*Organization, error) {
|
func (s *organizations) Update(ctx context.Context, organization string, options OrganizationUpdateOptions) (*Organization, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we don't send a user provided ID.
|
// Make sure we don't send a user provided ID.
|
||||||
|
@ -250,7 +250,7 @@ func (s *organizations) Update(ctx context.Context, organization string, options
|
||||||
// Delete an organization by its name.
|
// Delete an organization by its name.
|
||||||
func (s *organizations) Delete(ctx context.Context, organization string) error {
|
func (s *organizations) Delete(ctx context.Context, organization string) error {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return errors.New("Invalid value for organization")
|
return errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("organizations/%s", url.QueryEscape(organization))
|
u := fmt.Sprintf("organizations/%s", url.QueryEscape(organization))
|
||||||
|
@ -265,7 +265,7 @@ func (s *organizations) Delete(ctx context.Context, organization string) error {
|
||||||
// Capacity shows the currently used capacity of an organization.
|
// Capacity shows the currently used capacity of an organization.
|
||||||
func (s *organizations) Capacity(ctx context.Context, organization string) (*Capacity, error) {
|
func (s *organizations) Capacity(ctx context.Context, organization string) (*Capacity, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("organizations/%s/capacity", url.QueryEscape(organization))
|
u := fmt.Sprintf("organizations/%s/capacity", url.QueryEscape(organization))
|
||||||
|
@ -291,7 +291,7 @@ type RunQueueOptions struct {
|
||||||
// RunQueue shows the current run queue of an organization.
|
// RunQueue shows the current run queue of an organization.
|
||||||
func (s *organizations) RunQueue(ctx context.Context, organization string, options RunQueueOptions) (*RunQueue, error) {
|
func (s *organizations) RunQueue(ctx context.Context, organization string, options RunQueueOptions) (*RunQueue, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("organizations/%s/runs/queue", url.QueryEscape(organization))
|
u := fmt.Sprintf("organizations/%s/runs/queue", url.QueryEscape(organization))
|
||||||
|
|
|
@ -44,7 +44,7 @@ type OrganizationToken struct {
|
||||||
// Generate a new organization token, replacing any existing token.
|
// Generate a new organization token, replacing any existing token.
|
||||||
func (s *organizationTokens) Generate(ctx context.Context, organization string) (*OrganizationToken, error) {
|
func (s *organizationTokens) Generate(ctx context.Context, organization string) (*OrganizationToken, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization))
|
u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization))
|
||||||
|
@ -65,7 +65,7 @@ func (s *organizationTokens) Generate(ctx context.Context, organization string)
|
||||||
// Read an organization token.
|
// Read an organization token.
|
||||||
func (s *organizationTokens) Read(ctx context.Context, organization string) (*OrganizationToken, error) {
|
func (s *organizationTokens) Read(ctx context.Context, organization string) (*OrganizationToken, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization))
|
u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization))
|
||||||
|
@ -86,7 +86,7 @@ func (s *organizationTokens) Read(ctx context.Context, organization string) (*Or
|
||||||
// Delete an organization token.
|
// Delete an organization token.
|
||||||
func (s *organizationTokens) Delete(ctx context.Context, organization string) error {
|
func (s *organizationTokens) Delete(ctx context.Context, organization string) error {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return errors.New("Invalid value for organization")
|
return errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization))
|
u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization))
|
||||||
|
|
|
@ -70,7 +70,7 @@ type PlanStatusTimestamps struct {
|
||||||
// Read a plan by its ID.
|
// Read a plan by its ID.
|
||||||
func (s *plans) Read(ctx context.Context, planID string) (*Plan, error) {
|
func (s *plans) Read(ctx context.Context, planID string) (*Plan, error) {
|
||||||
if !validStringID(&planID) {
|
if !validStringID(&planID) {
|
||||||
return nil, errors.New("Invalid value for plan ID")
|
return nil, errors.New("invalid value for plan ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("plans/%s", url.QueryEscape(planID))
|
u := fmt.Sprintf("plans/%s", url.QueryEscape(planID))
|
||||||
|
@ -91,7 +91,7 @@ func (s *plans) Read(ctx context.Context, planID string) (*Plan, error) {
|
||||||
// Logs retrieves the logs of a plan.
|
// Logs retrieves the logs of a plan.
|
||||||
func (s *plans) Logs(ctx context.Context, planID string) (io.Reader, error) {
|
func (s *plans) Logs(ctx context.Context, planID string) (io.Reader, error) {
|
||||||
if !validStringID(&planID) {
|
if !validStringID(&planID) {
|
||||||
return nil, errors.New("Invalid value for plan ID")
|
return nil, errors.New("invalid value for plan ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the plan to make sure it exists.
|
// Get the plan to make sure it exists.
|
||||||
|
@ -102,12 +102,12 @@ func (s *plans) Logs(ctx context.Context, planID string) (io.Reader, error) {
|
||||||
|
|
||||||
// Return an error if the log URL is empty.
|
// Return an error if the log URL is empty.
|
||||||
if p.LogReadURL == "" {
|
if p.LogReadURL == "" {
|
||||||
return nil, fmt.Errorf("Plan %s does not have a log URL", planID)
|
return nil, fmt.Errorf("plan %s does not have a log URL", planID)
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.Parse(p.LogReadURL)
|
u, err := url.Parse(p.LogReadURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Invalid log URL: %v", err)
|
return nil, fmt.Errorf("invalid log URL: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
done := func() (bool, error) {
|
done := func() (bool, error) {
|
||||||
|
|
|
@ -62,10 +62,15 @@ type PolicyList struct {
|
||||||
|
|
||||||
// Policy represents a Terraform Enterprise policy.
|
// Policy represents a Terraform Enterprise policy.
|
||||||
type Policy struct {
|
type Policy struct {
|
||||||
ID string `jsonapi:"primary,policies"`
|
ID string `jsonapi:"primary,policies"`
|
||||||
Name string `jsonapi:"attr,name"`
|
Name string `jsonapi:"attr,name"`
|
||||||
Enforce []*Enforcement `jsonapi:"attr,enforce"`
|
Description string `jsonapi:"attr,description"`
|
||||||
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
|
Enforce []*Enforcement `jsonapi:"attr,enforce"`
|
||||||
|
PolicySetCount int `jsonapi:"attr,policy-set-count"`
|
||||||
|
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Organization *Organization `jsonapi:"relation,organization"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enforcement describes a enforcement.
|
// Enforcement describes a enforcement.
|
||||||
|
@ -77,12 +82,15 @@ type Enforcement struct {
|
||||||
// PolicyListOptions represents the options for listing policies.
|
// PolicyListOptions represents the options for listing policies.
|
||||||
type PolicyListOptions struct {
|
type PolicyListOptions struct {
|
||||||
ListOptions
|
ListOptions
|
||||||
|
|
||||||
|
// A search string (partial policy name) used to filter the results.
|
||||||
|
Search *string `url:"search[name],omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// List all the policies for a given organization
|
// List all the policies for a given organization
|
||||||
func (s *policies) List(ctx context.Context, organization string, options PolicyListOptions) (*PolicyList, error) {
|
func (s *policies) List(ctx context.Context, organization string, options PolicyListOptions) (*PolicyList, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("organizations/%s/policies", url.QueryEscape(organization))
|
u := fmt.Sprintf("organizations/%s/policies", url.QueryEscape(organization))
|
||||||
|
@ -108,6 +116,9 @@ type PolicyCreateOptions struct {
|
||||||
// The name of the policy.
|
// The name of the policy.
|
||||||
Name *string `jsonapi:"attr,name"`
|
Name *string `jsonapi:"attr,name"`
|
||||||
|
|
||||||
|
// A description of the policy's purpose.
|
||||||
|
Description *string `jsonapi:"attr,description,omitempty"`
|
||||||
|
|
||||||
// The enforcements of the policy.
|
// The enforcements of the policy.
|
||||||
Enforce []*EnforcementOptions `jsonapi:"attr,enforce"`
|
Enforce []*EnforcementOptions `jsonapi:"attr,enforce"`
|
||||||
}
|
}
|
||||||
|
@ -120,20 +131,20 @@ type EnforcementOptions struct {
|
||||||
|
|
||||||
func (o PolicyCreateOptions) valid() error {
|
func (o PolicyCreateOptions) valid() error {
|
||||||
if !validString(o.Name) {
|
if !validString(o.Name) {
|
||||||
return errors.New("Name is required")
|
return errors.New("name is required")
|
||||||
}
|
}
|
||||||
if !validStringID(o.Name) {
|
if !validStringID(o.Name) {
|
||||||
return errors.New("Invalid value for name")
|
return errors.New("invalid value for name")
|
||||||
}
|
}
|
||||||
if o.Enforce == nil {
|
if o.Enforce == nil {
|
||||||
return errors.New("Enforce is required")
|
return errors.New("enforce is required")
|
||||||
}
|
}
|
||||||
for _, e := range o.Enforce {
|
for _, e := range o.Enforce {
|
||||||
if !validString(e.Path) {
|
if !validString(e.Path) {
|
||||||
return errors.New("Enforcement path is required")
|
return errors.New("enforcement path is required")
|
||||||
}
|
}
|
||||||
if e.Mode == nil {
|
if e.Mode == nil {
|
||||||
return errors.New("Enforcement mode is required")
|
return errors.New("enforcement mode is required")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -142,7 +153,7 @@ func (o PolicyCreateOptions) valid() error {
|
||||||
// Create a policy and associate it with an organization.
|
// Create a policy and associate it with an organization.
|
||||||
func (s *policies) Create(ctx context.Context, organization string, options PolicyCreateOptions) (*Policy, error) {
|
func (s *policies) Create(ctx context.Context, organization string, options PolicyCreateOptions) (*Policy, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
if err := options.valid(); err != nil {
|
if err := options.valid(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -169,7 +180,7 @@ func (s *policies) Create(ctx context.Context, organization string, options Poli
|
||||||
// Read a policy by its ID.
|
// Read a policy by its ID.
|
||||||
func (s *policies) Read(ctx context.Context, policyID string) (*Policy, error) {
|
func (s *policies) Read(ctx context.Context, policyID string) (*Policy, error) {
|
||||||
if !validStringID(&policyID) {
|
if !validStringID(&policyID) {
|
||||||
return nil, errors.New("Invalid value for policy ID")
|
return nil, errors.New("invalid value for policy ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("policies/%s", url.QueryEscape(policyID))
|
u := fmt.Sprintf("policies/%s", url.QueryEscape(policyID))
|
||||||
|
@ -192,24 +203,17 @@ type PolicyUpdateOptions struct {
|
||||||
// For internal use only!
|
// For internal use only!
|
||||||
ID string `jsonapi:"primary,policies"`
|
ID string `jsonapi:"primary,policies"`
|
||||||
|
|
||||||
// The enforcements of the policy.
|
// A description of the policy's purpose.
|
||||||
Enforce []*EnforcementOptions `jsonapi:"attr,enforce"`
|
Description *string `jsonapi:"attr,description,omitempty"`
|
||||||
}
|
|
||||||
|
|
||||||
func (o PolicyUpdateOptions) valid() error {
|
// The enforcements of the policy.
|
||||||
if o.Enforce == nil {
|
Enforce []*EnforcementOptions `jsonapi:"attr,enforce,omitempty"`
|
||||||
return errors.New("Enforce is required")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update an existing policy.
|
// Update an existing policy.
|
||||||
func (s *policies) Update(ctx context.Context, policyID string, options PolicyUpdateOptions) (*Policy, error) {
|
func (s *policies) Update(ctx context.Context, policyID string, options PolicyUpdateOptions) (*Policy, error) {
|
||||||
if !validStringID(&policyID) {
|
if !validStringID(&policyID) {
|
||||||
return nil, errors.New("Invalid value for policy ID")
|
return nil, errors.New("invalid value for policy ID")
|
||||||
}
|
|
||||||
if err := options.valid(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we don't send a user provided ID.
|
// Make sure we don't send a user provided ID.
|
||||||
|
@ -233,7 +237,7 @@ func (s *policies) Update(ctx context.Context, policyID string, options PolicyUp
|
||||||
// Delete a policy by its ID.
|
// Delete a policy by its ID.
|
||||||
func (s *policies) Delete(ctx context.Context, policyID string) error {
|
func (s *policies) Delete(ctx context.Context, policyID string) error {
|
||||||
if !validStringID(&policyID) {
|
if !validStringID(&policyID) {
|
||||||
return errors.New("Invalid value for policy ID")
|
return errors.New("invalid value for policy ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("policies/%s", url.QueryEscape(policyID))
|
u := fmt.Sprintf("policies/%s", url.QueryEscape(policyID))
|
||||||
|
@ -248,7 +252,7 @@ func (s *policies) Delete(ctx context.Context, policyID string) error {
|
||||||
// Upload the policy content of the policy.
|
// Upload the policy content of the policy.
|
||||||
func (s *policies) Upload(ctx context.Context, policyID string, content []byte) error {
|
func (s *policies) Upload(ctx context.Context, policyID string, content []byte) error {
|
||||||
if !validStringID(&policyID) {
|
if !validStringID(&policyID) {
|
||||||
return errors.New("Invalid value for policy ID")
|
return errors.New("invalid value for policy ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("policies/%s/upload", url.QueryEscape(policyID))
|
u := fmt.Sprintf("policies/%s/upload", url.QueryEscape(policyID))
|
||||||
|
@ -263,7 +267,7 @@ func (s *policies) Upload(ctx context.Context, policyID string, content []byte)
|
||||||
// Download the policy content of the policy.
|
// Download the policy content of the policy.
|
||||||
func (s *policies) Download(ctx context.Context, policyID string) ([]byte, error) {
|
func (s *policies) Download(ctx context.Context, policyID string) ([]byte, error) {
|
||||||
if !validStringID(&policyID) {
|
if !validStringID(&policyID) {
|
||||||
return nil, errors.New("Invalid value for policy ID")
|
return nil, errors.New("invalid value for policy ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("policies/%s/download", url.QueryEscape(policyID))
|
u := fmt.Sprintf("policies/%s/download", url.QueryEscape(policyID))
|
||||||
|
|
|
@ -118,7 +118,7 @@ type PolicyCheckListOptions struct {
|
||||||
// List all policy checks of the given run.
|
// List all policy checks of the given run.
|
||||||
func (s *policyChecks) List(ctx context.Context, runID string, options PolicyCheckListOptions) (*PolicyCheckList, error) {
|
func (s *policyChecks) List(ctx context.Context, runID string, options PolicyCheckListOptions) (*PolicyCheckList, error) {
|
||||||
if !validStringID(&runID) {
|
if !validStringID(&runID) {
|
||||||
return nil, errors.New("Invalid value for run ID")
|
return nil, errors.New("invalid value for run ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("runs/%s/policy-checks", url.QueryEscape(runID))
|
u := fmt.Sprintf("runs/%s/policy-checks", url.QueryEscape(runID))
|
||||||
|
@ -139,7 +139,7 @@ func (s *policyChecks) List(ctx context.Context, runID string, options PolicyChe
|
||||||
// Read a policy check by its ID.
|
// Read a policy check by its ID.
|
||||||
func (s *policyChecks) Read(ctx context.Context, policyCheckID string) (*PolicyCheck, error) {
|
func (s *policyChecks) Read(ctx context.Context, policyCheckID string) (*PolicyCheck, error) {
|
||||||
if !validStringID(&policyCheckID) {
|
if !validStringID(&policyCheckID) {
|
||||||
return nil, errors.New("Invalid value for policy check ID")
|
return nil, errors.New("invalid value for policy check ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("policy-checks/%s", url.QueryEscape(policyCheckID))
|
u := fmt.Sprintf("policy-checks/%s", url.QueryEscape(policyCheckID))
|
||||||
|
@ -160,7 +160,7 @@ func (s *policyChecks) Read(ctx context.Context, policyCheckID string) (*PolicyC
|
||||||
// Override a soft-mandatory or warning policy.
|
// Override a soft-mandatory or warning policy.
|
||||||
func (s *policyChecks) Override(ctx context.Context, policyCheckID string) (*PolicyCheck, error) {
|
func (s *policyChecks) Override(ctx context.Context, policyCheckID string) (*PolicyCheck, error) {
|
||||||
if !validStringID(&policyCheckID) {
|
if !validStringID(&policyCheckID) {
|
||||||
return nil, errors.New("Invalid value for policy check ID")
|
return nil, errors.New("invalid value for policy check ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("policy-checks/%s/actions/override", url.QueryEscape(policyCheckID))
|
u := fmt.Sprintf("policy-checks/%s/actions/override", url.QueryEscape(policyCheckID))
|
||||||
|
@ -181,7 +181,7 @@ func (s *policyChecks) Override(ctx context.Context, policyCheckID string) (*Pol
|
||||||
// Logs retrieves the logs of a policy check.
|
// Logs retrieves the logs of a policy check.
|
||||||
func (s *policyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reader, error) {
|
func (s *policyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reader, error) {
|
||||||
if !validStringID(&policyCheckID) {
|
if !validStringID(&policyCheckID) {
|
||||||
return nil, errors.New("Invalid value for policy check ID")
|
return nil, errors.New("invalid value for policy check ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop until the context is canceled or the policy check is finished
|
// Loop until the context is canceled or the policy check is finished
|
||||||
|
|
|
@ -0,0 +1,381 @@
|
||||||
|
package tfe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Compile-time proof of interface implementation.
|
||||||
|
var _ PolicySets = (*policySets)(nil)
|
||||||
|
|
||||||
|
// PolicySets describes all the policy set related methods that the Terraform
|
||||||
|
// Enterprise API supports.
|
||||||
|
//
|
||||||
|
// TFE API docs: https://www.terraform.io/docs/enterprise/api/policies.html
|
||||||
|
type PolicySets interface {
|
||||||
|
// List all the policy sets for a given organization
|
||||||
|
List(ctx context.Context, organization string, options PolicySetListOptions) (*PolicySetList, error)
|
||||||
|
|
||||||
|
// Create a policy set and associate it with an organization.
|
||||||
|
Create(ctx context.Context, organization string, options PolicySetCreateOptions) (*PolicySet, error)
|
||||||
|
|
||||||
|
// Read a policy set by its ID.
|
||||||
|
Read(ctx context.Context, policySetID string) (*PolicySet, error)
|
||||||
|
|
||||||
|
// Update an existing policy set.
|
||||||
|
Update(ctx context.Context, policySetID string, options PolicySetUpdateOptions) (*PolicySet, error)
|
||||||
|
|
||||||
|
// Add policies to a policy set.
|
||||||
|
AddPolicies(ctx context.Context, policySetID string, options PolicySetAddPoliciesOptions) error
|
||||||
|
|
||||||
|
// Remove policies from a policy set.
|
||||||
|
RemovePolicies(ctx context.Context, policySetID string, options PolicySetRemovePoliciesOptions) error
|
||||||
|
|
||||||
|
// Attach a policy set to workspaces.
|
||||||
|
AttachToWorkspaces(ctx context.Context, policySetID string, options PolicySetAttachToWorkspacesOptions) error
|
||||||
|
|
||||||
|
// Detach a policy set from workspaces.
|
||||||
|
DetachFromWorkspaces(ctx context.Context, policySetID string, options PolicySetDetachFromWorkspacesOptions) error
|
||||||
|
|
||||||
|
// Delete a policy set by its ID.
|
||||||
|
Delete(ctx context.Context, policyID string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// policySets implements PolicySets.
|
||||||
|
type policySets struct {
|
||||||
|
client *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicySetList represents a list of policy sets..
|
||||||
|
type PolicySetList struct {
|
||||||
|
*Pagination
|
||||||
|
Items []*PolicySet
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicySet represents a Terraform Enterprise policy set.
|
||||||
|
type PolicySet struct {
|
||||||
|
ID string `jsonapi:"primary,policy-sets"`
|
||||||
|
Name string `jsonapi:"attr,name"`
|
||||||
|
Description string `jsonapi:"attr,description"`
|
||||||
|
Global bool `jsonapi:"attr,global"`
|
||||||
|
PolicyCount int `jsonapi:"attr,policy-count"`
|
||||||
|
WorkspaceCount int `jsonapi:"attr,workspace-count"`
|
||||||
|
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
||||||
|
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Organization *Organization `jsonapi:"relation,organization"`
|
||||||
|
Policies []*Policy `jsonapi:"relation,policies"`
|
||||||
|
Workspaces []*Workspace `jsonapi:"relation,workspaces"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicySetListOptions represents the options for listing policy sets.
|
||||||
|
type PolicySetListOptions struct {
|
||||||
|
ListOptions
|
||||||
|
|
||||||
|
// A search string (partial policy set name) used to filter the results.
|
||||||
|
Search *string `url:"search[name],omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all the policies for a given organization
|
||||||
|
func (s *policySets) List(ctx context.Context, organization string, options PolicySetListOptions) (*PolicySetList, error) {
|
||||||
|
if !validStringID(&organization) {
|
||||||
|
return nil, errors.New("invalid value for organization")
|
||||||
|
}
|
||||||
|
|
||||||
|
u := fmt.Sprintf("organizations/%s/policy-sets", url.QueryEscape(organization))
|
||||||
|
req, err := s.client.newRequest("GET", u, &options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
psl := &PolicySetList{}
|
||||||
|
err = s.client.do(ctx, req, psl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return psl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicySetCreateOptions represents the options for creating a new policy set.
|
||||||
|
type PolicySetCreateOptions struct {
|
||||||
|
// For internal use only!
|
||||||
|
ID string `jsonapi:"primary,policy-sets"`
|
||||||
|
|
||||||
|
// The name of the policy set.
|
||||||
|
Name *string `jsonapi:"attr,name"`
|
||||||
|
|
||||||
|
// The description of the policy set.
|
||||||
|
Description *string `jsonapi:"attr,description,omitempty"`
|
||||||
|
|
||||||
|
// Whether or not the policy set is global.
|
||||||
|
Global *bool `jsonapi:"attr,global,omitempty"`
|
||||||
|
|
||||||
|
// The initial members of the policy set.
|
||||||
|
Policies []*Policy `jsonapi:"relation,policies,omitempty"`
|
||||||
|
|
||||||
|
// The initial list of workspaces the policy set should be attached to.
|
||||||
|
Workspaces []*Workspace `jsonapi:"relation,workspaces,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o PolicySetCreateOptions) valid() error {
|
||||||
|
if !validString(o.Name) {
|
||||||
|
return errors.New("name is required")
|
||||||
|
}
|
||||||
|
if !validStringID(o.Name) {
|
||||||
|
return errors.New("invalid value for name")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a policy set and associate it with an organization.
|
||||||
|
func (s *policySets) Create(ctx context.Context, organization string, options PolicySetCreateOptions) (*PolicySet, error) {
|
||||||
|
if !validStringID(&organization) {
|
||||||
|
return nil, errors.New("invalid value for organization")
|
||||||
|
}
|
||||||
|
if err := options.valid(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we don't send a user provided ID.
|
||||||
|
options.ID = ""
|
||||||
|
|
||||||
|
u := fmt.Sprintf("organizations/%s/policy-sets", url.QueryEscape(organization))
|
||||||
|
req, err := s.client.newRequest("POST", u, &options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := &PolicySet{}
|
||||||
|
err = s.client.do(ctx, req, ps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ps, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a policy set by its ID.
|
||||||
|
func (s *policySets) Read(ctx context.Context, policySetID string) (*PolicySet, error) {
|
||||||
|
if !validStringID(&policySetID) {
|
||||||
|
return nil, errors.New("invalid value for policy set ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
u := fmt.Sprintf("policy-sets/%s", url.QueryEscape(policySetID))
|
||||||
|
req, err := s.client.newRequest("GET", u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := &PolicySet{}
|
||||||
|
err = s.client.do(ctx, req, ps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ps, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicySetUpdateOptions represents the options for updating a policy set.
|
||||||
|
type PolicySetUpdateOptions struct {
|
||||||
|
// For internal use only!
|
||||||
|
ID string `jsonapi:"primary,policy-sets"`
|
||||||
|
|
||||||
|
/// The name of the policy set.
|
||||||
|
Name *string `jsonapi:"attr,name,omitempty"`
|
||||||
|
|
||||||
|
// The description of the policy set.
|
||||||
|
Description *string `jsonapi:"attr,description,omitempty"`
|
||||||
|
|
||||||
|
// Whether or not the policy set is global.
|
||||||
|
Global *bool `jsonapi:"attr,global,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o PolicySetUpdateOptions) valid() error {
|
||||||
|
if o.Name != nil && !validStringID(o.Name) {
|
||||||
|
return errors.New("invalid value for name")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update an existing policy set.
|
||||||
|
func (s *policySets) Update(ctx context.Context, policySetID string, options PolicySetUpdateOptions) (*PolicySet, error) {
|
||||||
|
if !validStringID(&policySetID) {
|
||||||
|
return nil, errors.New("invalid value for policy set ID")
|
||||||
|
}
|
||||||
|
if err := options.valid(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we don't send a user provided ID.
|
||||||
|
options.ID = ""
|
||||||
|
|
||||||
|
u := fmt.Sprintf("policy-sets/%s", url.QueryEscape(policySetID))
|
||||||
|
req, err := s.client.newRequest("PATCH", u, &options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := &PolicySet{}
|
||||||
|
err = s.client.do(ctx, req, ps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ps, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicySetAddPoliciesOptions represents the options for adding policies to a policy set.
|
||||||
|
type PolicySetAddPoliciesOptions struct {
|
||||||
|
/// The policies to add to the policy set.
|
||||||
|
Policies []*Policy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o PolicySetAddPoliciesOptions) valid() error {
|
||||||
|
if o.Policies == nil {
|
||||||
|
return errors.New("policies is required")
|
||||||
|
}
|
||||||
|
if len(o.Policies) == 0 {
|
||||||
|
return errors.New("must provide at least one policy")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add policies to a policy set
|
||||||
|
func (s *policySets) AddPolicies(ctx context.Context, policySetID string, options PolicySetAddPoliciesOptions) error {
|
||||||
|
if !validStringID(&policySetID) {
|
||||||
|
return errors.New("invalid value for policy set ID")
|
||||||
|
}
|
||||||
|
if err := options.valid(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
u := fmt.Sprintf("policy-sets/%s/relationships/policies", url.QueryEscape(policySetID))
|
||||||
|
req, err := s.client.newRequest("POST", u, options.Policies)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.client.do(ctx, req, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicySetRemovePoliciesOptions represents the options for removing policies from a policy set.
|
||||||
|
type PolicySetRemovePoliciesOptions struct {
|
||||||
|
/// The policies to remove from the policy set.
|
||||||
|
Policies []*Policy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o PolicySetRemovePoliciesOptions) valid() error {
|
||||||
|
if o.Policies == nil {
|
||||||
|
return errors.New("policies is required")
|
||||||
|
}
|
||||||
|
if len(o.Policies) == 0 {
|
||||||
|
return errors.New("must provide at least one policy")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove policies from a policy set
|
||||||
|
func (s *policySets) RemovePolicies(ctx context.Context, policySetID string, options PolicySetRemovePoliciesOptions) error {
|
||||||
|
if !validStringID(&policySetID) {
|
||||||
|
return errors.New("invalid value for policy set ID")
|
||||||
|
}
|
||||||
|
if err := options.valid(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
u := fmt.Sprintf("policy-sets/%s/relationships/policies", url.QueryEscape(policySetID))
|
||||||
|
req, err := s.client.newRequest("DELETE", u, options.Policies)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.client.do(ctx, req, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicySetAttachToWorkspacesOptions represents the options for attaching a policy set to workspaces.
|
||||||
|
type PolicySetAttachToWorkspacesOptions struct {
|
||||||
|
/// The workspaces on which to attach the policy set.
|
||||||
|
Workspaces []*Workspace
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o PolicySetAttachToWorkspacesOptions) valid() error {
|
||||||
|
if o.Workspaces == nil {
|
||||||
|
return errors.New("workspaces is required")
|
||||||
|
}
|
||||||
|
if len(o.Workspaces) == 0 {
|
||||||
|
return errors.New("must provide at least one workspace")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach a policy set to workspaces
|
||||||
|
func (s *policySets) AttachToWorkspaces(ctx context.Context, policySetID string, options PolicySetAttachToWorkspacesOptions) error {
|
||||||
|
if !validStringID(&policySetID) {
|
||||||
|
return errors.New("invalid value for policy set ID")
|
||||||
|
}
|
||||||
|
if err := options.valid(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
u := fmt.Sprintf("policy-sets/%s/relationships/workspaces", url.QueryEscape(policySetID))
|
||||||
|
req, err := s.client.newRequest("POST", u, options.Workspaces)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.client.do(ctx, req, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicySetDetachFromWorkspacesOptions represents the options for detaching a policy set from workspaces.
|
||||||
|
type PolicySetDetachFromWorkspacesOptions struct {
|
||||||
|
/// The workspaces from which to detach the policy set.
|
||||||
|
Workspaces []*Workspace
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o PolicySetDetachFromWorkspacesOptions) valid() error {
|
||||||
|
if o.Workspaces == nil {
|
||||||
|
return errors.New("workspaces is required")
|
||||||
|
}
|
||||||
|
if len(o.Workspaces) == 0 {
|
||||||
|
return errors.New("must provide at least one workspace")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detach a policy set from workspaces
|
||||||
|
func (s *policySets) DetachFromWorkspaces(ctx context.Context, policySetID string, options PolicySetDetachFromWorkspacesOptions) error {
|
||||||
|
if !validStringID(&policySetID) {
|
||||||
|
return errors.New("invalid value for policy set ID")
|
||||||
|
}
|
||||||
|
if err := options.valid(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
u := fmt.Sprintf("policy-sets/%s/relationships/workspaces", url.QueryEscape(policySetID))
|
||||||
|
req, err := s.client.newRequest("DELETE", u, options.Workspaces)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.client.do(ctx, req, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a policy set by its ID.
|
||||||
|
func (s *policySets) Delete(ctx context.Context, policySetID string) error {
|
||||||
|
if !validStringID(&policySetID) {
|
||||||
|
return errors.New("invalid value for policy set ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
u := fmt.Sprintf("policy-sets/%s", url.QueryEscape(policySetID))
|
||||||
|
req, err := s.client.newRequest("DELETE", u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.client.do(ctx, req, nil)
|
||||||
|
}
|
|
@ -136,7 +136,7 @@ type RunListOptions struct {
|
||||||
// List all the runs of the given workspace.
|
// List all the runs of the given workspace.
|
||||||
func (s *runs) List(ctx context.Context, workspaceID string, options RunListOptions) (*RunList, error) {
|
func (s *runs) List(ctx context.Context, workspaceID string, options RunListOptions) (*RunList, error) {
|
||||||
if !validStringID(&workspaceID) {
|
if !validStringID(&workspaceID) {
|
||||||
return nil, errors.New("Invalid value for workspace ID")
|
return nil, errors.New("invalid value for workspace ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("workspaces/%s/runs", url.QueryEscape(workspaceID))
|
u := fmt.Sprintf("workspaces/%s/runs", url.QueryEscape(workspaceID))
|
||||||
|
@ -177,7 +177,7 @@ type RunCreateOptions struct {
|
||||||
|
|
||||||
func (o RunCreateOptions) valid() error {
|
func (o RunCreateOptions) valid() error {
|
||||||
if o.Workspace == nil {
|
if o.Workspace == nil {
|
||||||
return errors.New("Workspace is required")
|
return errors.New("workspace is required")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -208,7 +208,7 @@ func (s *runs) Create(ctx context.Context, options RunCreateOptions) (*Run, erro
|
||||||
// Read a run by its ID.
|
// Read a run by its ID.
|
||||||
func (s *runs) Read(ctx context.Context, runID string) (*Run, error) {
|
func (s *runs) Read(ctx context.Context, runID string) (*Run, error) {
|
||||||
if !validStringID(&runID) {
|
if !validStringID(&runID) {
|
||||||
return nil, errors.New("Invalid value for run ID")
|
return nil, errors.New("invalid value for run ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("runs/%s", url.QueryEscape(runID))
|
u := fmt.Sprintf("runs/%s", url.QueryEscape(runID))
|
||||||
|
@ -235,7 +235,7 @@ type RunApplyOptions struct {
|
||||||
// Apply a run by its ID.
|
// Apply a run by its ID.
|
||||||
func (s *runs) Apply(ctx context.Context, runID string, options RunApplyOptions) error {
|
func (s *runs) Apply(ctx context.Context, runID string, options RunApplyOptions) error {
|
||||||
if !validStringID(&runID) {
|
if !validStringID(&runID) {
|
||||||
return errors.New("Invalid value for run ID")
|
return errors.New("invalid value for run ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("runs/%s/actions/apply", url.QueryEscape(runID))
|
u := fmt.Sprintf("runs/%s/actions/apply", url.QueryEscape(runID))
|
||||||
|
@ -256,7 +256,7 @@ type RunCancelOptions struct {
|
||||||
// Cancel a run by its ID.
|
// Cancel a run by its ID.
|
||||||
func (s *runs) Cancel(ctx context.Context, runID string, options RunCancelOptions) error {
|
func (s *runs) Cancel(ctx context.Context, runID string, options RunCancelOptions) error {
|
||||||
if !validStringID(&runID) {
|
if !validStringID(&runID) {
|
||||||
return errors.New("Invalid value for run ID")
|
return errors.New("invalid value for run ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("runs/%s/actions/cancel", url.QueryEscape(runID))
|
u := fmt.Sprintf("runs/%s/actions/cancel", url.QueryEscape(runID))
|
||||||
|
@ -277,7 +277,7 @@ type RunForceCancelOptions struct {
|
||||||
// ForceCancel is used to forcefully cancel a run by its ID.
|
// ForceCancel is used to forcefully cancel a run by its ID.
|
||||||
func (s *runs) ForceCancel(ctx context.Context, runID string, options RunForceCancelOptions) error {
|
func (s *runs) ForceCancel(ctx context.Context, runID string, options RunForceCancelOptions) error {
|
||||||
if !validStringID(&runID) {
|
if !validStringID(&runID) {
|
||||||
return errors.New("Invalid value for run ID")
|
return errors.New("invalid value for run ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("runs/%s/actions/force-cancel", url.QueryEscape(runID))
|
u := fmt.Sprintf("runs/%s/actions/force-cancel", url.QueryEscape(runID))
|
||||||
|
@ -298,7 +298,7 @@ type RunDiscardOptions struct {
|
||||||
// Discard a run by its ID.
|
// Discard a run by its ID.
|
||||||
func (s *runs) Discard(ctx context.Context, runID string, options RunDiscardOptions) error {
|
func (s *runs) Discard(ctx context.Context, runID string, options RunDiscardOptions) error {
|
||||||
if !validStringID(&runID) {
|
if !validStringID(&runID) {
|
||||||
return errors.New("Invalid value for run ID")
|
return errors.New("invalid value for run ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("runs/%s/actions/discard", url.QueryEscape(runID))
|
u := fmt.Sprintf("runs/%s/actions/discard", url.QueryEscape(runID))
|
||||||
|
|
|
@ -57,7 +57,7 @@ type SSHKeyListOptions struct {
|
||||||
// List all the SSH keys for a given organization
|
// List all the SSH keys for a given organization
|
||||||
func (s *sshKeys) List(ctx context.Context, organization string, options SSHKeyListOptions) (*SSHKeyList, error) {
|
func (s *sshKeys) List(ctx context.Context, organization string, options SSHKeyListOptions) (*SSHKeyList, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("organizations/%s/ssh-keys", url.QueryEscape(organization))
|
u := fmt.Sprintf("organizations/%s/ssh-keys", url.QueryEscape(organization))
|
||||||
|
@ -89,10 +89,10 @@ type SSHKeyCreateOptions struct {
|
||||||
|
|
||||||
func (o SSHKeyCreateOptions) valid() error {
|
func (o SSHKeyCreateOptions) valid() error {
|
||||||
if !validString(o.Name) {
|
if !validString(o.Name) {
|
||||||
return errors.New("Name is required")
|
return errors.New("name is required")
|
||||||
}
|
}
|
||||||
if !validString(o.Value) {
|
if !validString(o.Value) {
|
||||||
return errors.New("Value is required")
|
return errors.New("value is required")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,7 @@ func (o SSHKeyCreateOptions) valid() error {
|
||||||
// Create an SSH key and associate it with an organization.
|
// Create an SSH key and associate it with an organization.
|
||||||
func (s *sshKeys) Create(ctx context.Context, organization string, options SSHKeyCreateOptions) (*SSHKey, error) {
|
func (s *sshKeys) Create(ctx context.Context, organization string, options SSHKeyCreateOptions) (*SSHKey, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := options.valid(); err != nil {
|
if err := options.valid(); err != nil {
|
||||||
|
@ -128,7 +128,7 @@ func (s *sshKeys) Create(ctx context.Context, organization string, options SSHKe
|
||||||
// Read an SSH key by its ID.
|
// Read an SSH key by its ID.
|
||||||
func (s *sshKeys) Read(ctx context.Context, sshKeyID string) (*SSHKey, error) {
|
func (s *sshKeys) Read(ctx context.Context, sshKeyID string) (*SSHKey, error) {
|
||||||
if !validStringID(&sshKeyID) {
|
if !validStringID(&sshKeyID) {
|
||||||
return nil, errors.New("Invalid value for SSH key ID")
|
return nil, errors.New("invalid value for SSH key ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("ssh-keys/%s", url.QueryEscape(sshKeyID))
|
u := fmt.Sprintf("ssh-keys/%s", url.QueryEscape(sshKeyID))
|
||||||
|
@ -161,7 +161,7 @@ type SSHKeyUpdateOptions struct {
|
||||||
// Update an SSH key by its ID.
|
// Update an SSH key by its ID.
|
||||||
func (s *sshKeys) Update(ctx context.Context, sshKeyID string, options SSHKeyUpdateOptions) (*SSHKey, error) {
|
func (s *sshKeys) Update(ctx context.Context, sshKeyID string, options SSHKeyUpdateOptions) (*SSHKey, error) {
|
||||||
if !validStringID(&sshKeyID) {
|
if !validStringID(&sshKeyID) {
|
||||||
return nil, errors.New("Invalid value for SSH key ID")
|
return nil, errors.New("invalid value for SSH key ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we don't send a user provided ID.
|
// Make sure we don't send a user provided ID.
|
||||||
|
@ -185,7 +185,7 @@ func (s *sshKeys) Update(ctx context.Context, sshKeyID string, options SSHKeyUpd
|
||||||
// Delete an SSH key by its ID.
|
// Delete an SSH key by its ID.
|
||||||
func (s *sshKeys) Delete(ctx context.Context, sshKeyID string) error {
|
func (s *sshKeys) Delete(ctx context.Context, sshKeyID string) error {
|
||||||
if !validStringID(&sshKeyID) {
|
if !validStringID(&sshKeyID) {
|
||||||
return errors.New("Invalid value for SSH key ID")
|
return errors.New("invalid value for SSH key ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("ssh-keys/%s", url.QueryEscape(sshKeyID))
|
u := fmt.Sprintf("ssh-keys/%s", url.QueryEscape(sshKeyID))
|
||||||
|
|
|
@ -67,10 +67,10 @@ type StateVersionListOptions struct {
|
||||||
|
|
||||||
func (o StateVersionListOptions) valid() error {
|
func (o StateVersionListOptions) valid() error {
|
||||||
if !validString(o.Organization) {
|
if !validString(o.Organization) {
|
||||||
return errors.New("Organization is required")
|
return errors.New("organization is required")
|
||||||
}
|
}
|
||||||
if !validString(o.Workspace) {
|
if !validString(o.Workspace) {
|
||||||
return errors.New("Workspace is required")
|
return errors.New("workspace is required")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -121,10 +121,10 @@ func (o StateVersionCreateOptions) valid() error {
|
||||||
return errors.New("MD5 is required")
|
return errors.New("MD5 is required")
|
||||||
}
|
}
|
||||||
if o.Serial == nil {
|
if o.Serial == nil {
|
||||||
return errors.New("Serial is required")
|
return errors.New("serial is required")
|
||||||
}
|
}
|
||||||
if !validString(o.State) {
|
if !validString(o.State) {
|
||||||
return errors.New("State is required")
|
return errors.New("state is required")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,7 @@ func (o StateVersionCreateOptions) valid() error {
|
||||||
// Create a new state version for the given workspace.
|
// Create a new state version for the given workspace.
|
||||||
func (s *stateVersions) Create(ctx context.Context, workspaceID string, options StateVersionCreateOptions) (*StateVersion, error) {
|
func (s *stateVersions) Create(ctx context.Context, workspaceID string, options StateVersionCreateOptions) (*StateVersion, error) {
|
||||||
if !validStringID(&workspaceID) {
|
if !validStringID(&workspaceID) {
|
||||||
return nil, errors.New("Invalid value for workspace ID")
|
return nil, errors.New("invalid value for workspace ID")
|
||||||
}
|
}
|
||||||
if err := options.valid(); err != nil {
|
if err := options.valid(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -159,7 +159,7 @@ func (s *stateVersions) Create(ctx context.Context, workspaceID string, options
|
||||||
// Read a state version by its ID.
|
// Read a state version by its ID.
|
||||||
func (s *stateVersions) Read(ctx context.Context, svID string) (*StateVersion, error) {
|
func (s *stateVersions) Read(ctx context.Context, svID string) (*StateVersion, error) {
|
||||||
if !validStringID(&svID) {
|
if !validStringID(&svID) {
|
||||||
return nil, errors.New("Invalid value for state version ID")
|
return nil, errors.New("invalid value for state version ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("state-versions/%s", url.QueryEscape(svID))
|
u := fmt.Sprintf("state-versions/%s", url.QueryEscape(svID))
|
||||||
|
@ -180,7 +180,7 @@ func (s *stateVersions) Read(ctx context.Context, svID string) (*StateVersion, e
|
||||||
// Current reads the latest available state from the given workspace.
|
// Current reads the latest available state from the given workspace.
|
||||||
func (s *stateVersions) Current(ctx context.Context, workspaceID string) (*StateVersion, error) {
|
func (s *stateVersions) Current(ctx context.Context, workspaceID string) (*StateVersion, error) {
|
||||||
if !validStringID(&workspaceID) {
|
if !validStringID(&workspaceID) {
|
||||||
return nil, errors.New("Invalid value for workspace ID")
|
return nil, errors.New("invalid value for workspace ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("workspaces/%s/current-state-version", url.QueryEscape(workspaceID))
|
u := fmt.Sprintf("workspaces/%s/current-state-version", url.QueryEscape(workspaceID))
|
||||||
|
|
|
@ -64,7 +64,7 @@ type TeamListOptions struct {
|
||||||
// List all the teams of the given organization.
|
// List all the teams of the given organization.
|
||||||
func (s *teams) List(ctx context.Context, organization string, options TeamListOptions) (*TeamList, error) {
|
func (s *teams) List(ctx context.Context, organization string, options TeamListOptions) (*TeamList, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("organizations/%s/teams", url.QueryEscape(organization))
|
u := fmt.Sprintf("organizations/%s/teams", url.QueryEscape(organization))
|
||||||
|
@ -93,10 +93,10 @@ type TeamCreateOptions struct {
|
||||||
|
|
||||||
func (o TeamCreateOptions) valid() error {
|
func (o TeamCreateOptions) valid() error {
|
||||||
if !validString(o.Name) {
|
if !validString(o.Name) {
|
||||||
return errors.New("Name is required")
|
return errors.New("name is required")
|
||||||
}
|
}
|
||||||
if !validStringID(o.Name) {
|
if !validStringID(o.Name) {
|
||||||
return errors.New("Invalid value for name")
|
return errors.New("invalid value for name")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,7 @@ func (o TeamCreateOptions) valid() error {
|
||||||
// Create a new team with the given options.
|
// Create a new team with the given options.
|
||||||
func (s *teams) Create(ctx context.Context, organization string, options TeamCreateOptions) (*Team, error) {
|
func (s *teams) Create(ctx context.Context, organization string, options TeamCreateOptions) (*Team, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
if err := options.valid(); err != nil {
|
if err := options.valid(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -131,7 +131,7 @@ func (s *teams) Create(ctx context.Context, organization string, options TeamCre
|
||||||
// Read a single team by its ID.
|
// Read a single team by its ID.
|
||||||
func (s *teams) Read(ctx context.Context, teamID string) (*Team, error) {
|
func (s *teams) Read(ctx context.Context, teamID string) (*Team, error) {
|
||||||
if !validStringID(&teamID) {
|
if !validStringID(&teamID) {
|
||||||
return nil, errors.New("Invalid value for team ID")
|
return nil, errors.New("invalid value for team ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID))
|
u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID))
|
||||||
|
@ -152,7 +152,7 @@ func (s *teams) Read(ctx context.Context, teamID string) (*Team, error) {
|
||||||
// Delete a team by its ID.
|
// Delete a team by its ID.
|
||||||
func (s *teams) Delete(ctx context.Context, teamID string) error {
|
func (s *teams) Delete(ctx context.Context, teamID string) error {
|
||||||
if !validStringID(&teamID) {
|
if !validStringID(&teamID) {
|
||||||
return errors.New("Invalid value for team ID")
|
return errors.New("invalid value for team ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID))
|
u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID))
|
||||||
|
|
|
@ -68,10 +68,10 @@ type TeamAccessListOptions struct {
|
||||||
|
|
||||||
func (o TeamAccessListOptions) valid() error {
|
func (o TeamAccessListOptions) valid() error {
|
||||||
if !validString(o.WorkspaceID) {
|
if !validString(o.WorkspaceID) {
|
||||||
return errors.New("Workspace ID is required")
|
return errors.New("workspace ID is required")
|
||||||
}
|
}
|
||||||
if !validStringID(o.WorkspaceID) {
|
if !validStringID(o.WorkspaceID) {
|
||||||
return errors.New("Invalid value for workspace ID")
|
return errors.New("invalid value for workspace ID")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -113,13 +113,13 @@ type TeamAccessAddOptions struct {
|
||||||
|
|
||||||
func (o TeamAccessAddOptions) valid() error {
|
func (o TeamAccessAddOptions) valid() error {
|
||||||
if o.Access == nil {
|
if o.Access == nil {
|
||||||
return errors.New("Access is required")
|
return errors.New("access is required")
|
||||||
}
|
}
|
||||||
if o.Team == nil {
|
if o.Team == nil {
|
||||||
return errors.New("Team is required")
|
return errors.New("team is required")
|
||||||
}
|
}
|
||||||
if o.Workspace == nil {
|
if o.Workspace == nil {
|
||||||
return errors.New("Workspace is required")
|
return errors.New("workspace is required")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -150,7 +150,7 @@ func (s *teamAccesses) Add(ctx context.Context, options TeamAccessAddOptions) (*
|
||||||
// Read a team access by its ID.
|
// Read a team access by its ID.
|
||||||
func (s *teamAccesses) Read(ctx context.Context, teamAccessID string) (*TeamAccess, error) {
|
func (s *teamAccesses) Read(ctx context.Context, teamAccessID string) (*TeamAccess, error) {
|
||||||
if !validStringID(&teamAccessID) {
|
if !validStringID(&teamAccessID) {
|
||||||
return nil, errors.New("Invalid value for team access ID")
|
return nil, errors.New("invalid value for team access ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("team-workspaces/%s", url.QueryEscape(teamAccessID))
|
u := fmt.Sprintf("team-workspaces/%s", url.QueryEscape(teamAccessID))
|
||||||
|
@ -171,7 +171,7 @@ func (s *teamAccesses) Read(ctx context.Context, teamAccessID string) (*TeamAcce
|
||||||
// Remove team access from a workspace.
|
// Remove team access from a workspace.
|
||||||
func (s *teamAccesses) Remove(ctx context.Context, teamAccessID string) error {
|
func (s *teamAccesses) Remove(ctx context.Context, teamAccessID string) error {
|
||||||
if !validStringID(&teamAccessID) {
|
if !validStringID(&teamAccessID) {
|
||||||
return errors.New("Invalid value for team access ID")
|
return errors.New("invalid value for team access ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("team-workspaces/%s", url.QueryEscape(teamAccessID))
|
u := fmt.Sprintf("team-workspaces/%s", url.QueryEscape(teamAccessID))
|
||||||
|
|
|
@ -38,7 +38,7 @@ type teamMember struct {
|
||||||
// List all members of a team.
|
// List all members of a team.
|
||||||
func (s *teamMembers) List(ctx context.Context, teamID string) ([]*User, error) {
|
func (s *teamMembers) List(ctx context.Context, teamID string) ([]*User, error) {
|
||||||
if !validStringID(&teamID) {
|
if !validStringID(&teamID) {
|
||||||
return nil, errors.New("Invalid value for team ID")
|
return nil, errors.New("invalid value for team ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
options := struct {
|
options := struct {
|
||||||
|
@ -69,10 +69,10 @@ type TeamMemberAddOptions struct {
|
||||||
|
|
||||||
func (o *TeamMemberAddOptions) valid() error {
|
func (o *TeamMemberAddOptions) valid() error {
|
||||||
if o.Usernames == nil {
|
if o.Usernames == nil {
|
||||||
return errors.New("Usernames is required")
|
return errors.New("usernames is required")
|
||||||
}
|
}
|
||||||
if len(o.Usernames) == 0 {
|
if len(o.Usernames) == 0 {
|
||||||
return errors.New("Invalid value for usernames")
|
return errors.New("invalid value for usernames")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ func (o *TeamMemberAddOptions) valid() error {
|
||||||
// Add multiple users to a team.
|
// Add multiple users to a team.
|
||||||
func (s *teamMembers) Add(ctx context.Context, teamID string, options TeamMemberAddOptions) error {
|
func (s *teamMembers) Add(ctx context.Context, teamID string, options TeamMemberAddOptions) error {
|
||||||
if !validStringID(&teamID) {
|
if !validStringID(&teamID) {
|
||||||
return errors.New("Invalid value for team ID")
|
return errors.New("invalid value for team ID")
|
||||||
}
|
}
|
||||||
if err := options.valid(); err != nil {
|
if err := options.valid(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -107,10 +107,10 @@ type TeamMemberRemoveOptions struct {
|
||||||
|
|
||||||
func (o *TeamMemberRemoveOptions) valid() error {
|
func (o *TeamMemberRemoveOptions) valid() error {
|
||||||
if o.Usernames == nil {
|
if o.Usernames == nil {
|
||||||
return errors.New("Usernames is required")
|
return errors.New("usernames is required")
|
||||||
}
|
}
|
||||||
if len(o.Usernames) == 0 {
|
if len(o.Usernames) == 0 {
|
||||||
return errors.New("Invalid value for usernames")
|
return errors.New("invalid value for usernames")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -118,7 +118,7 @@ func (o *TeamMemberRemoveOptions) valid() error {
|
||||||
// Remove multiple users from a team.
|
// Remove multiple users from a team.
|
||||||
func (s *teamMembers) Remove(ctx context.Context, teamID string, options TeamMemberRemoveOptions) error {
|
func (s *teamMembers) Remove(ctx context.Context, teamID string, options TeamMemberRemoveOptions) error {
|
||||||
if !validStringID(&teamID) {
|
if !validStringID(&teamID) {
|
||||||
return errors.New("Invalid value for team ID")
|
return errors.New("invalid value for team ID")
|
||||||
}
|
}
|
||||||
if err := options.valid(); err != nil {
|
if err := options.valid(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -44,7 +44,7 @@ type TeamToken struct {
|
||||||
// Generate a new team token, replacing any existing token.
|
// Generate a new team token, replacing any existing token.
|
||||||
func (s *teamTokens) Generate(ctx context.Context, teamID string) (*TeamToken, error) {
|
func (s *teamTokens) Generate(ctx context.Context, teamID string) (*TeamToken, error) {
|
||||||
if !validStringID(&teamID) {
|
if !validStringID(&teamID) {
|
||||||
return nil, errors.New("Invalid value for team ID")
|
return nil, errors.New("invalid value for team ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("teams/%s/authentication-token", url.QueryEscape(teamID))
|
u := fmt.Sprintf("teams/%s/authentication-token", url.QueryEscape(teamID))
|
||||||
|
@ -65,7 +65,7 @@ func (s *teamTokens) Generate(ctx context.Context, teamID string) (*TeamToken, e
|
||||||
// Read a team token by its ID.
|
// Read a team token by its ID.
|
||||||
func (s *teamTokens) Read(ctx context.Context, teamID string) (*TeamToken, error) {
|
func (s *teamTokens) Read(ctx context.Context, teamID string) (*TeamToken, error) {
|
||||||
if !validStringID(&teamID) {
|
if !validStringID(&teamID) {
|
||||||
return nil, errors.New("Invalid value for team ID")
|
return nil, errors.New("invalid value for team ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("teams/%s/authentication-token", url.QueryEscape(teamID))
|
u := fmt.Sprintf("teams/%s/authentication-token", url.QueryEscape(teamID))
|
||||||
|
@ -86,7 +86,7 @@ func (s *teamTokens) Read(ctx context.Context, teamID string) (*TeamToken, error
|
||||||
// Delete a team token by its ID.
|
// Delete a team token by its ID.
|
||||||
func (s *teamTokens) Delete(ctx context.Context, teamID string) error {
|
func (s *teamTokens) Delete(ctx context.Context, teamID string) error {
|
||||||
if !validStringID(&teamID) {
|
if !validStringID(&teamID) {
|
||||||
return errors.New("Invalid value for team ID")
|
return errors.New("invalid value for team ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("teams/%s/authentication-token", url.QueryEscape(teamID))
|
u := fmt.Sprintf("teams/%s/authentication-token", url.QueryEscape(teamID))
|
||||||
|
|
|
@ -7,30 +7,37 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
"github.com/hashicorp/go-cleanhttp"
|
"github.com/hashicorp/go-cleanhttp"
|
||||||
|
retryablehttp "github.com/hashicorp/go-retryablehttp"
|
||||||
"github.com/svanharmelen/jsonapi"
|
"github.com/svanharmelen/jsonapi"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
userAgent = "go-tfe"
|
||||||
|
headerRateLimit = "X-RateLimit-Limit"
|
||||||
|
headerRateReset = "X-RateLimit-Reset"
|
||||||
|
|
||||||
// DefaultAddress of Terraform Enterprise.
|
// DefaultAddress of Terraform Enterprise.
|
||||||
DefaultAddress = "https://app.terraform.io"
|
DefaultAddress = "https://app.terraform.io"
|
||||||
// DefaultBasePath on which the API is served.
|
// DefaultBasePath on which the API is served.
|
||||||
DefaultBasePath = "/api/v2/"
|
DefaultBasePath = "/api/v2/"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
userAgent = "go-tfe"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// random is used to generate pseudo-random numbers.
|
||||||
|
random = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
// ErrUnauthorized is returned when a receiving a 401.
|
// ErrUnauthorized is returned when a receiving a 401.
|
||||||
ErrUnauthorized = errors.New("unauthorized")
|
ErrUnauthorized = errors.New("unauthorized")
|
||||||
// ErrResourceNotFound is returned when a receiving a 404.
|
// ErrResourceNotFound is returned when a receiving a 404.
|
||||||
|
@ -82,7 +89,8 @@ type Client struct {
|
||||||
baseURL *url.URL
|
baseURL *url.URL
|
||||||
token string
|
token string
|
||||||
headers http.Header
|
headers http.Header
|
||||||
http *http.Client
|
http *retryablehttp.Client
|
||||||
|
limiter *rate.Limiter
|
||||||
|
|
||||||
Applies Applies
|
Applies Applies
|
||||||
ConfigurationVersions ConfigurationVersions
|
ConfigurationVersions ConfigurationVersions
|
||||||
|
@ -93,6 +101,7 @@ type Client struct {
|
||||||
Plans Plans
|
Plans Plans
|
||||||
Policies Policies
|
Policies Policies
|
||||||
PolicyChecks PolicyChecks
|
PolicyChecks PolicyChecks
|
||||||
|
PolicySets PolicySets
|
||||||
Runs Runs
|
Runs Runs
|
||||||
SSHKeys SSHKeys
|
SSHKeys SSHKeys
|
||||||
StateVersions StateVersions
|
StateVersions StateVersions
|
||||||
|
@ -131,7 +140,7 @@ func NewClient(cfg *Config) (*Client, error) {
|
||||||
// Parse the address to make sure its a valid URL.
|
// Parse the address to make sure its a valid URL.
|
||||||
baseURL, err := url.Parse(config.Address)
|
baseURL, err := url.Parse(config.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Invalid address: %v", err)
|
return nil, fmt.Errorf("invalid address: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
baseURL.Path = config.BasePath
|
baseURL.Path = config.BasePath
|
||||||
|
@ -141,7 +150,7 @@ func NewClient(cfg *Config) (*Client, error) {
|
||||||
|
|
||||||
// This value must be provided by the user.
|
// This value must be provided by the user.
|
||||||
if config.Token == "" {
|
if config.Token == "" {
|
||||||
return nil, fmt.Errorf("Missing API token")
|
return nil, fmt.Errorf("missing API token")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the client.
|
// Create the client.
|
||||||
|
@ -149,7 +158,20 @@ func NewClient(cfg *Config) (*Client, error) {
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
token: config.Token,
|
token: config.Token,
|
||||||
headers: config.Headers,
|
headers: config.Headers,
|
||||||
http: config.HTTPClient,
|
http: &retryablehttp.Client{
|
||||||
|
Backoff: rateLimitBackoff,
|
||||||
|
CheckRetry: rateLimitRetry,
|
||||||
|
ErrorHandler: retryablehttp.PassthroughErrorHandler,
|
||||||
|
HTTPClient: config.HTTPClient,
|
||||||
|
RetryWaitMin: 100 * time.Millisecond,
|
||||||
|
RetryWaitMax: 300 * time.Millisecond,
|
||||||
|
RetryMax: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the rate limiter.
|
||||||
|
if err := client.configureLimiter(); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the services.
|
// Create the services.
|
||||||
|
@ -162,6 +184,7 @@ func NewClient(cfg *Config) (*Client, error) {
|
||||||
client.Plans = &plans{client: client}
|
client.Plans = &plans{client: client}
|
||||||
client.Policies = &policies{client: client}
|
client.Policies = &policies{client: client}
|
||||||
client.PolicyChecks = &policyChecks{client: client}
|
client.PolicyChecks = &policyChecks{client: client}
|
||||||
|
client.PolicySets = &policySets{client: client}
|
||||||
client.Runs = &runs{client: client}
|
client.Runs = &runs{client: client}
|
||||||
client.SSHKeys = &sshKeys{client: client}
|
client.SSHKeys = &sshKeys{client: client}
|
||||||
client.StateVersions = &stateVersions{client: client}
|
client.StateVersions = &stateVersions{client: client}
|
||||||
|
@ -176,6 +199,96 @@ func NewClient(cfg *Config) (*Client, error) {
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rateLimitRetry provides a callback for Client.CheckRetry, which will only
|
||||||
|
// retry when receiving a 429 response which indicates being rate limited.
|
||||||
|
func rateLimitRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
||||||
|
// Do not retry on context.Canceled or context.DeadlineExceeded.
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return false, ctx.Err()
|
||||||
|
}
|
||||||
|
// Do not retry on any unexpected errors.
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
// Only retry when we are rate limited.
|
||||||
|
if resp.StatusCode == 429 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// rateLimitBackoff provides a callback for Client.Backoff which will use the
|
||||||
|
// X-RateLimit_Reset header to determine the time to wait. We add some jitter
|
||||||
|
// to prevent a thundering herd.
|
||||||
|
//
|
||||||
|
// min and max are mainly used for bounding the jitter that will be added to
|
||||||
|
// the reset time retrieved from the headers. But if the final wait time is
|
||||||
|
// less then min, min will be used instead.
|
||||||
|
func rateLimitBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
|
||||||
|
// First create some jitter bounded by the min and max durations.
|
||||||
|
jitter := time.Duration(rand.Float64() * float64(max-min))
|
||||||
|
|
||||||
|
if resp != nil {
|
||||||
|
if v := resp.Header.Get(headerRateReset); v != "" {
|
||||||
|
if reset, _ := strconv.ParseFloat(v, 64); reset > 0 {
|
||||||
|
// Only update min if the given time to wait is longer.
|
||||||
|
if wait := time.Duration(reset * 1e9); wait > min {
|
||||||
|
min = wait
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return min + jitter
|
||||||
|
}
|
||||||
|
|
||||||
|
// configureLimiter configures the rate limiter.
|
||||||
|
func (c *Client) configureLimiter() error {
|
||||||
|
u, err := c.baseURL.Parse("/")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new request.
|
||||||
|
req, err := http.NewRequest("GET", u.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach the default headers.
|
||||||
|
for k, v := range c.headers {
|
||||||
|
req.Header[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a single request to retrieve the rate limit headers.
|
||||||
|
resp, err := c.http.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
// Set default values for when rate limiting is disabled.
|
||||||
|
limit := rate.Inf
|
||||||
|
burst := 0
|
||||||
|
|
||||||
|
if v := resp.Header.Get(headerRateLimit); v != "" {
|
||||||
|
if rateLimit, _ := strconv.ParseFloat(v, 64); rateLimit > 0 {
|
||||||
|
// Configure the limit and burst using a split of 2/3 for the limit and
|
||||||
|
// 1/3 for the burst. This enables clients to burst 1/3 of the allowed
|
||||||
|
// calls before the limiter kicks in. The remaining calls will then be
|
||||||
|
// spread out evenly using intervals of time.Second / limit which should
|
||||||
|
// prevent hitting the rate limit.
|
||||||
|
limit = rate.Limit(rateLimit * 0.66)
|
||||||
|
burst = int(rateLimit * 0.33)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new limiter using the calculated values.
|
||||||
|
c.limiter = rate.NewLimiter(limit, burst)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListOptions is used to specify pagination options when making API requests.
|
// ListOptions is used to specify pagination options when making API requests.
|
||||||
// Pagination allows breaking up large result sets into chunks, or "pages".
|
// Pagination allows breaking up large result sets into chunks, or "pages".
|
||||||
type ListOptions struct {
|
type ListOptions struct {
|
||||||
|
@ -202,30 +315,20 @@ type Pagination struct {
|
||||||
// If v is supplied, the value will be JSONAPI encoded and included as the
|
// If v is supplied, the value will be JSONAPI encoded and included as the
|
||||||
// request body. If the method is GET, the value will be parsed and added as
|
// request body. If the method is GET, the value will be parsed and added as
|
||||||
// query parameters.
|
// query parameters.
|
||||||
func (c *Client) newRequest(method, path string, v interface{}) (*http.Request, error) {
|
func (c *Client) newRequest(method, path string, v interface{}) (*retryablehttp.Request, error) {
|
||||||
u, err := c.baseURL.Parse(path)
|
u, err := c.baseURL.Parse(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &http.Request{
|
// Create a request specific headers map.
|
||||||
Method: method,
|
reqHeaders := make(http.Header)
|
||||||
URL: u,
|
reqHeaders.Set("Authorization", "Bearer "+c.token)
|
||||||
Proto: "HTTP/1.1",
|
|
||||||
ProtoMajor: 1,
|
|
||||||
ProtoMinor: 1,
|
|
||||||
Header: make(http.Header),
|
|
||||||
Host: u.Host,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set default headers.
|
|
||||||
for k, v := range c.headers {
|
|
||||||
req.Header[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
|
var body interface{}
|
||||||
switch method {
|
switch method {
|
||||||
case "GET":
|
case "GET":
|
||||||
req.Header.Set("Accept", "application/vnd.api+json")
|
reqHeaders.Set("Accept", "application/vnd.api+json")
|
||||||
|
|
||||||
if v != nil {
|
if v != nil {
|
||||||
q, err := query.Values(v)
|
q, err := query.Values(v)
|
||||||
|
@ -235,37 +338,36 @@ func (c *Client) newRequest(method, path string, v interface{}) (*http.Request,
|
||||||
u.RawQuery = q.Encode()
|
u.RawQuery = q.Encode()
|
||||||
}
|
}
|
||||||
case "DELETE", "PATCH", "POST":
|
case "DELETE", "PATCH", "POST":
|
||||||
req.Header.Set("Accept", "application/vnd.api+json")
|
reqHeaders.Set("Accept", "application/vnd.api+json")
|
||||||
req.Header.Set("Content-Type", "application/vnd.api+json")
|
reqHeaders.Set("Content-Type", "application/vnd.api+json")
|
||||||
|
|
||||||
if v != nil {
|
if v != nil {
|
||||||
var body bytes.Buffer
|
buf := bytes.NewBuffer(nil)
|
||||||
if err := jsonapi.MarshalPayloadWithoutIncluded(&body, v); err != nil {
|
if err := jsonapi.MarshalPayloadWithoutIncluded(buf, v); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Body = ioutil.NopCloser(&body)
|
body = buf
|
||||||
req.ContentLength = int64(body.Len())
|
|
||||||
}
|
}
|
||||||
case "PUT":
|
case "PUT":
|
||||||
req.Header.Set("Accept", "application/json")
|
reqHeaders.Set("Accept", "application/json")
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
reqHeaders.Set("Content-Type", "application/octet-stream")
|
||||||
|
body = v
|
||||||
if v != nil {
|
|
||||||
switch v := v.(type) {
|
|
||||||
case *bytes.Buffer:
|
|
||||||
req.Body = ioutil.NopCloser(v)
|
|
||||||
req.ContentLength = int64(v.Len())
|
|
||||||
case []byte:
|
|
||||||
req.Body = ioutil.NopCloser(bytes.NewReader(v))
|
|
||||||
req.ContentLength = int64(len(v))
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("Unexpected type: %T", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the authorization header.
|
req, err := retryablehttp.NewRequest(method, u.String(), body)
|
||||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the default headers.
|
||||||
|
for k, v := range c.headers {
|
||||||
|
req.Header[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the request specific headers.
|
||||||
|
for k, v := range reqHeaders {
|
||||||
|
req.Header[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
@ -279,7 +381,13 @@ func (c *Client) newRequest(method, path string, v interface{}) (*http.Request,
|
||||||
//
|
//
|
||||||
// The provided ctx must be non-nil. If it is canceled or times out, ctx.Err()
|
// The provided ctx must be non-nil. If it is canceled or times out, ctx.Err()
|
||||||
// will be returned.
|
// will be returned.
|
||||||
func (c *Client) do(ctx context.Context, req *http.Request, v interface{}) error {
|
func (c *Client) do(ctx context.Context, req *retryablehttp.Request, v interface{}) error {
|
||||||
|
// Wait will block until the limiter can obtain a new token
|
||||||
|
// or returns an error if the given context is canceled.
|
||||||
|
if err := c.limiter.Wait(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Add the context to the request.
|
// Add the context to the request.
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
|
|
@ -73,10 +73,10 @@ type VariableListOptions struct {
|
||||||
|
|
||||||
func (o VariableListOptions) valid() error {
|
func (o VariableListOptions) valid() error {
|
||||||
if !validString(o.Organization) {
|
if !validString(o.Organization) {
|
||||||
return errors.New("Organization is required")
|
return errors.New("organization is required")
|
||||||
}
|
}
|
||||||
if !validString(o.Workspace) {
|
if !validString(o.Workspace) {
|
||||||
return errors.New("Workspace is required")
|
return errors.New("workspace is required")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -127,16 +127,16 @@ type VariableCreateOptions struct {
|
||||||
|
|
||||||
func (o VariableCreateOptions) valid() error {
|
func (o VariableCreateOptions) valid() error {
|
||||||
if !validString(o.Key) {
|
if !validString(o.Key) {
|
||||||
return errors.New("Key is required")
|
return errors.New("key is required")
|
||||||
}
|
}
|
||||||
if !validString(o.Value) {
|
if !validString(o.Value) {
|
||||||
return errors.New("Value is required")
|
return errors.New("value is required")
|
||||||
}
|
}
|
||||||
if o.Category == nil {
|
if o.Category == nil {
|
||||||
return errors.New("Category is required")
|
return errors.New("category is required")
|
||||||
}
|
}
|
||||||
if o.Workspace == nil {
|
if o.Workspace == nil {
|
||||||
return errors.New("Workspace is required")
|
return errors.New("workspace is required")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -167,7 +167,7 @@ func (s *variables) Create(ctx context.Context, options VariableCreateOptions) (
|
||||||
// Read a variable by its ID.
|
// Read a variable by its ID.
|
||||||
func (s *variables) Read(ctx context.Context, variableID string) (*Variable, error) {
|
func (s *variables) Read(ctx context.Context, variableID string) (*Variable, error) {
|
||||||
if !validStringID(&variableID) {
|
if !validStringID(&variableID) {
|
||||||
return nil, errors.New("Invalid value for variable ID")
|
return nil, errors.New("invalid value for variable ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("vars/%s", url.QueryEscape(variableID))
|
u := fmt.Sprintf("vars/%s", url.QueryEscape(variableID))
|
||||||
|
@ -206,7 +206,7 @@ type VariableUpdateOptions struct {
|
||||||
// Update values of an existing variable.
|
// Update values of an existing variable.
|
||||||
func (s *variables) Update(ctx context.Context, variableID string, options VariableUpdateOptions) (*Variable, error) {
|
func (s *variables) Update(ctx context.Context, variableID string, options VariableUpdateOptions) (*Variable, error) {
|
||||||
if !validStringID(&variableID) {
|
if !validStringID(&variableID) {
|
||||||
return nil, errors.New("Invalid value for variable ID")
|
return nil, errors.New("invalid value for variable ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we don't send a user provided ID.
|
// Make sure we don't send a user provided ID.
|
||||||
|
@ -230,7 +230,7 @@ func (s *variables) Update(ctx context.Context, variableID string, options Varia
|
||||||
// Delete a variable by its ID.
|
// Delete a variable by its ID.
|
||||||
func (s *variables) Delete(ctx context.Context, variableID string) error {
|
func (s *variables) Delete(ctx context.Context, variableID string) error {
|
||||||
if !validStringID(&variableID) {
|
if !validStringID(&variableID) {
|
||||||
return errors.New("Invalid value for variable ID")
|
return errors.New("invalid value for variable ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("vars/%s", url.QueryEscape(variableID))
|
u := fmt.Sprintf("vars/%s", url.QueryEscape(variableID))
|
||||||
|
|
|
@ -112,7 +112,7 @@ type WorkspaceListOptions struct {
|
||||||
// List all the workspaces within an organization.
|
// List all the workspaces within an organization.
|
||||||
func (s *workspaces) List(ctx context.Context, organization string, options WorkspaceListOptions) (*WorkspaceList, error) {
|
func (s *workspaces) List(ctx context.Context, organization string, options WorkspaceListOptions) (*WorkspaceList, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("organizations/%s/workspaces", url.QueryEscape(organization))
|
u := fmt.Sprintf("organizations/%s/workspaces", url.QueryEscape(organization))
|
||||||
|
@ -173,10 +173,10 @@ type VCSRepoOptions struct {
|
||||||
|
|
||||||
func (o WorkspaceCreateOptions) valid() error {
|
func (o WorkspaceCreateOptions) valid() error {
|
||||||
if !validString(o.Name) {
|
if !validString(o.Name) {
|
||||||
return errors.New("Name is required")
|
return errors.New("name is required")
|
||||||
}
|
}
|
||||||
if !validStringID(o.Name) {
|
if !validStringID(o.Name) {
|
||||||
return errors.New("Invalid value for name")
|
return errors.New("invalid value for name")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -184,7 +184,7 @@ func (o WorkspaceCreateOptions) valid() error {
|
||||||
// Create is used to create a new workspace.
|
// Create is used to create a new workspace.
|
||||||
func (s *workspaces) Create(ctx context.Context, organization string, options WorkspaceCreateOptions) (*Workspace, error) {
|
func (s *workspaces) Create(ctx context.Context, organization string, options WorkspaceCreateOptions) (*Workspace, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
if err := options.valid(); err != nil {
|
if err := options.valid(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -211,10 +211,10 @@ func (s *workspaces) Create(ctx context.Context, organization string, options Wo
|
||||||
// Read a workspace by its name.
|
// Read a workspace by its name.
|
||||||
func (s *workspaces) Read(ctx context.Context, organization, workspace string) (*Workspace, error) {
|
func (s *workspaces) Read(ctx context.Context, organization, workspace string) (*Workspace, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
if !validStringID(&workspace) {
|
if !validStringID(&workspace) {
|
||||||
return nil, errors.New("Invalid value for workspace")
|
return nil, errors.New("invalid value for workspace")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf(
|
u := fmt.Sprintf(
|
||||||
|
@ -270,10 +270,10 @@ type WorkspaceUpdateOptions struct {
|
||||||
// Update settings of an existing workspace.
|
// Update settings of an existing workspace.
|
||||||
func (s *workspaces) Update(ctx context.Context, organization, workspace string, options WorkspaceUpdateOptions) (*Workspace, error) {
|
func (s *workspaces) Update(ctx context.Context, organization, workspace string, options WorkspaceUpdateOptions) (*Workspace, error) {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return nil, errors.New("Invalid value for organization")
|
return nil, errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
if !validStringID(&workspace) {
|
if !validStringID(&workspace) {
|
||||||
return nil, errors.New("Invalid value for workspace")
|
return nil, errors.New("invalid value for workspace")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we don't send a user provided ID.
|
// Make sure we don't send a user provided ID.
|
||||||
|
@ -301,10 +301,10 @@ func (s *workspaces) Update(ctx context.Context, organization, workspace string,
|
||||||
// Delete a workspace by its name.
|
// Delete a workspace by its name.
|
||||||
func (s *workspaces) Delete(ctx context.Context, organization, workspace string) error {
|
func (s *workspaces) Delete(ctx context.Context, organization, workspace string) error {
|
||||||
if !validStringID(&organization) {
|
if !validStringID(&organization) {
|
||||||
return errors.New("Invalid value for organization")
|
return errors.New("invalid value for organization")
|
||||||
}
|
}
|
||||||
if !validStringID(&workspace) {
|
if !validStringID(&workspace) {
|
||||||
return errors.New("Invalid value for workspace")
|
return errors.New("invalid value for workspace")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf(
|
u := fmt.Sprintf(
|
||||||
|
@ -329,7 +329,7 @@ type WorkspaceLockOptions struct {
|
||||||
// Lock a workspace by its ID.
|
// Lock a workspace by its ID.
|
||||||
func (s *workspaces) Lock(ctx context.Context, workspaceID string, options WorkspaceLockOptions) (*Workspace, error) {
|
func (s *workspaces) Lock(ctx context.Context, workspaceID string, options WorkspaceLockOptions) (*Workspace, error) {
|
||||||
if !validStringID(&workspaceID) {
|
if !validStringID(&workspaceID) {
|
||||||
return nil, errors.New("Invalid value for workspace ID")
|
return nil, errors.New("invalid value for workspace ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("workspaces/%s/actions/lock", url.QueryEscape(workspaceID))
|
u := fmt.Sprintf("workspaces/%s/actions/lock", url.QueryEscape(workspaceID))
|
||||||
|
@ -350,7 +350,7 @@ func (s *workspaces) Lock(ctx context.Context, workspaceID string, options Works
|
||||||
// Unlock a workspace by its ID.
|
// Unlock a workspace by its ID.
|
||||||
func (s *workspaces) Unlock(ctx context.Context, workspaceID string) (*Workspace, error) {
|
func (s *workspaces) Unlock(ctx context.Context, workspaceID string) (*Workspace, error) {
|
||||||
if !validStringID(&workspaceID) {
|
if !validStringID(&workspaceID) {
|
||||||
return nil, errors.New("Invalid value for workspace ID")
|
return nil, errors.New("invalid value for workspace ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("workspaces/%s/actions/unlock", url.QueryEscape(workspaceID))
|
u := fmt.Sprintf("workspaces/%s/actions/unlock", url.QueryEscape(workspaceID))
|
||||||
|
@ -383,7 +383,7 @@ func (o WorkspaceAssignSSHKeyOptions) valid() error {
|
||||||
return errors.New("SSH key ID is required")
|
return errors.New("SSH key ID is required")
|
||||||
}
|
}
|
||||||
if !validStringID(o.SSHKeyID) {
|
if !validStringID(o.SSHKeyID) {
|
||||||
return errors.New("Invalid value for SSH key ID")
|
return errors.New("invalid value for SSH key ID")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -391,7 +391,7 @@ func (o WorkspaceAssignSSHKeyOptions) valid() error {
|
||||||
// AssignSSHKey to a workspace.
|
// AssignSSHKey to a workspace.
|
||||||
func (s *workspaces) AssignSSHKey(ctx context.Context, workspaceID string, options WorkspaceAssignSSHKeyOptions) (*Workspace, error) {
|
func (s *workspaces) AssignSSHKey(ctx context.Context, workspaceID string, options WorkspaceAssignSSHKeyOptions) (*Workspace, error) {
|
||||||
if !validStringID(&workspaceID) {
|
if !validStringID(&workspaceID) {
|
||||||
return nil, errors.New("Invalid value for workspace ID")
|
return nil, errors.New("invalid value for workspace ID")
|
||||||
}
|
}
|
||||||
if err := options.valid(); err != nil {
|
if err := options.valid(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -428,7 +428,7 @@ type workspaceUnassignSSHKeyOptions struct {
|
||||||
// UnassignSSHKey from a workspace.
|
// UnassignSSHKey from a workspace.
|
||||||
func (s *workspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*Workspace, error) {
|
func (s *workspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*Workspace, error) {
|
||||||
if !validStringID(&workspaceID) {
|
if !validStringID(&workspaceID) {
|
||||||
return nil, errors.New("Invalid value for workspace ID")
|
return nil, errors.New("invalid value for workspace ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
u := fmt.Sprintf("workspaces/%s/relationships/ssh-key", url.QueryEscape(workspaceID))
|
u := fmt.Sprintf("workspaces/%s/relationships/ssh-key", url.QueryEscape(workspaceID))
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
# This source code refers to The Go Authors for copyright purposes.
|
||||||
|
# The master list of authors is in the main Go distribution,
|
||||||
|
# visible at http://tip.golang.org/AUTHORS.
|
|
@ -0,0 +1,3 @@
|
||||||
|
# This source code was written by the Go contributors.
|
||||||
|
# The master list of contributors is in the main Go distribution,
|
||||||
|
# visible at http://tip.golang.org/CONTRIBUTORS.
|
|
@ -0,0 +1,27 @@
|
||||||
|
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,22 @@
|
||||||
|
Additional IP Rights Grant (Patents)
|
||||||
|
|
||||||
|
"This implementation" means the copyrightable works distributed by
|
||||||
|
Google as part of the Go project.
|
||||||
|
|
||||||
|
Google hereby grants to You a perpetual, worldwide, non-exclusive,
|
||||||
|
no-charge, royalty-free, irrevocable (except as stated in this section)
|
||||||
|
patent license to make, have made, use, offer to sell, sell, import,
|
||||||
|
transfer and otherwise run, modify and propagate the contents of this
|
||||||
|
implementation of Go, where such license applies only to those patent
|
||||||
|
claims, both currently owned or controlled by Google and acquired in
|
||||||
|
the future, licensable by Google that are necessarily infringed by this
|
||||||
|
implementation of Go. This grant does not include claims that would be
|
||||||
|
infringed only as a consequence of further modification of this
|
||||||
|
implementation. If you or your agent or exclusive licensee institute or
|
||||||
|
order or agree to the institution of patent litigation against any
|
||||||
|
entity (including a cross-claim or counterclaim in a lawsuit) alleging
|
||||||
|
that this implementation of Go or any code incorporated within this
|
||||||
|
implementation of Go constitutes direct or contributory patent
|
||||||
|
infringement, or inducement of patent infringement, then any patent
|
||||||
|
rights granted to you under this License for this implementation of Go
|
||||||
|
shall terminate as of the date such litigation is filed.
|
|
@ -0,0 +1,374 @@
|
||||||
|
// Copyright 2015 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package rate provides a rate limiter.
|
||||||
|
package rate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Limit defines the maximum frequency of some events.
|
||||||
|
// Limit is represented as number of events per second.
|
||||||
|
// A zero Limit allows no events.
|
||||||
|
type Limit float64
|
||||||
|
|
||||||
|
// Inf is the infinite rate limit; it allows all events (even if burst is zero).
|
||||||
|
const Inf = Limit(math.MaxFloat64)
|
||||||
|
|
||||||
|
// Every converts a minimum time interval between events to a Limit.
|
||||||
|
func Every(interval time.Duration) Limit {
|
||||||
|
if interval <= 0 {
|
||||||
|
return Inf
|
||||||
|
}
|
||||||
|
return 1 / Limit(interval.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Limiter controls how frequently events are allowed to happen.
|
||||||
|
// It implements a "token bucket" of size b, initially full and refilled
|
||||||
|
// at rate r tokens per second.
|
||||||
|
// Informally, in any large enough time interval, the Limiter limits the
|
||||||
|
// rate to r tokens per second, with a maximum burst size of b events.
|
||||||
|
// As a special case, if r == Inf (the infinite rate), b is ignored.
|
||||||
|
// See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets.
|
||||||
|
//
|
||||||
|
// The zero value is a valid Limiter, but it will reject all events.
|
||||||
|
// Use NewLimiter to create non-zero Limiters.
|
||||||
|
//
|
||||||
|
// Limiter has three main methods, Allow, Reserve, and Wait.
|
||||||
|
// Most callers should use Wait.
|
||||||
|
//
|
||||||
|
// Each of the three methods consumes a single token.
|
||||||
|
// They differ in their behavior when no token is available.
|
||||||
|
// If no token is available, Allow returns false.
|
||||||
|
// If no token is available, Reserve returns a reservation for a future token
|
||||||
|
// and the amount of time the caller must wait before using it.
|
||||||
|
// If no token is available, Wait blocks until one can be obtained
|
||||||
|
// or its associated context.Context is canceled.
|
||||||
|
//
|
||||||
|
// The methods AllowN, ReserveN, and WaitN consume n tokens.
|
||||||
|
type Limiter struct {
|
||||||
|
limit Limit
|
||||||
|
burst int
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
tokens float64
|
||||||
|
// last is the last time the limiter's tokens field was updated
|
||||||
|
last time.Time
|
||||||
|
// lastEvent is the latest time of a rate-limited event (past or future)
|
||||||
|
lastEvent time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit returns the maximum overall event rate.
|
||||||
|
func (lim *Limiter) Limit() Limit {
|
||||||
|
lim.mu.Lock()
|
||||||
|
defer lim.mu.Unlock()
|
||||||
|
return lim.limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Burst returns the maximum burst size. Burst is the maximum number of tokens
|
||||||
|
// that can be consumed in a single call to Allow, Reserve, or Wait, so higher
|
||||||
|
// Burst values allow more events to happen at once.
|
||||||
|
// A zero Burst allows no events, unless limit == Inf.
|
||||||
|
func (lim *Limiter) Burst() int {
|
||||||
|
return lim.burst
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLimiter returns a new Limiter that allows events up to rate r and permits
|
||||||
|
// bursts of at most b tokens.
|
||||||
|
func NewLimiter(r Limit, b int) *Limiter {
|
||||||
|
return &Limiter{
|
||||||
|
limit: r,
|
||||||
|
burst: b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow is shorthand for AllowN(time.Now(), 1).
|
||||||
|
func (lim *Limiter) Allow() bool {
|
||||||
|
return lim.AllowN(time.Now(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowN reports whether n events may happen at time now.
|
||||||
|
// Use this method if you intend to drop / skip events that exceed the rate limit.
|
||||||
|
// Otherwise use Reserve or Wait.
|
||||||
|
func (lim *Limiter) AllowN(now time.Time, n int) bool {
|
||||||
|
return lim.reserveN(now, n, 0).ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Reservation holds information about events that are permitted by a Limiter to happen after a delay.
|
||||||
|
// A Reservation may be canceled, which may enable the Limiter to permit additional events.
|
||||||
|
type Reservation struct {
|
||||||
|
ok bool
|
||||||
|
lim *Limiter
|
||||||
|
tokens int
|
||||||
|
timeToAct time.Time
|
||||||
|
// This is the Limit at reservation time, it can change later.
|
||||||
|
limit Limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK returns whether the limiter can provide the requested number of tokens
|
||||||
|
// within the maximum wait time. If OK is false, Delay returns InfDuration, and
|
||||||
|
// Cancel does nothing.
|
||||||
|
func (r *Reservation) OK() bool {
|
||||||
|
return r.ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay is shorthand for DelayFrom(time.Now()).
|
||||||
|
func (r *Reservation) Delay() time.Duration {
|
||||||
|
return r.DelayFrom(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfDuration is the duration returned by Delay when a Reservation is not OK.
|
||||||
|
const InfDuration = time.Duration(1<<63 - 1)
|
||||||
|
|
||||||
|
// DelayFrom returns the duration for which the reservation holder must wait
|
||||||
|
// before taking the reserved action. Zero duration means act immediately.
|
||||||
|
// InfDuration means the limiter cannot grant the tokens requested in this
|
||||||
|
// Reservation within the maximum wait time.
|
||||||
|
func (r *Reservation) DelayFrom(now time.Time) time.Duration {
|
||||||
|
if !r.ok {
|
||||||
|
return InfDuration
|
||||||
|
}
|
||||||
|
delay := r.timeToAct.Sub(now)
|
||||||
|
if delay < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return delay
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel is shorthand for CancelAt(time.Now()).
|
||||||
|
func (r *Reservation) Cancel() {
|
||||||
|
r.CancelAt(time.Now())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelAt indicates that the reservation holder will not perform the reserved action
|
||||||
|
// and reverses the effects of this Reservation on the rate limit as much as possible,
|
||||||
|
// considering that other reservations may have already been made.
|
||||||
|
func (r *Reservation) CancelAt(now time.Time) {
|
||||||
|
if !r.ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.lim.mu.Lock()
|
||||||
|
defer r.lim.mu.Unlock()
|
||||||
|
|
||||||
|
if r.lim.limit == Inf || r.tokens == 0 || r.timeToAct.Before(now) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate tokens to restore
|
||||||
|
// The duration between lim.lastEvent and r.timeToAct tells us how many tokens were reserved
|
||||||
|
// after r was obtained. These tokens should not be restored.
|
||||||
|
restoreTokens := float64(r.tokens) - r.limit.tokensFromDuration(r.lim.lastEvent.Sub(r.timeToAct))
|
||||||
|
if restoreTokens <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// advance time to now
|
||||||
|
now, _, tokens := r.lim.advance(now)
|
||||||
|
// calculate new number of tokens
|
||||||
|
tokens += restoreTokens
|
||||||
|
if burst := float64(r.lim.burst); tokens > burst {
|
||||||
|
tokens = burst
|
||||||
|
}
|
||||||
|
// update state
|
||||||
|
r.lim.last = now
|
||||||
|
r.lim.tokens = tokens
|
||||||
|
if r.timeToAct == r.lim.lastEvent {
|
||||||
|
prevEvent := r.timeToAct.Add(r.limit.durationFromTokens(float64(-r.tokens)))
|
||||||
|
if !prevEvent.Before(now) {
|
||||||
|
r.lim.lastEvent = prevEvent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reserve is shorthand for ReserveN(time.Now(), 1).
|
||||||
|
func (lim *Limiter) Reserve() *Reservation {
|
||||||
|
return lim.ReserveN(time.Now(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReserveN returns a Reservation that indicates how long the caller must wait before n events happen.
|
||||||
|
// The Limiter takes this Reservation into account when allowing future events.
|
||||||
|
// ReserveN returns false if n exceeds the Limiter's burst size.
|
||||||
|
// Usage example:
|
||||||
|
// r := lim.ReserveN(time.Now(), 1)
|
||||||
|
// if !r.OK() {
|
||||||
|
// // Not allowed to act! Did you remember to set lim.burst to be > 0 ?
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// time.Sleep(r.Delay())
|
||||||
|
// Act()
|
||||||
|
// Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events.
|
||||||
|
// If you need to respect a deadline or cancel the delay, use Wait instead.
|
||||||
|
// To drop or skip events exceeding rate limit, use Allow instead.
|
||||||
|
func (lim *Limiter) ReserveN(now time.Time, n int) *Reservation {
|
||||||
|
r := lim.reserveN(now, n, InfDuration)
|
||||||
|
return &r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait is shorthand for WaitN(ctx, 1).
|
||||||
|
func (lim *Limiter) Wait(ctx context.Context) (err error) {
|
||||||
|
return lim.WaitN(ctx, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitN blocks until lim permits n events to happen.
|
||||||
|
// It returns an error if n exceeds the Limiter's burst size, the Context is
|
||||||
|
// canceled, or the expected wait time exceeds the Context's Deadline.
|
||||||
|
// The burst limit is ignored if the rate limit is Inf.
|
||||||
|
func (lim *Limiter) WaitN(ctx context.Context, n int) (err error) {
|
||||||
|
if n > lim.burst && lim.limit != Inf {
|
||||||
|
return fmt.Errorf("rate: Wait(n=%d) exceeds limiter's burst %d", n, lim.burst)
|
||||||
|
}
|
||||||
|
// Check if ctx is already cancelled
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
// Determine wait limit
|
||||||
|
now := time.Now()
|
||||||
|
waitLimit := InfDuration
|
||||||
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
|
waitLimit = deadline.Sub(now)
|
||||||
|
}
|
||||||
|
// Reserve
|
||||||
|
r := lim.reserveN(now, n, waitLimit)
|
||||||
|
if !r.ok {
|
||||||
|
return fmt.Errorf("rate: Wait(n=%d) would exceed context deadline", n)
|
||||||
|
}
|
||||||
|
// Wait if necessary
|
||||||
|
delay := r.DelayFrom(now)
|
||||||
|
if delay == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t := time.NewTimer(delay)
|
||||||
|
defer t.Stop()
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
// We can proceed.
|
||||||
|
return nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
// Context was canceled before we could proceed. Cancel the
|
||||||
|
// reservation, which may permit other events to proceed sooner.
|
||||||
|
r.Cancel()
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLimit is shorthand for SetLimitAt(time.Now(), newLimit).
|
||||||
|
func (lim *Limiter) SetLimit(newLimit Limit) {
|
||||||
|
lim.SetLimitAt(time.Now(), newLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLimitAt sets a new Limit for the limiter. The new Limit, and Burst, may be violated
|
||||||
|
// or underutilized by those which reserved (using Reserve or Wait) but did not yet act
|
||||||
|
// before SetLimitAt was called.
|
||||||
|
func (lim *Limiter) SetLimitAt(now time.Time, newLimit Limit) {
|
||||||
|
lim.mu.Lock()
|
||||||
|
defer lim.mu.Unlock()
|
||||||
|
|
||||||
|
now, _, tokens := lim.advance(now)
|
||||||
|
|
||||||
|
lim.last = now
|
||||||
|
lim.tokens = tokens
|
||||||
|
lim.limit = newLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
// reserveN is a helper method for AllowN, ReserveN, and WaitN.
|
||||||
|
// maxFutureReserve specifies the maximum reservation wait duration allowed.
|
||||||
|
// reserveN returns Reservation, not *Reservation, to avoid allocation in AllowN and WaitN.
|
||||||
|
func (lim *Limiter) reserveN(now time.Time, n int, maxFutureReserve time.Duration) Reservation {
|
||||||
|
lim.mu.Lock()
|
||||||
|
|
||||||
|
if lim.limit == Inf {
|
||||||
|
lim.mu.Unlock()
|
||||||
|
return Reservation{
|
||||||
|
ok: true,
|
||||||
|
lim: lim,
|
||||||
|
tokens: n,
|
||||||
|
timeToAct: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
now, last, tokens := lim.advance(now)
|
||||||
|
|
||||||
|
// Calculate the remaining number of tokens resulting from the request.
|
||||||
|
tokens -= float64(n)
|
||||||
|
|
||||||
|
// Calculate the wait duration
|
||||||
|
var waitDuration time.Duration
|
||||||
|
if tokens < 0 {
|
||||||
|
waitDuration = lim.limit.durationFromTokens(-tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide result
|
||||||
|
ok := n <= lim.burst && waitDuration <= maxFutureReserve
|
||||||
|
|
||||||
|
// Prepare reservation
|
||||||
|
r := Reservation{
|
||||||
|
ok: ok,
|
||||||
|
lim: lim,
|
||||||
|
limit: lim.limit,
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
r.tokens = n
|
||||||
|
r.timeToAct = now.Add(waitDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
if ok {
|
||||||
|
lim.last = now
|
||||||
|
lim.tokens = tokens
|
||||||
|
lim.lastEvent = r.timeToAct
|
||||||
|
} else {
|
||||||
|
lim.last = last
|
||||||
|
}
|
||||||
|
|
||||||
|
lim.mu.Unlock()
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// advance calculates and returns an updated state for lim resulting from the passage of time.
|
||||||
|
// lim is not changed.
|
||||||
|
func (lim *Limiter) advance(now time.Time) (newNow time.Time, newLast time.Time, newTokens float64) {
|
||||||
|
last := lim.last
|
||||||
|
if now.Before(last) {
|
||||||
|
last = now
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid making delta overflow below when last is very old.
|
||||||
|
maxElapsed := lim.limit.durationFromTokens(float64(lim.burst) - lim.tokens)
|
||||||
|
elapsed := now.Sub(last)
|
||||||
|
if elapsed > maxElapsed {
|
||||||
|
elapsed = maxElapsed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the new number of tokens, due to time that passed.
|
||||||
|
delta := lim.limit.tokensFromDuration(elapsed)
|
||||||
|
tokens := lim.tokens + delta
|
||||||
|
if burst := float64(lim.burst); tokens > burst {
|
||||||
|
tokens = burst
|
||||||
|
}
|
||||||
|
|
||||||
|
return now, last, tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
// durationFromTokens is a unit conversion function from the number of tokens to the duration
|
||||||
|
// of time it takes to accumulate them at a rate of limit tokens per second.
|
||||||
|
func (limit Limit) durationFromTokens(tokens float64) time.Duration {
|
||||||
|
seconds := tokens / float64(limit)
|
||||||
|
return time.Nanosecond * time.Duration(1e9*seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokensFromDuration is a unit conversion function from a time duration to the number of tokens
|
||||||
|
// which could be accumulated during that duration at a rate of limit tokens per second.
|
||||||
|
func (limit Limit) tokensFromDuration(d time.Duration) float64 {
|
||||||
|
return d.Seconds() * float64(limit)
|
||||||
|
}
|
|
@ -296,7 +296,7 @@ github.com/hashicorp/consul/testutil/retry
|
||||||
github.com/hashicorp/errwrap
|
github.com/hashicorp/errwrap
|
||||||
# github.com/hashicorp/go-checkpoint v0.0.0-20171009173528-1545e56e46de
|
# github.com/hashicorp/go-checkpoint v0.0.0-20171009173528-1545e56e46de
|
||||||
github.com/hashicorp/go-checkpoint
|
github.com/hashicorp/go-checkpoint
|
||||||
# github.com/hashicorp/go-cleanhttp v0.0.0-20171130225243-06c9ea3a335b
|
# github.com/hashicorp/go-cleanhttp v0.5.0
|
||||||
github.com/hashicorp/go-cleanhttp
|
github.com/hashicorp/go-cleanhttp
|
||||||
# github.com/hashicorp/go-getter v0.0.0-20180327010114-90bb99a48d86
|
# github.com/hashicorp/go-getter v0.0.0-20180327010114-90bb99a48d86
|
||||||
github.com/hashicorp/go-getter
|
github.com/hashicorp/go-getter
|
||||||
|
@ -307,7 +307,7 @@ github.com/hashicorp/go-hclog
|
||||||
github.com/hashicorp/go-multierror
|
github.com/hashicorp/go-multierror
|
||||||
# github.com/hashicorp/go-plugin v0.0.0-20181002195811-1faddcf740b6
|
# github.com/hashicorp/go-plugin v0.0.0-20181002195811-1faddcf740b6
|
||||||
github.com/hashicorp/go-plugin
|
github.com/hashicorp/go-plugin
|
||||||
# github.com/hashicorp/go-retryablehttp v0.0.0-20160930035102-6e85be8fee1d
|
# github.com/hashicorp/go-retryablehttp v0.0.0-20180718195005-e651d75abec6
|
||||||
github.com/hashicorp/go-retryablehttp
|
github.com/hashicorp/go-retryablehttp
|
||||||
# github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90
|
# github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90
|
||||||
github.com/hashicorp/go-rootcerts
|
github.com/hashicorp/go-rootcerts
|
||||||
|
@ -315,7 +315,7 @@ github.com/hashicorp/go-rootcerts
|
||||||
github.com/hashicorp/go-safetemp
|
github.com/hashicorp/go-safetemp
|
||||||
# github.com/hashicorp/go-slug v0.1.0
|
# github.com/hashicorp/go-slug v0.1.0
|
||||||
github.com/hashicorp/go-slug
|
github.com/hashicorp/go-slug
|
||||||
# github.com/hashicorp/go-tfe v0.2.6
|
# github.com/hashicorp/go-tfe v0.2.9
|
||||||
github.com/hashicorp/go-tfe
|
github.com/hashicorp/go-tfe
|
||||||
# github.com/hashicorp/go-uuid v1.0.0
|
# github.com/hashicorp/go-uuid v1.0.0
|
||||||
github.com/hashicorp/go-uuid
|
github.com/hashicorp/go-uuid
|
||||||
|
@ -543,6 +543,8 @@ golang.org/x/text/encoding/unicode
|
||||||
golang.org/x/text/internal/tag
|
golang.org/x/text/internal/tag
|
||||||
golang.org/x/text/internal/utf8internal
|
golang.org/x/text/internal/utf8internal
|
||||||
golang.org/x/text/runes
|
golang.org/x/text/runes
|
||||||
|
# golang.org/x/time v0.0.0-20181108054448-85acf8d2951c
|
||||||
|
golang.org/x/time/rate
|
||||||
# google.golang.org/api v0.0.0-20181015145326-625cd1887957
|
# google.golang.org/api v0.0.0-20181015145326-625cd1887957
|
||||||
google.golang.org/api/iterator
|
google.golang.org/api/iterator
|
||||||
google.golang.org/api/option
|
google.golang.org/api/option
|
||||||
|
|
Loading…
Reference in New Issue