govendor: update `go-tfe`

This commit is contained in:
Sander van Harmelen 2018-09-20 21:54:54 +02:00
parent 5522500777
commit 4b9f17d967
10 changed files with 518 additions and 123 deletions

131
vendor/github.com/hashicorp/go-tfe/apply.go generated vendored Normal file
View File

@ -0,0 +1,131 @@
package tfe
import (
"context"
"errors"
"fmt"
"io"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ Applies = (*applies)(nil)
// Applies describes all the apply related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://www.terraform.io/docs/enterprise/api/apply.html
type Applies interface {
// Read an apply by its ID.
Read(ctx context.Context, applyID string) (*Apply, error)
// Logs retrieves the logs of an apply.
Logs(ctx context.Context, applyID string) (io.Reader, error)
}
// applies implements Applys.
type applies struct {
client *Client
}
// ApplyStatus represents an apply state.
type ApplyStatus string
//List all available apply statuses.
const (
ApplyCanceled ApplyStatus = "canceled"
ApplyCreated ApplyStatus = "created"
ApplyErrored ApplyStatus = "errored"
ApplyFinished ApplyStatus = "finished"
ApplyMFAWaiting ApplyStatus = "mfa_waiting"
ApplyPending ApplyStatus = "pending"
ApplyQueued ApplyStatus = "queued"
ApplyRunning ApplyStatus = "running"
)
// Apply represents a Terraform Enterprise apply.
type Apply struct {
ID string `jsonapi:"primary,applies"`
LogReadURL string `jsonapi:"attr,log-read-url"`
ResourceAdditions int `jsonapi:"attr,resource-additions"`
ResourceChanges int `jsonapi:"attr,resource-changes"`
ResourceDestructions int `jsonapi:"attr,resource-destructions"`
Status ApplyStatus `jsonapi:"attr,status"`
StatusTimestamps *ApplyStatusTimestamps `jsonapi:"attr,status-timestamps"`
}
// ApplyStatusTimestamps holds the timestamps for individual apply statuses.
type ApplyStatusTimestamps struct {
CanceledAt time.Time `json:"canceled-at"`
ErroredAt time.Time `json:"errored-at"`
FinishedAt time.Time `json:"finished-at"`
ForceCanceledAt time.Time `json:"force-canceled-at"`
QueuedAt time.Time `json:"queued-at"`
StartedAt time.Time `json:"started-at"`
}
// 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")
}
u := fmt.Sprintf("applies/%s", url.QueryEscape(applyID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
a := &Apply{}
err = s.client.do(ctx, req, a)
if err != nil {
return nil, err
}
return a, nil
}
// 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")
}
// Get the apply to make sure it exists.
a, err := s.Read(ctx, applyID)
if err != nil {
return nil, err
}
// 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)
}
u, err := url.Parse(a.LogReadURL)
if err != nil {
return nil, fmt.Errorf("Invalid log URL: %v", err)
}
done := func() (bool, error) {
a, err := s.Read(ctx, a.ID)
if err != nil {
return false, err
}
switch a.Status {
case ApplyCanceled, ApplyErrored, ApplyFinished:
return true, nil
default:
return false, nil
}
}
return &LogReader{
client: s.client,
ctx: ctx,
done: done,
logURL: u,
}, nil
}

91
vendor/github.com/hashicorp/go-tfe/logreader.go generated vendored Normal file
View File

@ -0,0 +1,91 @@
package tfe
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
// LogReader implements io.Reader for streaming logs.
type LogReader struct {
client *Client
ctx context.Context
done func() (bool, error)
logURL *url.URL
offset int64
}
func (r *LogReader) Read(l []byte) (int, error) {
if written, err := r.read(l); err != io.ErrNoProgress {
return written, err
}
// Loop until we can any data, the context is canceled or the
// run is finsished. If we would return right away without any
// data, we could and up causing a io.ErrNoProgress error.
for {
select {
case <-r.ctx.Done():
return 0, r.ctx.Err()
case <-time.After(500 * time.Millisecond):
if written, err := r.read(l); err != io.ErrNoProgress {
return written, err
}
}
}
}
func (r *LogReader) read(l []byte) (int, error) {
// Update the query string.
r.logURL.RawQuery = fmt.Sprintf("limit=%d&offset=%d", len(l), r.offset)
// Create a new request.
req, err := http.NewRequest("GET", r.logURL.String(), nil)
if err != nil {
return 0, err
}
req = req.WithContext(r.ctx)
// Retrieve the next chunk.
resp, err := r.client.http.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
// Basic response checking.
if err := checkResponseCode(resp); err != nil {
return 0, err
}
// Read the retrieved chunk.
written, err := resp.Body.Read(l)
if err != nil && err != io.EOF {
// Ignore io.EOF errors returned when reading from the response
// body as this indicates the end of the chunk and not the end
// of the logfile.
return written, err
}
// Check if we need to continue the loop and wait 500 miliseconds
// before checking if there is a new chunk available or that the
// run is finished and we are done reading all chunks.
if written == 0 {
done, err := r.done()
if err != nil {
return 0, err
}
if done {
return 0, io.EOF
}
return 0, io.ErrNoProgress
}
// Update the offset for the next read.
r.offset += int64(written)
return written, nil
}

