Merge pull request #19359 from hashicorp/f-update-deps
depencies: update `go-tfe`
This commit is contained in:
commit
785307f4fb
10
go.mod
10
go.mod
|
@ -45,7 +45,6 @@ require (
|
|||
github.com/golang/protobuf v1.2.0
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
|
||||
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/gophercloud/gophercloud v0.0.0-20170524130959-3027adb1ce72
|
||||
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/errwrap v1.0.0
|
||||
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-hclog v0.0.0-20181001195459-61d530d6c27f
|
||||
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-multierror v1.0.0
|
||||
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-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-tfe v0.2.6
|
||||
github.com/hashicorp/go-tfe v0.2.9
|
||||
github.com/hashicorp/go-uuid v1.0.0
|
||||
github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577
|
||||
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/soheilhy/cmux v0.1.4 // indirect
|
||||
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-openstack v0.0.0-20170616075611-4080a521c6ea
|
||||
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/oauth2 v0.0.0-20181003184128-c57b0facaced
|
||||
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/appengine v1.2.0 // indirect
|
||||
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/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-cleanhttp v0.0.0-20171130225243-06c9ea3a335b h1:xrvnoavY7pMnMB/4x+cSAMgkzwjiSyilS55LZ14Ko7o=
|
||||
github.com/hashicorp/go-cleanhttp v0.0.0-20171130225243-06c9ea3a335b/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
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-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-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-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-retryablehttp v0.0.0-20160930035102-6e85be8fee1d h1:/T1aqTlRV/71ER/wHvhqTZaXGQW7XSO+F16mIIHw7zc=
|
||||
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 h1:qCv4319q2q7XKn0MQbi8p37hsJ+9Xo8e6yojA73JVxk=
|
||||
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/go.mod h1:o4zcYY1e0GEZI6eSEr+43QDYmuGglw1qSO6qdHUHCgg=
|
||||
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-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-tfe v0.2.6 h1:o2ryV7ZS0BgaLfNvzWz+A/6J70UETMy+wFL+DQlUy/M=
|
||||
github.com/hashicorp/go-tfe v0.2.6/go.mod h1:nJs7lSMcNPGQQtjyPG6en099CQ/f83+hfeeSqehl2Fg=
|
||||
github.com/hashicorp/go-tfe v0.2.7 h1:Cy0irO9Qfgdn7FmvxSoXIQrRa3iM/kFmp/c0oCboCow=
|
||||
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/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
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/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
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-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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=
|
||||
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/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
|
||||
|
||||
go:
|
||||
- 1.6.3
|
||||
- 1.8.1
|
||||
|
||||
branches:
|
||||
only:
|
||||
|
|
|
@ -14,13 +14,16 @@ makes `retryablehttp` very easy to drop into existing programs.
|
|||
|
||||
`retryablehttp` performs automatic retries under certain conditions. Mainly, if
|
||||
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.
|
||||
Otherwise, the response is returned and left to the caller to interpret.
|
||||
response code is received (except 501), then a retry is invoked after a wait
|
||||
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
|
||||
(POST/PUT et. al) require an `io.ReadSeeker` to be provided. This enables the
|
||||
request body to be "rewound" if the initial request fails so that the full
|
||||
request can be attempted again.
|
||||
(POST/PUT et. al) can have the body provided in a number of ways (some more or
|
||||
less efficient) that allow "rewinding" the request body if the initial request
|
||||
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
|
||||
===========
|
||||
|
|
|
@ -8,18 +8,28 @@
|
|||
// response is received, then a retry is invoked. 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
|
||||
// (POST/PUT et. al) require an io.ReadSeeker to be provided. This enables the
|
||||
// request body to be "rewound" if the initial request fails so that the full
|
||||
// request can be attempted again.
|
||||
// Requests which take a request body should provide a non-nil function
|
||||
// parameter. The best choice is to provide either a function satisfying
|
||||
// ReaderFunc which provides multiple io.Readers in an efficient manner, a
|
||||
// *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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
@ -44,6 +54,9 @@ var (
|
|||
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
|
||||
// for automatically sending the right Content-Length header when possible.
|
||||
type LenReader interface {
|
||||
|
@ -54,32 +67,118 @@ type LenReader interface {
|
|||
type Request struct {
|
||||
// body is a seekable reader over the request body payload. This is
|
||||
// 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
|
||||
// like an *http.Request so that all meta methods are supported.
|
||||
*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.
|
||||
func NewRequest(method, url string, body io.ReadSeeker) (*Request, error) {
|
||||
// Wrap the body in a noop ReadCloser if non-nil. This prevents the
|
||||
// reader from being closed by the HTTP client.
|
||||
var rcBody io.ReadCloser
|
||||
if body != nil {
|
||||
rcBody = ioutil.NopCloser(body)
|
||||
func NewRequest(method, url string, rawBody interface{}) (*Request, error) {
|
||||
var err error
|
||||
var body ReaderFunc
|
||||
var contentLength int64
|
||||
|
||||
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, rcBody)
|
||||
httpReq, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if we can set the Content-Length automatically.
|
||||
if lr, ok := body.(LenReader); ok {
|
||||
httpReq.ContentLength = int64(lr.Len())
|
||||
}
|
||||
httpReq.ContentLength = contentLength
|
||||
|
||||
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
|
||||
// aborted it is up to the CheckResponse callback to properly close any
|
||||
// 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
|
||||
// like automatic retries to tolerate minor outages.
|
||||
|
@ -128,6 +238,12 @@ type Client struct {
|
|||
// CheckRetry specifies the policy for handling retries, and is called
|
||||
// after each request. The default policy is DefaultRetryPolicy.
|
||||
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.
|
||||
|
@ -139,12 +255,18 @@ func NewClient() *Client {
|
|||
RetryWaitMax: defaultRetryWaitMax,
|
||||
RetryMax: defaultRetryMax,
|
||||
CheckRetry: DefaultRetryPolicy,
|
||||
Backoff: DefaultBackoff,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultRetryPolicy provides a default callback for Client.CheckRetry, which
|
||||
// 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 {
|
||||
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
|
||||
// errors and may relate to outages on the server side. This will catch
|
||||
// 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 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.
|
||||
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++ {
|
||||
var code int // HTTP response code
|
||||
|
||||
// Always rewind the request body when non-nil.
|
||||
if req.body != nil {
|
||||
if _, err := req.body.Seek(0, 0); err != nil {
|
||||
return nil, fmt.Errorf("failed to seek body: %v", err)
|
||||
body, err := req.body()
|
||||
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
|
||||
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.
|
||||
checkOK, checkErr := c.CheckRetry(resp, err)
|
||||
checkOK, checkErr := c.CheckRetry(req.Request.Context(), resp, err)
|
||||
|
||||
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 {
|
||||
// Call this here to maintain the behavior of logging all requests,
|
||||
// even if CheckRetry signals to stop.
|
||||
|
@ -202,25 +397,38 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
|
|||
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.
|
||||
if err == nil {
|
||||
if err == nil && resp != nil {
|
||||
c.drainBody(resp.Body)
|
||||
}
|
||||
|
||||
remain := c.RetryMax - i
|
||||
if remain == 0 {
|
||||
break
|
||||
}
|
||||
wait := backoff(c.RetryWaitMin, c.RetryWaitMax, i)
|
||||
wait := c.Backoff(c.RetryWaitMin, c.RetryWaitMax, i, resp)
|
||||
desc := fmt.Sprintf("%s %s", req.Method, req.URL)
|
||||
if code > 0 {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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",
|
||||
req.Method, req.URL, c.RetryMax+1)
|
||||
}
|
||||
|
@ -230,7 +438,9 @@ func (c *Client) drainBody(body io.ReadCloser) {
|
|||
defer body.Close()
|
||||
_, err := io.Copy(ioutil.Discard, io.LimitReader(body, respReadLimit))
|
||||
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.
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
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) {
|
||||
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] [Organization Tokens](https://www.terraform.io/docs/enterprise/api/organization-tokens.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)
|
||||
- [ ] [Registry Modules](https://www.terraform.io/docs/enterprise/api/modules.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.
|
||||
func (s *applies) Read(ctx context.Context, applyID string) (*Apply, error) {
|
||||
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))
|
||||
|
@ -90,7 +90,7 @@ func (s *applies) Read(ctx context.Context, applyID string) (*Apply, error) {
|
|||
// Logs retrieves the logs of an apply.
|
||||
func (s *applies) Logs(ctx context.Context, applyID string) (io.Reader, error) {
|
||||
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.
|
||||
|
@ -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.
|
||||
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)
|
||||
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) {
|
||||
|
|
|
@ -101,7 +101,7 @@ type ConfigurationVersionListOptions struct {
|
|||
// List returns all configuration versions of a workspace.
|
||||
func (s *configurationVersions) List(ctx context.Context, workspaceID string, options ConfigurationVersionListOptions) (*ConfigurationVersionList, error) {
|
||||
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))
|
||||
|
@ -137,7 +137,7 @@ type ConfigurationVersionCreateOptions struct {
|
|||
// configuration version will be usable once data is uploaded to it.
|
||||
func (s *configurationVersions) Create(ctx context.Context, workspaceID string, options ConfigurationVersionCreateOptions) (*ConfigurationVersion, error) {
|
||||
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.
|
||||
|
@ -161,7 +161,7 @@ func (s *configurationVersions) Create(ctx context.Context, workspaceID string,
|
|||
// Read a configuration version by its ID.
|
||||
func (s *configurationVersions) Read(ctx context.Context, cvID string) (*ConfigurationVersion, error) {
|
||||
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))
|
||||
|
|
|
@ -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)
|
||||
|
||||
// Attach the default headers.
|
||||
for k, v := range r.client.headers {
|
||||
req.Header[k] = v
|
||||
}
|
||||
|
||||
// Retrieve the next chunk.
|
||||
resp, err := r.client.http.Do(req)
|
||||
resp, err := r.client.http.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ type OAuthClientListOptions struct {
|
|||
// List all the OAuth clients for a given organization.
|
||||
func (s *oAuthClients) List(ctx context.Context, organization string, options OAuthClientListOptions) (*OAuthClientList, error) {
|
||||
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))
|
||||
|
@ -121,16 +121,16 @@ type OAuthClientCreateOptions struct {
|
|||
|
||||
func (o OAuthClientCreateOptions) valid() error {
|
||||
if !validString(o.APIURL) {
|
||||
return errors.New("APIURL is required")
|
||||
return errors.New("API URL is required")
|
||||
}
|
||||
if !validString(o.HTTPURL) {
|
||||
return errors.New("HTTPURL is required")
|
||||
return errors.New("HTTP URL is required")
|
||||
}
|
||||
if !validString(o.OAuthToken) {
|
||||
return errors.New("OAuthToken is required")
|
||||
return errors.New("OAuth token is required")
|
||||
}
|
||||
if o.ServiceProvider == nil {
|
||||
return errors.New("ServiceProvider is required")
|
||||
return errors.New("service provider is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -138,7 +138,7 @@ func (o OAuthClientCreateOptions) valid() error {
|
|||
// 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) {
|
||||
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 {
|
||||
return nil, err
|
||||
|
@ -165,7 +165,7 @@ func (s *oAuthClients) Create(ctx context.Context, organization string, options
|
|||
// Read an OAuth client by its ID.
|
||||
func (s *oAuthClients) Read(ctx context.Context, oAuthClientID string) (*OAuthClient, error) {
|
||||
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))
|
||||
|
@ -186,7 +186,7 @@ func (s *oAuthClients) Read(ctx context.Context, oAuthClientID string) (*OAuthCl
|
|||
// Delete an OAuth client by its ID.
|
||||
func (s *oAuthClients) Delete(ctx context.Context, oAuthClientID string) error {
|
||||
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))
|
||||
|
|
|
@ -62,7 +62,7 @@ type OAuthTokenListOptions struct {
|
|||
// List all the OAuth tokens for a given organization.
|
||||
func (s *oAuthTokens) List(ctx context.Context, organization string, options OAuthTokenListOptions) (*OAuthTokenList, error) {
|
||||
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))
|
||||
|
@ -83,7 +83,7 @@ func (s *oAuthTokens) List(ctx context.Context, organization string, options OAu
|
|||
// Read an OAuth token by its ID.
|
||||
func (s *oAuthTokens) Read(ctx context.Context, oAuthTokenID string) (*OAuthToken, error) {
|
||||
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))
|
||||
|
@ -113,7 +113,7 @@ type OAuthTokenUpdateOptions struct {
|
|||
// Update an existing OAuth token.
|
||||
func (s *oAuthTokens) Update(ctx context.Context, oAuthTokenID string, options OAuthTokenUpdateOptions) (*OAuthToken, error) {
|
||||
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.
|
||||
|
@ -137,7 +137,7 @@ func (s *oAuthTokens) Update(ctx context.Context, oAuthTokenID string, options O
|
|||
// Delete an OAuth token by its ID.
|
||||
func (s *oAuthTokens) Delete(ctx context.Context, oAuthTokenID string) error {
|
||||
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))
|
||||
|
|
|
@ -147,13 +147,13 @@ type OrganizationCreateOptions struct {
|
|||
|
||||
func (o OrganizationCreateOptions) valid() error {
|
||||
if !validString(o.Name) {
|
||||
return errors.New("Name is required")
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if !validStringID(o.Name) {
|
||||
return errors.New("Invalid value for name")
|
||||
return errors.New("invalid value for name")
|
||||
}
|
||||
if !validString(o.Email) {
|
||||
return errors.New("Email is required")
|
||||
return errors.New("email is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -184,7 +184,7 @@ func (s *organizations) Create(ctx context.Context, options OrganizationCreateOp
|
|||
// Read an organization by its name.
|
||||
func (s *organizations) Read(ctx context.Context, organization string) (*Organization, error) {
|
||||
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))
|
||||
|
@ -226,7 +226,7 @@ type OrganizationUpdateOptions struct {
|
|||
// Update attributes of an existing organization.
|
||||
func (s *organizations) Update(ctx context.Context, organization string, options OrganizationUpdateOptions) (*Organization, error) {
|
||||
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.
|
||||
|
@ -250,7 +250,7 @@ func (s *organizations) Update(ctx context.Context, organization string, options
|
|||
// Delete an organization by its name.
|
||||
func (s *organizations) Delete(ctx context.Context, organization string) error {
|
||||
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))
|
||||
|
@ -265,7 +265,7 @@ func (s *organizations) Delete(ctx context.Context, organization string) error {
|
|||
// Capacity shows the currently used capacity of an organization.
|
||||
func (s *organizations) Capacity(ctx context.Context, organization string) (*Capacity, error) {
|
||||
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))
|
||||
|
@ -291,7 +291,7 @@ type RunQueueOptions struct {
|
|||
// RunQueue shows the current run queue of an organization.
|
||||
func (s *organizations) RunQueue(ctx context.Context, organization string, options RunQueueOptions) (*RunQueue, error) {
|
||||
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))
|
||||
|
|
|
@ -44,7 +44,7 @@ type OrganizationToken struct {
|
|||
// Generate a new organization token, replacing any existing token.
|
||||
func (s *organizationTokens) Generate(ctx context.Context, organization string) (*OrganizationToken, error) {
|
||||
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))
|
||||
|
@ -65,7 +65,7 @@ func (s *organizationTokens) Generate(ctx context.Context, organization string)
|
|||
// Read an organization token.
|
||||
func (s *organizationTokens) Read(ctx context.Context, organization string) (*OrganizationToken, error) {
|
||||
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))
|
||||
|
@ -86,7 +86,7 @@ func (s *organizationTokens) Read(ctx context.Context, organization string) (*Or
|
|||
// Delete an organization token.
|
||||
func (s *organizationTokens) Delete(ctx context.Context, organization string) error {
|
||||
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))
|
||||
|
|
|
@ -70,7 +70,7 @@ type PlanStatusTimestamps struct {
|
|||
// Read a plan by its ID.
|
||||
func (s *plans) Read(ctx context.Context, planID string) (*Plan, error) {
|
||||
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))
|
||||
|
@ -91,7 +91,7 @@ func (s *plans) Read(ctx context.Context, planID string) (*Plan, error) {
|
|||
// Logs retrieves the logs of a plan.
|
||||
func (s *plans) Logs(ctx context.Context, planID string) (io.Reader, error) {
|
||||
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.
|
||||
|
@ -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.
|
||||
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)
|
||||
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) {
|
||||
|
|
|
@ -62,10 +62,15 @@ type PolicyList struct {
|
|||
|
||||
// Policy represents a Terraform Enterprise policy.
|
||||
type Policy struct {
|
||||
ID string `jsonapi:"primary,policies"`
|
||||
Name string `jsonapi:"attr,name"`
|
||||
Enforce []*Enforcement `jsonapi:"attr,enforce"`
|
||||
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
|
||||
ID string `jsonapi:"primary,policies"`
|
||||
Name string `jsonapi:"attr,name"`
|
||||
Description string `jsonapi:"attr,description"`
|
||||
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.
|
||||
|
@ -77,12 +82,15 @@ type Enforcement struct {
|
|||
// PolicyListOptions represents the options for listing policies.
|
||||
type PolicyListOptions struct {
|
||||
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
|
||||
func (s *policies) List(ctx context.Context, organization string, options PolicyListOptions) (*PolicyList, error) {
|
||||
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))
|
||||
|
@ -108,6 +116,9 @@ type PolicyCreateOptions struct {
|
|||
// The name of the policy.
|
||||
Name *string `jsonapi:"attr,name"`
|
||||
|
||||
// A description of the policy's purpose.
|
||||
Description *string `jsonapi:"attr,description,omitempty"`
|
||||
|
||||
// The enforcements of the policy.
|
||||
Enforce []*EnforcementOptions `jsonapi:"attr,enforce"`
|
||||
}
|
||||
|
@ -120,20 +131,20 @@ type EnforcementOptions struct {
|
|||
|
||||
func (o PolicyCreateOptions) valid() error {
|
||||
if !validString(o.Name) {
|
||||
return errors.New("Name is required")
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if !validStringID(o.Name) {
|
||||
return errors.New("Invalid value for name")
|
||||
return errors.New("invalid value for name")
|
||||
}
|
||||
if o.Enforce == nil {
|
||||
return errors.New("Enforce is required")
|
||||
return errors.New("enforce is required")
|
||||
}
|
||||
for _, e := range o.Enforce {
|
||||
if !validString(e.Path) {
|
||||
return errors.New("Enforcement path is required")
|
||||
return errors.New("enforcement path is required")
|
||||
}
|
||||
if e.Mode == nil {
|
||||
return errors.New("Enforcement mode is required")
|
||||
return errors.New("enforcement mode is required")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -142,7 +153,7 @@ func (o PolicyCreateOptions) valid() error {
|
|||
// Create a policy and associate it with an organization.
|
||||
func (s *policies) Create(ctx context.Context, organization string, options PolicyCreateOptions) (*Policy, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
|
@ -169,7 +180,7 @@ func (s *policies) Create(ctx context.Context, organization string, options Poli
|
|||
// Read a policy by its ID.
|
||||
func (s *policies) Read(ctx context.Context, policyID string) (*Policy, error) {
|
||||
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))
|
||||
|
@ -192,24 +203,17 @@ type PolicyUpdateOptions struct {
|
|||
// For internal use only!
|
||||
ID string `jsonapi:"primary,policies"`
|
||||
|
||||
// The enforcements of the policy.
|
||||
Enforce []*EnforcementOptions `jsonapi:"attr,enforce"`
|
||||
}
|
||||
// A description of the policy's purpose.
|
||||
Description *string `jsonapi:"attr,description,omitempty"`
|
||||
|
||||
func (o PolicyUpdateOptions) valid() error {
|
||||
if o.Enforce == nil {
|
||||
return errors.New("Enforce is required")
|
||||
}
|
||||
return nil
|
||||
// The enforcements of the policy.
|
||||
Enforce []*EnforcementOptions `jsonapi:"attr,enforce,omitempty"`
|
||||
}
|
||||
|
||||
// Update an existing policy.
|
||||
func (s *policies) Update(ctx context.Context, policyID string, options PolicyUpdateOptions) (*Policy, error) {
|
||||
if !validStringID(&policyID) {
|
||||
return nil, errors.New("Invalid value for policy ID")
|
||||
}
|
||||
if err := options.valid(); err != nil {
|
||||
return nil, err
|
||||
return nil, errors.New("invalid value for policy 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.
|
||||
func (s *policies) Delete(ctx context.Context, policyID string) error {
|
||||
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))
|
||||
|
@ -248,7 +252,7 @@ func (s *policies) Delete(ctx context.Context, policyID string) error {
|
|||
// Upload the policy content of the policy.
|
||||
func (s *policies) Upload(ctx context.Context, policyID string, content []byte) error {
|
||||
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))
|
||||
|
@ -263,7 +267,7 @@ func (s *policies) Upload(ctx context.Context, policyID string, content []byte)
|
|||
// Download the policy content of the policy.
|
||||
func (s *policies) Download(ctx context.Context, policyID string) ([]byte, error) {
|
||||
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))
|
||||
|
|
|
@ -118,7 +118,7 @@ type PolicyCheckListOptions struct {
|
|||
// List all policy checks of the given run.
|
||||
func (s *policyChecks) List(ctx context.Context, runID string, options PolicyCheckListOptions) (*PolicyCheckList, error) {
|
||||
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))
|
||||
|
@ -139,7 +139,7 @@ func (s *policyChecks) List(ctx context.Context, runID string, options PolicyChe
|
|||
// Read a policy check by its ID.
|
||||
func (s *policyChecks) Read(ctx context.Context, policyCheckID string) (*PolicyCheck, error) {
|
||||
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))
|
||||
|
@ -160,7 +160,7 @@ func (s *policyChecks) Read(ctx context.Context, policyCheckID string) (*PolicyC
|
|||
// Override a soft-mandatory or warning policy.
|
||||
func (s *policyChecks) Override(ctx context.Context, policyCheckID string) (*PolicyCheck, error) {
|
||||
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))
|
||||
|
@ -181,7 +181,7 @@ func (s *policyChecks) Override(ctx context.Context, policyCheckID string) (*Pol
|
|||
// Logs retrieves the logs of a policy check.
|
||||
func (s *policyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reader, error) {
|
||||
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
|
||||
|
|
|
@ -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.
|
||||
func (s *runs) List(ctx context.Context, workspaceID string, options RunListOptions) (*RunList, error) {
|
||||
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))
|
||||
|
@ -177,7 +177,7 @@ type RunCreateOptions struct {
|
|||
|
||||
func (o RunCreateOptions) valid() error {
|
||||
if o.Workspace == nil {
|
||||
return errors.New("Workspace is required")
|
||||
return errors.New("workspace is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -208,7 +208,7 @@ func (s *runs) Create(ctx context.Context, options RunCreateOptions) (*Run, erro
|
|||
// Read a run by its ID.
|
||||
func (s *runs) Read(ctx context.Context, runID string) (*Run, error) {
|
||||
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))
|
||||
|
@ -235,7 +235,7 @@ type RunApplyOptions struct {
|
|||
// Apply a run by its ID.
|
||||
func (s *runs) Apply(ctx context.Context, runID string, options RunApplyOptions) error {
|
||||
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))
|
||||
|
@ -256,7 +256,7 @@ type RunCancelOptions struct {
|
|||
// Cancel a run by its ID.
|
||||
func (s *runs) Cancel(ctx context.Context, runID string, options RunCancelOptions) error {
|
||||
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))
|
||||
|
@ -277,7 +277,7 @@ type RunForceCancelOptions struct {
|
|||
// ForceCancel is used to forcefully cancel a run by its ID.
|
||||
func (s *runs) ForceCancel(ctx context.Context, runID string, options RunForceCancelOptions) error {
|
||||
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))
|
||||
|
@ -298,7 +298,7 @@ type RunDiscardOptions struct {
|
|||
// Discard a run by its ID.
|
||||
func (s *runs) Discard(ctx context.Context, runID string, options RunDiscardOptions) error {
|
||||
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))
|
||||
|
|
|
@ -57,7 +57,7 @@ type SSHKeyListOptions struct {
|
|||
// List all the SSH keys for a given organization
|
||||
func (s *sshKeys) List(ctx context.Context, organization string, options SSHKeyListOptions) (*SSHKeyList, error) {
|
||||
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))
|
||||
|
@ -89,10 +89,10 @@ type SSHKeyCreateOptions struct {
|
|||
|
||||
func (o SSHKeyCreateOptions) valid() error {
|
||||
if !validString(o.Name) {
|
||||
return errors.New("Name is required")
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if !validString(o.Value) {
|
||||
return errors.New("Value is required")
|
||||
return errors.New("value is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ func (o SSHKeyCreateOptions) valid() error {
|
|||
// Create an SSH key and associate it with an organization.
|
||||
func (s *sshKeys) Create(ctx context.Context, organization string, options SSHKeyCreateOptions) (*SSHKey, error) {
|
||||
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 {
|
||||
|
@ -128,7 +128,7 @@ func (s *sshKeys) Create(ctx context.Context, organization string, options SSHKe
|
|||
// Read an SSH key by its ID.
|
||||
func (s *sshKeys) Read(ctx context.Context, sshKeyID string) (*SSHKey, error) {
|
||||
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))
|
||||
|
@ -161,7 +161,7 @@ type SSHKeyUpdateOptions struct {
|
|||
// Update an SSH key by its ID.
|
||||
func (s *sshKeys) Update(ctx context.Context, sshKeyID string, options SSHKeyUpdateOptions) (*SSHKey, error) {
|
||||
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.
|
||||
|
@ -185,7 +185,7 @@ func (s *sshKeys) Update(ctx context.Context, sshKeyID string, options SSHKeyUpd
|
|||
// Delete an SSH key by its ID.
|
||||
func (s *sshKeys) Delete(ctx context.Context, sshKeyID string) error {
|
||||
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))
|
||||
|
|
|
@ -67,10 +67,10 @@ type StateVersionListOptions struct {
|
|||
|
||||
func (o StateVersionListOptions) valid() error {
|
||||
if !validString(o.Organization) {
|
||||
return errors.New("Organization is required")
|
||||
return errors.New("organization is required")
|
||||
}
|
||||
if !validString(o.Workspace) {
|
||||
return errors.New("Workspace is required")
|
||||
return errors.New("workspace is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -121,10 +121,10 @@ func (o StateVersionCreateOptions) valid() error {
|
|||
return errors.New("MD5 is required")
|
||||
}
|
||||
if o.Serial == nil {
|
||||
return errors.New("Serial is required")
|
||||
return errors.New("serial is required")
|
||||
}
|
||||
if !validString(o.State) {
|
||||
return errors.New("State is required")
|
||||
return errors.New("state is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ func (o StateVersionCreateOptions) valid() error {
|
|||
// Create a new state version for the given workspace.
|
||||
func (s *stateVersions) Create(ctx context.Context, workspaceID string, options StateVersionCreateOptions) (*StateVersion, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
|
@ -159,7 +159,7 @@ func (s *stateVersions) Create(ctx context.Context, workspaceID string, options
|
|||
// Read a state version by its ID.
|
||||
func (s *stateVersions) Read(ctx context.Context, svID string) (*StateVersion, error) {
|
||||
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))
|
||||
|
@ -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.
|
||||
func (s *stateVersions) Current(ctx context.Context, workspaceID string) (*StateVersion, error) {
|
||||
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))
|
||||
|
|
|
@ -64,7 +64,7 @@ type TeamListOptions struct {
|
|||
// List all the teams of the given organization.
|
||||
func (s *teams) List(ctx context.Context, organization string, options TeamListOptions) (*TeamList, error) {
|
||||
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))
|
||||
|
@ -93,10 +93,10 @@ type TeamCreateOptions struct {
|
|||
|
||||
func (o TeamCreateOptions) valid() error {
|
||||
if !validString(o.Name) {
|
||||
return errors.New("Name is required")
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if !validStringID(o.Name) {
|
||||
return errors.New("Invalid value for name")
|
||||
return errors.New("invalid value for name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -104,7 +104,7 @@ func (o TeamCreateOptions) valid() error {
|
|||
// Create a new team with the given options.
|
||||
func (s *teams) Create(ctx context.Context, organization string, options TeamCreateOptions) (*Team, error) {
|
||||
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 {
|
||||
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.
|
||||
func (s *teams) Read(ctx context.Context, teamID string) (*Team, error) {
|
||||
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))
|
||||
|
@ -152,7 +152,7 @@ func (s *teams) Read(ctx context.Context, teamID string) (*Team, error) {
|
|||
// Delete a team by its ID.
|
||||
func (s *teams) Delete(ctx context.Context, teamID string) error {
|
||||
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))
|
||||
|
|
|
@ -68,10 +68,10 @@ type TeamAccessListOptions struct {
|
|||
|
||||
func (o TeamAccessListOptions) valid() error {
|
||||
if !validString(o.WorkspaceID) {
|
||||
return errors.New("Workspace ID is required")
|
||||
return errors.New("workspace ID is required")
|
||||
}
|
||||
if !validStringID(o.WorkspaceID) {
|
||||
return errors.New("Invalid value for workspace ID")
|
||||
return errors.New("invalid value for workspace ID")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -113,13 +113,13 @@ type TeamAccessAddOptions struct {
|
|||
|
||||
func (o TeamAccessAddOptions) valid() error {
|
||||
if o.Access == nil {
|
||||
return errors.New("Access is required")
|
||||
return errors.New("access is required")
|
||||
}
|
||||
if o.Team == nil {
|
||||
return errors.New("Team is required")
|
||||
return errors.New("team is required")
|
||||
}
|
||||
if o.Workspace == nil {
|
||||
return errors.New("Workspace is required")
|
||||
return errors.New("workspace is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -150,7 +150,7 @@ func (s *teamAccesses) Add(ctx context.Context, options TeamAccessAddOptions) (*
|
|||
// Read a team access by its ID.
|
||||
func (s *teamAccesses) Read(ctx context.Context, teamAccessID string) (*TeamAccess, error) {
|
||||
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))
|
||||
|
@ -171,7 +171,7 @@ func (s *teamAccesses) Read(ctx context.Context, teamAccessID string) (*TeamAcce
|
|||
// Remove team access from a workspace.
|
||||
func (s *teamAccesses) Remove(ctx context.Context, teamAccessID string) error {
|
||||
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))
|
||||
|
|
|
@ -38,7 +38,7 @@ type teamMember struct {
|
|||
// List all members of a team.
|
||||
func (s *teamMembers) List(ctx context.Context, teamID string) ([]*User, error) {
|
||||
if !validStringID(&teamID) {
|
||||
return nil, errors.New("Invalid value for team ID")
|
||||
return nil, errors.New("invalid value for team ID")
|
||||
}
|
||||
|
||||
options := struct {
|
||||
|
@ -69,10 +69,10 @@ type TeamMemberAddOptions struct {
|
|||
|
||||
func (o *TeamMemberAddOptions) valid() error {
|
||||
if o.Usernames == nil {
|
||||
return errors.New("Usernames is required")
|
||||
return errors.New("usernames is required")
|
||||
}
|
||||
if len(o.Usernames) == 0 {
|
||||
return errors.New("Invalid value for usernames")
|
||||
return errors.New("invalid value for usernames")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ func (o *TeamMemberAddOptions) valid() error {
|
|||
// Add multiple users to a team.
|
||||
func (s *teamMembers) Add(ctx context.Context, teamID string, options TeamMemberAddOptions) error {
|
||||
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 {
|
||||
return err
|
||||
|
@ -107,10 +107,10 @@ type TeamMemberRemoveOptions struct {
|
|||
|
||||
func (o *TeamMemberRemoveOptions) valid() error {
|
||||
if o.Usernames == nil {
|
||||
return errors.New("Usernames is required")
|
||||
return errors.New("usernames is required")
|
||||
}
|
||||
if len(o.Usernames) == 0 {
|
||||
return errors.New("Invalid value for usernames")
|
||||
return errors.New("invalid value for usernames")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ func (o *TeamMemberRemoveOptions) valid() error {
|
|||
// Remove multiple users from a team.
|
||||
func (s *teamMembers) Remove(ctx context.Context, teamID string, options TeamMemberRemoveOptions) error {
|
||||
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 {
|
||||
return err
|
||||
|
|
|
@ -44,7 +44,7 @@ type TeamToken struct {
|
|||
// Generate a new team token, replacing any existing token.
|
||||
func (s *teamTokens) Generate(ctx context.Context, teamID string) (*TeamToken, error) {
|
||||
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))
|
||||
|
@ -65,7 +65,7 @@ func (s *teamTokens) Generate(ctx context.Context, teamID string) (*TeamToken, e
|
|||
// Read a team token by its ID.
|
||||
func (s *teamTokens) Read(ctx context.Context, teamID string) (*TeamToken, error) {
|
||||
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))
|
||||
|
@ -86,7 +86,7 @@ func (s *teamTokens) Read(ctx context.Context, teamID string) (*TeamToken, error
|
|||
// Delete a team token by its ID.
|
||||
func (s *teamTokens) Delete(ctx context.Context, teamID string) error {
|
||||
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))
|
||||
|
|
|
@ -7,30 +7,37 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
"github.com/hashicorp/go-cleanhttp"
|
||||
retryablehttp "github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/svanharmelen/jsonapi"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
const (
|
||||
userAgent = "go-tfe"
|
||||
headerRateLimit = "X-RateLimit-Limit"
|
||||
headerRateReset = "X-RateLimit-Reset"
|
||||
|
||||
// DefaultAddress of Terraform Enterprise.
|
||||
DefaultAddress = "https://app.terraform.io"
|
||||
// DefaultBasePath on which the API is served.
|
||||
DefaultBasePath = "/api/v2/"
|
||||
)
|
||||
|
||||
const (
|
||||
userAgent = "go-tfe"
|
||||
)
|
||||
|
||||
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 = errors.New("unauthorized")
|
||||
// ErrResourceNotFound is returned when a receiving a 404.
|
||||
|
@ -82,7 +89,8 @@ type Client struct {
|
|||
baseURL *url.URL
|
||||
token string
|
||||
headers http.Header
|
||||
http *http.Client
|
||||
http *retryablehttp.Client
|
||||
limiter *rate.Limiter
|
||||
|
||||
Applies Applies
|
||||
ConfigurationVersions ConfigurationVersions
|
||||
|
@ -93,6 +101,7 @@ type Client struct {
|
|||
Plans Plans
|
||||
Policies Policies
|
||||
PolicyChecks PolicyChecks
|
||||
PolicySets PolicySets
|
||||
Runs Runs
|
||||
SSHKeys SSHKeys
|
||||
StateVersions StateVersions
|
||||
|
@ -131,7 +140,7 @@ func NewClient(cfg *Config) (*Client, error) {
|
|||
// Parse the address to make sure its a valid URL.
|
||||
baseURL, err := url.Parse(config.Address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Invalid address: %v", err)
|
||||
return nil, fmt.Errorf("invalid address: %v", err)
|
||||
}
|
||||
|
||||
baseURL.Path = config.BasePath
|
||||
|
@ -141,7 +150,7 @@ func NewClient(cfg *Config) (*Client, error) {
|
|||
|
||||
// This value must be provided by the user.
|
||||
if config.Token == "" {
|
||||
return nil, fmt.Errorf("Missing API token")
|
||||
return nil, fmt.Errorf("missing API token")
|
||||
}
|
||||
|
||||
// Create the client.
|
||||
|
@ -149,7 +158,20 @@ func NewClient(cfg *Config) (*Client, error) {
|
|||
baseURL: baseURL,
|
||||
token: config.Token,
|
||||
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.
|
||||
|
@ -162,6 +184,7 @@ func NewClient(cfg *Config) (*Client, error) {
|
|||
client.Plans = &plans{client: client}
|
||||
client.Policies = &policies{client: client}
|
||||
client.PolicyChecks = &policyChecks{client: client}
|
||||
client.PolicySets = &policySets{client: client}
|
||||
client.Runs = &runs{client: client}
|
||||
client.SSHKeys = &sshKeys{client: client}
|
||||
client.StateVersions = &stateVersions{client: client}
|
||||
|
@ -176,6 +199,96 @@ func NewClient(cfg *Config) (*Client, error) {
|
|||
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.
|
||||
// Pagination allows breaking up large result sets into chunks, or "pages".
|
||||
type ListOptions struct {
|
||||
|
@ -202,30 +315,20 @@ type Pagination struct {
|
|||
// 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
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
Method: method,
|
||||
URL: u,
|
||||
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
|
||||
}
|
||||
// Create a request specific headers map.
|
||||
reqHeaders := make(http.Header)
|
||||
reqHeaders.Set("Authorization", "Bearer "+c.token)
|
||||
|
||||
var body interface{}
|
||||
switch method {
|
||||
case "GET":
|
||||
req.Header.Set("Accept", "application/vnd.api+json")
|
||||
reqHeaders.Set("Accept", "application/vnd.api+json")
|
||||
|
||||
if v != nil {
|
||||
q, err := query.Values(v)
|
||||
|
@ -235,37 +338,36 @@ func (c *Client) newRequest(method, path string, v interface{}) (*http.Request,
|
|||
u.RawQuery = q.Encode()
|
||||
}
|
||||
case "DELETE", "PATCH", "POST":
|
||||
req.Header.Set("Accept", "application/vnd.api+json")
|
||||
req.Header.Set("Content-Type", "application/vnd.api+json")
|
||||
reqHeaders.Set("Accept", "application/vnd.api+json")
|
||||
reqHeaders.Set("Content-Type", "application/vnd.api+json")
|
||||
|
||||
if v != nil {
|
||||
var body bytes.Buffer
|
||||
if err := jsonapi.MarshalPayloadWithoutIncluded(&body, v); err != nil {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
if err := jsonapi.MarshalPayloadWithoutIncluded(buf, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Body = ioutil.NopCloser(&body)
|
||||
req.ContentLength = int64(body.Len())
|
||||
body = buf
|
||||
}
|
||||
case "PUT":
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
reqHeaders.Set("Accept", "application/json")
|
||||
reqHeaders.Set("Content-Type", "application/octet-stream")
|
||||
body = v
|
||||
}
|
||||
|
||||
// Set the authorization header.
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
req, err := retryablehttp.NewRequest(method, u.String(), body)
|
||||
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
|
||||
}
|
||||
|
@ -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()
|
||||
// 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.
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
|
|
|
@ -73,10 +73,10 @@ type VariableListOptions struct {
|
|||
|
||||
func (o VariableListOptions) valid() error {
|
||||
if !validString(o.Organization) {
|
||||
return errors.New("Organization is required")
|
||||
return errors.New("organization is required")
|
||||
}
|
||||
if !validString(o.Workspace) {
|
||||
return errors.New("Workspace is required")
|
||||
return errors.New("workspace is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -127,16 +127,16 @@ type VariableCreateOptions struct {
|
|||
|
||||
func (o VariableCreateOptions) valid() error {
|
||||
if !validString(o.Key) {
|
||||
return errors.New("Key is required")
|
||||
return errors.New("key is required")
|
||||
}
|
||||
if !validString(o.Value) {
|
||||
return errors.New("Value is required")
|
||||
return errors.New("value is required")
|
||||
}
|
||||
if o.Category == nil {
|
||||
return errors.New("Category is required")
|
||||
return errors.New("category is required")
|
||||
}
|
||||
if o.Workspace == nil {
|
||||
return errors.New("Workspace is required")
|
||||
return errors.New("workspace is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -167,7 +167,7 @@ func (s *variables) Create(ctx context.Context, options VariableCreateOptions) (
|
|||
// Read a variable by its ID.
|
||||
func (s *variables) Read(ctx context.Context, variableID string) (*Variable, error) {
|
||||
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))
|
||||
|
@ -206,7 +206,7 @@ type VariableUpdateOptions struct {
|
|||
// Update values of an existing variable.
|
||||
func (s *variables) Update(ctx context.Context, variableID string, options VariableUpdateOptions) (*Variable, error) {
|
||||
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.
|
||||
|
@ -230,7 +230,7 @@ func (s *variables) Update(ctx context.Context, variableID string, options Varia
|
|||
// Delete a variable by its ID.
|
||||
func (s *variables) Delete(ctx context.Context, variableID string) error {
|
||||
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))
|
||||
|
|
|
@ -112,7 +112,7 @@ type WorkspaceListOptions struct {
|
|||
// List all the workspaces within an organization.
|
||||
func (s *workspaces) List(ctx context.Context, organization string, options WorkspaceListOptions) (*WorkspaceList, error) {
|
||||
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))
|
||||
|
@ -173,10 +173,10 @@ type VCSRepoOptions struct {
|
|||
|
||||
func (o WorkspaceCreateOptions) valid() error {
|
||||
if !validString(o.Name) {
|
||||
return errors.New("Name is required")
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if !validStringID(o.Name) {
|
||||
return errors.New("Invalid value for name")
|
||||
return errors.New("invalid value for name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -184,7 +184,7 @@ func (o WorkspaceCreateOptions) valid() error {
|
|||
// Create is used to create a new workspace.
|
||||
func (s *workspaces) Create(ctx context.Context, organization string, options WorkspaceCreateOptions) (*Workspace, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
|
@ -211,10 +211,10 @@ func (s *workspaces) Create(ctx context.Context, organization string, options Wo
|
|||
// Read a workspace by its name.
|
||||
func (s *workspaces) Read(ctx context.Context, organization, workspace string) (*Workspace, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("Invalid value for organization")
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
if !validStringID(&workspace) {
|
||||
return nil, errors.New("Invalid value for workspace")
|
||||
return nil, errors.New("invalid value for workspace")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf(
|
||||
|
@ -270,10 +270,10 @@ type WorkspaceUpdateOptions struct {
|
|||
// Update settings of an existing workspace.
|
||||
func (s *workspaces) Update(ctx context.Context, organization, workspace string, options WorkspaceUpdateOptions) (*Workspace, error) {
|
||||
if !validStringID(&organization) {
|
||||
return nil, errors.New("Invalid value for organization")
|
||||
return nil, errors.New("invalid value for organization")
|
||||
}
|
||||
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.
|
||||
|
@ -301,10 +301,10 @@ func (s *workspaces) Update(ctx context.Context, organization, workspace string,
|
|||
// Delete a workspace by its name.
|
||||
func (s *workspaces) Delete(ctx context.Context, organization, workspace string) error {
|
||||
if !validStringID(&organization) {
|
||||
return errors.New("Invalid value for organization")
|
||||
return errors.New("invalid value for organization")
|
||||
}
|
||||
if !validStringID(&workspace) {
|
||||
return errors.New("Invalid value for workspace")
|
||||
return errors.New("invalid value for workspace")
|
||||
}
|
||||
|
||||
u := fmt.Sprintf(
|
||||
|
@ -329,7 +329,7 @@ type WorkspaceLockOptions struct {
|
|||
// Lock a workspace by its ID.
|
||||
func (s *workspaces) Lock(ctx context.Context, workspaceID string, options WorkspaceLockOptions) (*Workspace, error) {
|
||||
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))
|
||||
|
@ -350,7 +350,7 @@ func (s *workspaces) Lock(ctx context.Context, workspaceID string, options Works
|
|||
// Unlock a workspace by its ID.
|
||||
func (s *workspaces) Unlock(ctx context.Context, workspaceID string) (*Workspace, error) {
|
||||
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))
|
||||
|
@ -383,7 +383,7 @@ func (o WorkspaceAssignSSHKeyOptions) valid() error {
|
|||
return errors.New("SSH key ID is required")
|
||||
}
|
||||
if !validStringID(o.SSHKeyID) {
|
||||
return errors.New("Invalid value for SSH key ID")
|
||||
return errors.New("invalid value for SSH key ID")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -391,7 +391,7 @@ func (o WorkspaceAssignSSHKeyOptions) valid() error {
|
|||
// AssignSSHKey to a workspace.
|
||||
func (s *workspaces) AssignSSHKey(ctx context.Context, workspaceID string, options WorkspaceAssignSSHKeyOptions) (*Workspace, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
|
@ -428,7 +428,7 @@ type workspaceUnassignSSHKeyOptions struct {
|
|||
// UnassignSSHKey from a workspace.
|
||||
func (s *workspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*Workspace, error) {
|
||||
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))
|
||||
|
|
|
@ -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/go-checkpoint v0.0.0-20171009173528-1545e56e46de
|
||||
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-getter v0.0.0-20180327010114-90bb99a48d86
|
||||
github.com/hashicorp/go-getter
|
||||
|
@ -307,7 +307,7 @@ github.com/hashicorp/go-hclog
|
|||
github.com/hashicorp/go-multierror
|
||||
# github.com/hashicorp/go-plugin v0.0.0-20181002195811-1faddcf740b6
|
||||
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-rootcerts v0.0.0-20160503143440-6bb64b370b90
|
||||
github.com/hashicorp/go-rootcerts
|
||||
|
@ -315,7 +315,7 @@ github.com/hashicorp/go-rootcerts
|
|||
github.com/hashicorp/go-safetemp
|
||||
# github.com/hashicorp/go-slug v0.1.0
|
||||
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-uuid v1.0.0
|
||||
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/utf8internal
|
||||
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/iterator
|
||||
google.golang.org/api/option
|
||||
|
|
Loading…
Reference in New Issue