View File

@ -17,8 +17,17 @@ var _ OAuthClients = (*oAuthClients)(nil)
// TFE API docs:
// https://www.terraform.io/docs/enterprise/api/oauth-clients.html
type OAuthClients interface {
// Create a VCS connection between an organization and a VCS provider.
// List all the OAuth clients for a given organization.
List(ctx context.Context, organization string, options OAuthClientListOptions) (*OAuthClientList, error)
// Create an OAuth client to connect an organization and a VCS provider.
Create(ctx context.Context, organization string, options OAuthClientCreateOptions) (*OAuthClient, error)
// Read an OAuth client by its ID.
Read(ctx context.Context, oAuthClientID string) (*OAuthClient, error)
// Delete an OAuth client by its ID.
Delete(ctx context.Context, oAuthClientID string) error
}
// oAuthClients implements OAuthClients.
@ -40,6 +49,12 @@ const (
ServiceProviderGitlabEE ServiceProviderType = "gitlab_enterprise_edition"
)
// OAuthClientList represents a list of OAuth clients.
type OAuthClientList struct {
*Pagination
Items []*OAuthClient
}
// OAuthClient represents a connection between an organization and a VCS
// provider.
type OAuthClient struct {
@ -56,7 +71,34 @@ type OAuthClient struct {
// Relations
Organization *Organization `jsonapi:"relation,organization"`
OAuthToken []*OAuthToken `jsonapi:"relation,oauth-token"`
OAuthTokens []*OAuthToken `jsonapi:"relation,oauth-tokens"`
}
// OAuthClientListOptions represents the options for listing
// OAuth clients.
type OAuthClientListOptions struct {
ListOptions
}
// 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")
}
u := fmt.Sprintf("organizations/%s/oauth-clients", url.QueryEscape(organization))
req, err := s.client.newRequest("GET", u, &options)
if err != nil {
return nil, err
}
ocl := &OAuthClientList{}
err = s.client.do(ctx, req, ocl)
if err != nil {
return nil, err
}
return ocl, nil
}
// OAuthClientCreateOptions represents the options for creating an OAuth client.
@ -70,11 +112,8 @@ type OAuthClientCreateOptions struct {
// The homepage of your VCS provider.
HTTPURL *string `jsonapi:"attr,http-url"`
// The key you were given by your VCS provider.
Key *string `jsonapi:"attr,key"`
// The secret you were given by your VCS provider.
Secret *string `jsonapi:"attr,secret"`
// The token string you were given by your VCS provider.
OAuthToken *string `jsonapi:"attr,oauth-token-string"`
// The VCS provider being connected with.
ServiceProvider *ServiceProviderType `jsonapi:"attr,service-provider"`
@ -87,11 +126,8 @@ func (o OAuthClientCreateOptions) valid() error {
if !validString(o.HTTPURL) {
return errors.New("HTTPURL is required")
}
if !validString(o.Key) {
return errors.New("Key is required")
}
if !validString(o.Secret) {
return errors.New("Secret is required")
if !validString(o.OAuthToken) {
return errors.New("OAuthToken is required")
}
if o.ServiceProvider == nil {
return errors.New("ServiceProvider is required")
@ -99,7 +135,7 @@ func (o OAuthClientCreateOptions) valid() error {
return nil
}
// Create a VCS connection between an organization and a VCS provider.
// Create an OAuth client to connect an organization and a VCS provider.
func (s *oAuthClients) Create(ctx context.Context, organization string, options OAuthClientCreateOptions) (*OAuthClient, error) {
if !validStringID(&organization) {
return nil, errors.New("Invalid value for organization")
@ -125,3 +161,39 @@ func (s *oAuthClients) Create(ctx context.Context, organization string, options
return oc, nil
}
// 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")
}
u := fmt.Sprintf("oauth-clients/%s", url.QueryEscape(oAuthClientID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
oc := &OAuthClient{}
err = s.client.do(ctx, req, oc)
if err != nil {
return nil, err
}
return oc, err
}
// 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")
}
u := fmt.Sprintf("oauth-clients/%s", url.QueryEscape(oAuthClientID))
req, err := s.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}

View File

@ -17,8 +17,16 @@ var _ OAuthTokens = (*oAuthTokens)(nil)
// TFE API docs:
// https://www.terraform.io/docs/enterprise/api/oauth-tokens.html
type OAuthTokens interface {
// List all the OAuth Tokens for a given organization.
// List all the OAuth tokens for a given organization.
List(ctx context.Context, organization string, options OAuthTokenListOptions) (*OAuthTokenList, error)
// Read a OAuth token by its ID.
Read(ctx context.Context, oAuthTokenID string) (*OAuthToken, error)
// Update an existing OAuth token.
Update(ctx context.Context, oAuthTokenID string, options OAuthTokenUpdateOptions) (*OAuthToken, error)
// Delete a OAuth token by its ID.
Delete(ctx context.Context, oAuthTokenID string) error
}
// oAuthTokens implements OAuthTokens.
@ -71,3 +79,72 @@ func (s *oAuthTokens) List(ctx context.Context, organization string, options OAu
return otl, nil
}
// 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")
}
u := fmt.Sprintf("oauth-tokens/%s", url.QueryEscape(oAuthTokenID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
ot := &OAuthToken{}
err = s.client.do(ctx, req, ot)
if err != nil {
return nil, err
}
return ot, err
}
// OAuthTokenUpdateOptions represents the options for updating an OAuth token.
type OAuthTokenUpdateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,oauth-tokens"`
// A private SSH key to be used for git clone operations.
PrivateSSHKey *string `jsonapi:"attr,ssh-key"`
}
// 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")
}
// Make sure we don't send a user provided ID.
options.ID = ""
u := fmt.Sprintf("oauth-tokens/%s", url.QueryEscape(oAuthTokenID))
req, err := s.client.newRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
ot := &OAuthToken{}
err = s.client.do(ctx, req, ot)
if err != nil {
return nil, err
}
return ot, err
}
// 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")
}
u := fmt.Sprintf("oauth-tokens/%s", url.QueryEscape(oAuthTokenID))
req, err := s.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}

View File

@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
@ -47,23 +46,24 @@ const (
// Plan represents a Terraform Enterprise plan.
type Plan struct {
ID string `jsonapi:"primary,plans"`
HasChanges bool `jsonapi:"attr,has-changes"`
LogReadURL string `jsonapi:"attr,log-read-url"`
Status PlanStatus `jsonapi:"attr,status"`
StatusTimestamps *PlanStatusTimestamps `jsonapi:"attr,status-timestamps"`
ID string `jsonapi:"primary,plans"`
HasChanges bool `jsonapi:"attr,has-changes"`
LogReadURL string `jsonapi:"attr,log-read-url"`
ResourceAdditions int `jsonapi:"attr,resource-additions"`
ResourceChanges int `jsonapi:"attr,resource-changes"`
ResourceDestructions int `jsonapi:"attr,resource-destructions"`
Status PlanStatus `jsonapi:"attr,status"`
StatusTimestamps *PlanStatusTimestamps `jsonapi:"attr,status-timestamps"`
}
// PlanStatusTimestamps holds the timestamps for individual plan statuses.
type PlanStatusTimestamps struct {
CanceledAt time.Time `json:"canceled-at"`
CreatedAt time.Time `json:"created-at"`
ErroredAt time.Time `json:"errored-at"`
FinishedAt time.Time `json:"finished-at"`
MFAWaitingAt time.Time `json:"mfa_waiting-at"`
PendingAt time.Time `json:"pending-at"`
QueuedAt time.Time `json:"queued-at"`
RunningAt time.Time `json:"running-at"`
CanceledAt time.Time `json:"canceled-at"`
ErroredAt time.Time `json:"errored-at"`
FinishedAt time.Time `json:"finished-at"`
ForceCanceledAt time.Time `json:"force-canceled-at"`
QueuedAt time.Time `json:"queued-at"`
StartedAt time.Time `json:"started-at"`
}
// Read a plan by its ID.
@ -109,98 +109,24 @@ func (s *plans) Logs(ctx context.Context, planID string) (io.Reader, error) {
return nil, fmt.Errorf("Invalid log URL: %v", err)
}
done := func() (bool, error) {
p, err := s.Read(ctx, p.ID)
if err != nil {
return false, err
}
switch p.Status {
case PlanCanceled, PlanErrored, PlanFinished:
return true, nil
default:
return false, nil
}
}
return &LogReader{
client: s.client,
ctx: ctx,
done: done,
logURL: u,
plan: p,
}, nil
}
// LogReader implements io.Reader for streaming plan logs.
type LogReader struct {
client *Client
ctx context.Context
logURL *url.URL
offset int64
plan *Plan
reads uint64
}
func (r *LogReader) Read(l []byte) (int, error) {
if written, err := r.read(l); err != io.ErrNoProgress {
return written, err
}
// Loop until we can any data, the context is canceled or the plan
// is finsished running. If we would return right away without any
// data, we could and up causing a io.ErrNoProgress error.
for {
select {
case <-r.ctx.Done():
return 0, r.ctx.Err()
case <-time.After(500 * time.Millisecond):
if written, err := r.read(l); err != io.ErrNoProgress {
return written, err
}
}
}
}
func (r *LogReader) read(l []byte) (int, error) {
// Update the query string.
r.logURL.RawQuery = fmt.Sprintf("limit=%d&offset=%d", len(l), r.offset)
// Create a new request.
req, err := http.NewRequest("GET", r.logURL.String(), nil)
if err != nil {
return 0, err
}
req = req.WithContext(r.ctx)
// Retrieve the next chunk.
resp, err := r.client.http.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
// Basic response checking.
if err := checkResponseCode(resp); err != nil {
return 0, err
}
// Read the retrieved chunk.
written, err := resp.Body.Read(l)
if err != nil && err != io.EOF {
// Ignore io.EOF errors returned when reading from the response
// body as this indicates the end of the chunk and not the end
// of the logfile.
return written, err
}
// Check if we need to continue the loop and wait 500 miliseconds
// before checking if there is a new chunk available or that the
// plan is finished and we are done reading all chunks.
if written == 0 {
if r.reads%2 == 0 {
r.plan, err = r.client.Plans.Read(r.ctx, r.plan.ID)
if err != nil {
return 0, err
}
}
switch r.plan.Status {
case PlanCanceled, PlanErrored, PlanFinished:
return 0, io.EOF
default:
r.reads++
return 0, io.ErrNoProgress
}
}
// Update the offset for the next read.
r.offset += int64(written)
return written, nil
}

View File

@ -1,9 +1,11 @@
package tfe
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/url"
"time"
)
@ -20,8 +22,14 @@ type PolicyChecks interface {
// List all policy checks of the given run.
List(ctx context.Context, runID string, options PolicyCheckListOptions) (*PolicyCheckList, error)
// Read a policy check by its ID.
Read(ctx context.Context, policyCheckID string) (*PolicyCheck, error)
// Override a soft-mandatory or warning policy.
Override(ctx context.Context, policyCheckID string) (*PolicyCheck, error)
// Logs retrieves the logs of a policy check.
Logs(ctx context.Context, policyCheckID string) (io.Reader, error)
}
// policyChecks implements PolicyChecks.
@ -64,7 +72,7 @@ type PolicyCheck struct {
Actions *PolicyActions `jsonapi:"attr,actions"`
Permissions *PolicyPermissions `jsonapi:"attr,permissions"`
Result *PolicyResult `jsonapi:"attr,result"`
Scope PolicyScope `jsonapi:"attr,source"`
Scope PolicyScope `jsonapi:"attr,scope"`
Status PolicyStatus `jsonapi:"attr,status"`
StatusTimestamps *PolicyStatusTimestamps `jsonapi:"attr,status-timestamps"`
}
@ -127,6 +135,27 @@ func (s *policyChecks) List(ctx context.Context, runID string, options PolicyChe
return pcl, nil
}
// 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")
}
u := fmt.Sprintf("policy-checks/%s", url.QueryEscape(policyCheckID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
pc := &PolicyCheck{}
err = s.client.do(ctx, req, pc)
if err != nil {
return nil, err
}
return pc, nil
}
// Override a soft-mandatory or warning policy.
func (s *policyChecks) Override(ctx context.Context, policyCheckID string) (*PolicyCheck, error) {
if !validStringID(&policyCheckID) {
@ -147,3 +176,44 @@ func (s *policyChecks) Override(ctx context.Context, policyCheckID string) (*Pol
return pc, nil
}
// 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")
}
// Loop until the context is canceled or the policy check is finished
// running. The policy check logs are not streamed and so only available
// once the check is finished.
for {
pc, err := s.Read(ctx, policyCheckID)
if err != nil {
return nil, err
}
switch pc.Status {
case PolicyPending, PolicyQueued:
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(500 * time.Millisecond):
continue
}
}
u := fmt.Sprintf("policy-checks/%s/output", url.QueryEscape(policyCheckID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
logs := bytes.NewBuffer(nil)
err = s.client.do(ctx, req, logs)
if err != nil {
return nil, err
}
return logs, nil
}
}

View File

@ -89,15 +89,17 @@ type Run struct {
StatusTimestamps *RunStatusTimestamps `jsonapi:"attr,status-timestamps"`
// Relations
Apply *Apply `jsonapi:"relation,apply"`
ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"`
Plan *Plan `jsonapi:"relation,plan"`
PolicyChecks []*PolicyCheck `jsonapi:"relation,policy-checks"`
Workspace *Workspace `jsonapi:"relation,workspace"`
}
// RunActions represents the run actions.
type RunActions struct {
IsCancelable bool `json:"is-cancelable"`
IsComfirmable bool `json:"is-comfirmable"`
IsConfirmable bool `json:"is-confirmable"`
IsDiscardable bool `json:"is-discardable"`
}

View File

@ -58,7 +58,7 @@ func DefaultConfig() *Config {
Address: os.Getenv("TFE_ADDRESS"),
BasePath: DefaultBasePath,
Token: os.Getenv("TFE_TOKEN"),
HTTPClient: cleanhttp.DefaultClient(),
HTTPClient: cleanhttp.DefaultPooledClient(),
}
// Set the default address if none is given.
@ -77,6 +77,7 @@ type Client struct {
http *http.Client
userAgent string
Applies Applies
ConfigurationVersions ConfigurationVersions
OAuthClients OAuthClients
OAuthTokens OAuthTokens
@ -142,6 +143,7 @@ func NewClient(cfg *Config) (*Client, error) {
}
// Create the services.
client.Applies = &applies{client: client}
client.ConfigurationVersions = &configurationVersions{client: client}
client.OAuthClients = &oAuthClients{client: client}
client.OAuthTokens = &oAuthTokens{client: client}

View File

@ -21,6 +21,9 @@ type Variables interface {
// Create is used to create a new variable.
Create(ctx context.Context, options VariableCreateOptions) (*Variable, error)
// Read a variable by its ID.
Read(ctx context.Context, variableID string) (*Variable, error)
// Update values of an existing variable.
Update(ctx context.Context, variableID string, options VariableUpdateOptions) (*Variable, error)
@ -161,6 +164,27 @@ func (s *variables) Create(ctx context.Context, options VariableCreateOptions) (
return v, nil
}
// 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")
}
u := fmt.Sprintf("vars/%s", url.QueryEscape(variableID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
v := &Variable{}
err = s.client.do(ctx, req, v)
if err != nil {
return nil, err
}
return v, err
}
// VariableUpdateOptions represents the options for updating a variable.
type VariableUpdateOptions struct {
// For internal use only!

6
vendor/vendor.json vendored
View File

@ -1804,10 +1804,10 @@
"revisionTime": "2018-07-12T07:51:27Z"
},
{
"checksumSHA1": "Xo/jovk4kg+1xHsdyfTtBhcLkXo=",
"checksumSHA1": "V2A92CHPEiPIsU4Wepl0ukznka8=",
"path": "github.com/hashicorp/go-tfe",
"revision": "6781009f2a64d61df9aff58f17427f0ef43abad0",
"revisionTime": "2018-09-08T08:19:18Z"
"revision": "cca0c15746d89219f9732f6c07d267827fef25cd",
"revisionTime": "2018-09-20T19:42:22Z"
},
{
"checksumSHA1": "85XUnluYJL7F55ptcwdmN8eSOsk=",