Merge pull request #1254 from p0tr3c-terraform/feat/tfc-human-friendly-workspace-names
feat: human readable tfc workspace namesmain
commit
817b68cbab
1
go.mod
1
go.mod
|
@ -26,6 +26,7 @@ require (
|
|||
github.com/hashicorp/go-getter v1.5.3
|
||||
github.com/hashicorp/go-hclog v0.9.2
|
||||
github.com/hashicorp/go-plugin v1.3.0
|
||||
github.com/hashicorp/go-tfe v0.8.1
|
||||
github.com/hashicorp/go-version v1.3.0
|
||||
github.com/hashicorp/hcl/v2 v2.7.2
|
||||
github.com/hashicorp/terraform v0.14.0
|
||||
|
|
5
go.sum
5
go.sum
|
@ -342,6 +342,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
|
@ -422,10 +423,12 @@ github.com/hashicorp/go-retryablehttp v0.5.2/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es
|
|||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=
|
||||
github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
|
||||
github.com/hashicorp/go-slug v0.4.1 h1:/jAo8dNuLgSImoLXaX7Od7QB4TfYCVPam+OpAt5bZqc=
|
||||
github.com/hashicorp/go-slug v0.4.1/go.mod h1:I5tq5Lv0E2xcNXNkmx7BSfzi1PsJ2cNjs3cC3LwyhK8=
|
||||
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-tfe v0.8.1 h1:J6ulpLaKPHrcnwudRjxvlMYIGzqQFlnPhg3SVFh5N4E=
|
||||
github.com/hashicorp/go-tfe v0.8.1/go.mod h1:XAV72S4O1iP8BDaqiaPLmL2B4EE6almocnOn8E8stHc=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
|
||||
|
@ -742,6 +745,7 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
|
|||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
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=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go v3.0.82+incompatible/go.mod h1:0PfYow01SHPMhKY31xa+EFz2RStxIqj6JFAJS+IkCi4=
|
||||
github.com/tencentyun/cos-go-sdk-v5 v0.0.0-20190808065407-f07404cefc8c/go.mod h1:wk2XFUg6egk4tSDNZtXeKfe2G6690UVyt163PuUxBZk=
|
||||
|
@ -1032,6 +1036,7 @@ golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
|||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
|
|
@ -52,7 +52,7 @@ func GetBackend(config config.SupplierConfig, opts *Options) (Backend, error) {
|
|||
case BackendKeyHTTPS:
|
||||
return NewHTTPReader(&http.Client{}, fmt.Sprintf("%s://%s", config.Backend, config.Path), opts)
|
||||
case BackendKeyTFCloud:
|
||||
return NewTFCloudReader(&http.Client{}, config.Path, opts)
|
||||
return NewTFCloudReader(config.Path, opts), nil
|
||||
default:
|
||||
return nil, errors.Errorf("Unsupported backend '%s'", backend)
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
package backend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
tfe "github.com/hashicorp/go-tfe"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
pkghttp "github.com/snyk/driftctl/pkg/http"
|
||||
)
|
||||
|
||||
const BackendKeyTFCloud = "tfcloud"
|
||||
|
@ -28,72 +28,103 @@ type TFCloudBody struct {
|
|||
}
|
||||
|
||||
type TFCloudBackend struct {
|
||||
request *http.Request
|
||||
client pkghttp.HTTPClient
|
||||
reader io.ReadCloser
|
||||
opts *Options
|
||||
client *tfe.Client
|
||||
reader io.ReadCloser
|
||||
opts *Options
|
||||
workspacePath string
|
||||
}
|
||||
|
||||
func NewTFCloudReader(client pkghttp.HTTPClient, workspaceId string, opts *Options) (*TFCloudBackend, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/workspaces/%s/current-state-version", opts.TFCloudEndpoint, workspaceId), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/vnd.api+json")
|
||||
return &TFCloudBackend{req, client, nil, opts}, nil
|
||||
func NewTFCloudReader(workspacePath string, opts *Options) *TFCloudBackend {
|
||||
return &TFCloudBackend{opts: opts, workspacePath: workspacePath}
|
||||
}
|
||||
|
||||
func (t *TFCloudBackend) authorize() error {
|
||||
func (t *TFCloudBackend) getToken() (string, error) {
|
||||
token := t.opts.TFCloudToken
|
||||
if token == "" {
|
||||
tfConfigFile, err := getTerraformConfigFile()
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
file, err := os.Open(tfConfigFile)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
reader := NewTFCloudConfigReader(file)
|
||||
token, err = reader.GetToken(t.request.URL.Host)
|
||||
|
||||
u, err := url.Parse(t.opts.TFCloudEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
return reader.GetToken(u.Host)
|
||||
}
|
||||
t.request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// A regular expression used to validate string workspace ID patterns.
|
||||
var reStringID = regexp.MustCompile(`^ws-[a-zA-Z0-9\-\._]+$`)
|
||||
|
||||
// isValidWorkspaceID checks if the given input is present and non-empty.
|
||||
func isValidWorkspaceID(v string) bool {
|
||||
return v != "" && reStringID.MatchString(v)
|
||||
}
|
||||
|
||||
func (t *TFCloudBackend) getWorkspaceId() (string, error) {
|
||||
if isValidWorkspaceID(t.workspacePath) {
|
||||
return t.workspacePath, nil
|
||||
}
|
||||
workspacePath := strings.Split(t.workspacePath, "/")
|
||||
if len(workspacePath) != 2 {
|
||||
return "", errors.New("unable to parse terraform cloud workspace, it should be either a workspace id (ws-xxxxx) or a {org}/{workspaceName}")
|
||||
}
|
||||
workspace, err := t.client.Workspaces.Read(context.Background(), workspacePath[0], workspacePath[1])
|
||||
if err != nil {
|
||||
return "", errors.Errorf("unable to read terraform workspace id: %s", err.Error())
|
||||
}
|
||||
return workspace.ID, nil
|
||||
}
|
||||
|
||||
func (t *TFCloudBackend) initTFEClient() error {
|
||||
token, err := t.getToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := &tfe.Config{
|
||||
Token: token,
|
||||
Address: t.opts.TFCloudEndpoint,
|
||||
}
|
||||
tfcClient, err := tfe.NewClient(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.client = tfcClient
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TFCloudBackend) Read(p []byte) (n int, err error) {
|
||||
if t.reader == nil {
|
||||
if err := t.authorize(); err != nil {
|
||||
return 0, err
|
||||
if t.client == nil {
|
||||
if err := t.initTFEClient(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
res, err := t.client.Do(t.request)
|
||||
|
||||
workspaceId, err := t.getWorkspaceId()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if res.StatusCode < 200 || res.StatusCode >= 400 {
|
||||
return 0, errors.Errorf("error requesting terraform cloud backend state: status code: %d", res.StatusCode)
|
||||
}
|
||||
|
||||
body := TFCloudBody{}
|
||||
bodyBytes, _ := ioutil.ReadAll(res.Body)
|
||||
err = json.Unmarshal(bodyBytes, &body)
|
||||
stateVersion, err := t.client.StateVersions.Current(context.Background(), workspaceId)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, errors.Errorf("unable to read current state version: %s", err.Error())
|
||||
}
|
||||
|
||||
rawURL := body.Data.Attributes.HostedStateDownloadUrl
|
||||
logrus.WithFields(logrus.Fields{"hosted-state-download-url": rawURL}).Trace("Terraform Cloud backend response")
|
||||
|
||||
h, err := NewHTTPReader(t.client, rawURL, &Options{})
|
||||
state, err := t.client.StateVersions.Download(context.Background(), stateVersion.DownloadURL)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, errors.Errorf("unable to download current state content: %s", err.Error())
|
||||
}
|
||||
t.reader = h
|
||||
t.reader = io.NopCloser(bytes.NewReader(state))
|
||||
}
|
||||
return t.reader.Read(p)
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
package backend
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/jarcoal/httpmock"
|
||||
"github.com/snyk/driftctl/test/mocks"
|
||||
|
||||
tfe "github.com/hashicorp/go-tfe"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func TestTFCloudBackend_Read(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
type args struct {
|
||||
workspaceId string
|
||||
options *Options
|
||||
|
@ -19,93 +19,130 @@ func TestTFCloudBackend_Read(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
url string
|
||||
wantErr error
|
||||
expected string
|
||||
mock func()
|
||||
mock func(*mocks.Workspaces, *mocks.StateVersions)
|
||||
}{
|
||||
{
|
||||
name: "Should fetch URL with auth header",
|
||||
args: args{
|
||||
workspaceId: "workspaceId",
|
||||
workspaceId: "ws-ABCDEFG12345678",
|
||||
options: &Options{
|
||||
TFCloudToken: "TOKEN",
|
||||
TFCloudEndpoint: "https://app.terraform.io/api/v2",
|
||||
},
|
||||
},
|
||||
url: "https://app.terraform.io/api/v2/workspaces/workspaceId/current-state-version",
|
||||
wantErr: nil,
|
||||
expected: "{}",
|
||||
mock: func() {
|
||||
httpmock.Reset()
|
||||
httpmock.RegisterResponder(
|
||||
"GET",
|
||||
"https://app.terraform.io/api/v2/workspaces/workspaceId/current-state-version",
|
||||
httpmock.NewBytesResponder(http.StatusOK, []byte(`{"data":{"attributes":{"hosted-state-download-url":"https://archivist.terraform.io/v1/object/test"}}}`)),
|
||||
)
|
||||
httpmock.RegisterResponder(
|
||||
"GET",
|
||||
"https://archivist.terraform.io/v1/object/test",
|
||||
httpmock.NewBytesResponder(http.StatusOK, []byte(`{}`)),
|
||||
)
|
||||
mock: func(Workspaces *mocks.Workspaces, StateVersions *mocks.StateVersions) {
|
||||
retDownloadUrl := "https://archivist.terraform.io/v1/object/test"
|
||||
StateVersions.On("Current", mock.Anything, "ws-ABCDEFG12345678").Return(&tfe.StateVersion{DownloadURL: retDownloadUrl}, nil)
|
||||
StateVersions.On("Download", mock.Anything, retDownloadUrl).Return([]byte(`{}`), nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Should resolve path and return state",
|
||||
args: args{
|
||||
workspaceId: "some-org/some-workspace",
|
||||
options: &Options{
|
||||
TFCloudToken: "TOKEN",
|
||||
TFCloudEndpoint: "https://app.terraform.io/api/v2",
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
expected: "{}",
|
||||
mock: func(Workspaces *mocks.Workspaces, StateVersions *mocks.StateVersions) {
|
||||
Workspaces.On("Read", mock.Anything, "some-org", "some-workspace").Return(&tfe.Workspace{ID: "ws-ABCDEFG12345678"}, nil)
|
||||
retDownloadUrl := "https://archivist.terraform.io/v1/object/test"
|
||||
StateVersions.On("Current", mock.Anything, "ws-ABCDEFG12345678").Return(&tfe.StateVersion{DownloadURL: retDownloadUrl}, nil)
|
||||
StateVersions.On("Download", mock.Anything, retDownloadUrl).Return([]byte(`{}`), nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Should fail with wrong workspaceId",
|
||||
args: args{
|
||||
workspaceId: "wrong_workspaceId",
|
||||
workspaceId: "ws-ABCDEFG12345678",
|
||||
options: &Options{
|
||||
TFCloudToken: "TOKEN",
|
||||
TFCloudEndpoint: "https://app.terraform.io/api/v2",
|
||||
},
|
||||
},
|
||||
url: "https://app.terraform.io/api/v2/workspaces/wrong_workspaceId/current-state-version",
|
||||
mock: func() {
|
||||
httpmock.Reset()
|
||||
httpmock.RegisterResponder(
|
||||
"GET",
|
||||
"https://app.terraform.io/api/v2/workspaces/wrong_workspaceId/current-state-version",
|
||||
httpmock.NewBytesResponder(http.StatusNotFound, []byte{}),
|
||||
)
|
||||
mock: func(Workspaces *mocks.Workspaces, StateVersions *mocks.StateVersions) {
|
||||
retDownloadUrl := "https://archivist.terraform.io/v1/object/test"
|
||||
StateVersions.On("Current", mock.Anything, "ws-ABCDEFG12345678").Return(&tfe.StateVersion{DownloadURL: retDownloadUrl}, errors.New("resource not found"))
|
||||
},
|
||||
wantErr: errors.New("error requesting terraform cloud backend state: status code: 404"),
|
||||
wantErr: errors.New("unable to read current state version: resource not found"),
|
||||
},
|
||||
{
|
||||
name: "Should fail with bad authentication token",
|
||||
name: "Should fail with download error",
|
||||
args: args{
|
||||
workspaceId: "workspaceId",
|
||||
workspaceId: "ws-ABCDEFG12345678",
|
||||
options: &Options{
|
||||
TFCloudToken: "TOKEN",
|
||||
TFCloudEndpoint: "https://app.terraform.io/api/v2",
|
||||
},
|
||||
},
|
||||
url: "https://app.terraform.io/api/v2/workspaces/workspaceId/current-state-version",
|
||||
mock: func() {
|
||||
httpmock.Reset()
|
||||
httpmock.RegisterResponder(
|
||||
"GET",
|
||||
"https://app.terraform.io/api/v2/workspaces/workspaceId/current-state-version",
|
||||
httpmock.NewBytesResponder(http.StatusUnauthorized, []byte{}),
|
||||
)
|
||||
mock: func(Workspaces *mocks.Workspaces, StateVersions *mocks.StateVersions) {
|
||||
retDownloadUrl := "https://archivist.terraform.io/v1/object/test"
|
||||
StateVersions.On("Current", mock.Anything, "ws-ABCDEFG12345678").Return(&tfe.StateVersion{DownloadURL: retDownloadUrl}, nil)
|
||||
StateVersions.On("Download", mock.Anything, retDownloadUrl).Return([]byte(`{}`), errors.New("connection terminated"))
|
||||
},
|
||||
wantErr: errors.New("error requesting terraform cloud backend state: status code: 401"),
|
||||
wantErr: errors.New("unable to download current state content: connection terminated"),
|
||||
},
|
||||
{
|
||||
name: "Should fail with bad authentication token - workspace id",
|
||||
args: args{
|
||||
workspaceId: "ws-ABCDEFG12345678",
|
||||
options: &Options{
|
||||
TFCloudToken: "TOKEN",
|
||||
TFCloudEndpoint: "https://app.terraform.io/api/v2",
|
||||
},
|
||||
},
|
||||
mock: func(Workspaces *mocks.Workspaces, StateVersions *mocks.StateVersions) {
|
||||
retDownloadUrl := "https://archivist.terraform.io/v1/object/test"
|
||||
StateVersions.On("Current", mock.Anything, "ws-ABCDEFG12345678").Return(&tfe.StateVersion{DownloadURL: retDownloadUrl}, errors.New("unauthorized"))
|
||||
},
|
||||
wantErr: errors.New("unable to read current state version: unauthorized"),
|
||||
},
|
||||
{
|
||||
name: "Should fail with bad authentication token - full path",
|
||||
args: args{
|
||||
workspaceId: "some-org/some-workspace",
|
||||
options: &Options{
|
||||
TFCloudToken: "TOKEN",
|
||||
TFCloudEndpoint: "https://app.terraform.io/api/v2",
|
||||
},
|
||||
},
|
||||
mock: func(Workspaces *mocks.Workspaces, StateVersions *mocks.StateVersions) {
|
||||
Workspaces.On("Read", mock.Anything, "some-org", "some-workspace").Return(&tfe.Workspace{ID: "ws-ABCDEFG12345678"}, errors.New("unauthorized"))
|
||||
},
|
||||
wantErr: errors.New("unable to read terraform workspace id: unauthorized"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mock()
|
||||
reader := NewTFCloudReader(tt.args.workspaceId, tt.args.options)
|
||||
|
||||
reader, err := NewTFCloudReader(&http.Client{}, tt.args.workspaceId, tt.args.options)
|
||||
assert.NoError(t, err)
|
||||
fakeWorkspaces := &mocks.Workspaces{}
|
||||
fakeStateVersions := &mocks.StateVersions{}
|
||||
tt.mock(fakeWorkspaces, fakeStateVersions)
|
||||
|
||||
reader.client = &tfe.Client{
|
||||
Workspaces: fakeWorkspaces,
|
||||
StateVersions: fakeStateVersions,
|
||||
}
|
||||
|
||||
got := make([]byte, len(tt.expected))
|
||||
_, err = reader.Read(got)
|
||||
_, err := reader.Read(got)
|
||||
if tt.wantErr != nil {
|
||||
assert.EqualError(t, err, tt.wantErr.Error())
|
||||
return
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
fakeWorkspaces.AssertExpectations(t)
|
||||
fakeStateVersions.AssertExpectations(t)
|
||||
assert.NotNil(t, got)
|
||||
assert.Equal(t, tt.expected, string(got))
|
||||
})
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
// Code generated by mockery v2.9.4. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
tfe "github.com/hashicorp/go-tfe"
|
||||
)
|
||||
|
||||
// StateVersions is an autogenerated mock type for the StateVersions type
|
||||
type StateVersions struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: ctx, workspaceID, options
|
||||
func (_m *StateVersions) Create(ctx context.Context, workspaceID string, options tfe.StateVersionCreateOptions) (*tfe.StateVersion, error) {
|
||||
ret := _m.Called(ctx, workspaceID, options)
|
||||
|
||||
var r0 *tfe.StateVersion
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, tfe.StateVersionCreateOptions) *tfe.StateVersion); ok {
|
||||
r0 = rf(ctx, workspaceID, options)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.StateVersion)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, tfe.StateVersionCreateOptions) error); ok {
|
||||
r1 = rf(ctx, workspaceID, options)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Current provides a mock function with given fields: ctx, workspaceID
|
||||
func (_m *StateVersions) Current(ctx context.Context, workspaceID string) (*tfe.StateVersion, error) {
|
||||
ret := _m.Called(ctx, workspaceID)
|
||||
|
||||
var r0 *tfe.StateVersion
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *tfe.StateVersion); ok {
|
||||
r0 = rf(ctx, workspaceID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.StateVersion)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, workspaceID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Download provides a mock function with given fields: ctx, url
|
||||
func (_m *StateVersions) Download(ctx context.Context, url string) ([]byte, error) {
|
||||
ret := _m.Called(ctx, url)
|
||||
|
||||
var r0 []byte
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) []byte); ok {
|
||||
r0 = rf(ctx, url)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, url)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: ctx, options
|
||||
func (_m *StateVersions) List(ctx context.Context, options tfe.StateVersionListOptions) (*tfe.StateVersionList, error) {
|
||||
ret := _m.Called(ctx, options)
|
||||
|
||||
var r0 *tfe.StateVersionList
|
||||
if rf, ok := ret.Get(0).(func(context.Context, tfe.StateVersionListOptions) *tfe.StateVersionList); ok {
|
||||
r0 = rf(ctx, options)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.StateVersionList)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, tfe.StateVersionListOptions) error); ok {
|
||||
r1 = rf(ctx, options)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Read provides a mock function with given fields: ctx, svID
|
||||
func (_m *StateVersions) Read(ctx context.Context, svID string) (*tfe.StateVersion, error) {
|
||||
ret := _m.Called(ctx, svID)
|
||||
|
||||
var r0 *tfe.StateVersion
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *tfe.StateVersion); ok {
|
||||
r0 = rf(ctx, svID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.StateVersion)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, svID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
|
@ -0,0 +1,343 @@
|
|||
// Code generated by mockery v2.9.4. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
tfe "github.com/hashicorp/go-tfe"
|
||||
)
|
||||
|
||||
// Workspaces is an autogenerated mock type for the Workspaces type
|
||||
type Workspaces struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// AssignSSHKey provides a mock function with given fields: ctx, workspaceID, options
|
||||
func (_m *Workspaces) AssignSSHKey(ctx context.Context, workspaceID string, options tfe.WorkspaceAssignSSHKeyOptions) (*tfe.Workspace, error) {
|
||||
ret := _m.Called(ctx, workspaceID, options)
|
||||
|
||||
var r0 *tfe.Workspace
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, tfe.WorkspaceAssignSSHKeyOptions) *tfe.Workspace); ok {
|
||||
r0 = rf(ctx, workspaceID, options)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.Workspace)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, tfe.WorkspaceAssignSSHKeyOptions) error); ok {
|
||||
r1 = rf(ctx, workspaceID, options)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: ctx, organization, options
|
||||
func (_m *Workspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) {
|
||||
ret := _m.Called(ctx, organization, options)
|
||||
|
||||
var r0 *tfe.Workspace
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, tfe.WorkspaceCreateOptions) *tfe.Workspace); ok {
|
||||
r0 = rf(ctx, organization, options)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.Workspace)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, tfe.WorkspaceCreateOptions) error); ok {
|
||||
r1 = rf(ctx, organization, options)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: ctx, organization, workspace
|
||||
func (_m *Workspaces) Delete(ctx context.Context, organization string, workspace string) error {
|
||||
ret := _m.Called(ctx, organization, workspace)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = rf(ctx, organization, workspace)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteByID provides a mock function with given fields: ctx, workspaceID
|
||||
func (_m *Workspaces) DeleteByID(ctx context.Context, workspaceID string) error {
|
||||
ret := _m.Called(ctx, workspaceID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = rf(ctx, workspaceID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ForceUnlock provides a mock function with given fields: ctx, workspaceID
|
||||
func (_m *Workspaces) ForceUnlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
|
||||
ret := _m.Called(ctx, workspaceID)
|
||||
|
||||
var r0 *tfe.Workspace
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *tfe.Workspace); ok {
|
||||
r0 = rf(ctx, workspaceID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.Workspace)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, workspaceID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: ctx, organization, options
|
||||
func (_m *Workspaces) List(ctx context.Context, organization string, options tfe.WorkspaceListOptions) (*tfe.WorkspaceList, error) {
|
||||
ret := _m.Called(ctx, organization, options)
|
||||
|
||||
var r0 *tfe.WorkspaceList
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, tfe.WorkspaceListOptions) *tfe.WorkspaceList); ok {
|
||||
r0 = rf(ctx, organization, options)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.WorkspaceList)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, tfe.WorkspaceListOptions) error); ok {
|
||||
r1 = rf(ctx, organization, options)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Lock provides a mock function with given fields: ctx, workspaceID, options
|
||||
func (_m *Workspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) {
|
||||
ret := _m.Called(ctx, workspaceID, options)
|
||||
|
||||
var r0 *tfe.Workspace
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, tfe.WorkspaceLockOptions) *tfe.Workspace); ok {
|
||||
r0 = rf(ctx, workspaceID, options)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.Workspace)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, tfe.WorkspaceLockOptions) error); ok {
|
||||
r1 = rf(ctx, workspaceID, options)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Read provides a mock function with given fields: ctx, organization, workspace
|
||||
func (_m *Workspaces) Read(ctx context.Context, organization string, workspace string) (*tfe.Workspace, error) {
|
||||
ret := _m.Called(ctx, organization, workspace)
|
||||
|
||||
var r0 *tfe.Workspace
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) *tfe.Workspace); ok {
|
||||
r0 = rf(ctx, organization, workspace)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.Workspace)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = rf(ctx, organization, workspace)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ReadByID provides a mock function with given fields: ctx, workspaceID
|
||||
func (_m *Workspaces) ReadByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
|
||||
ret := _m.Called(ctx, workspaceID)
|
||||
|
||||
var r0 *tfe.Workspace
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *tfe.Workspace); ok {
|
||||
r0 = rf(ctx, workspaceID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.Workspace)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, workspaceID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RemoveVCSConnection provides a mock function with given fields: ctx, organization, workspace
|
||||
func (_m *Workspaces) RemoveVCSConnection(ctx context.Context, organization string, workspace string) (*tfe.Workspace, error) {
|
||||
ret := _m.Called(ctx, organization, workspace)
|
||||
|
||||
var r0 *tfe.Workspace
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) *tfe.Workspace); ok {
|
||||
r0 = rf(ctx, organization, workspace)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.Workspace)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = rf(ctx, organization, workspace)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RemoveVCSConnectionByID provides a mock function with given fields: ctx, workspaceID
|
||||
func (_m *Workspaces) RemoveVCSConnectionByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
|
||||
ret := _m.Called(ctx, workspaceID)
|
||||
|
||||
var r0 *tfe.Workspace
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *tfe.Workspace); ok {
|
||||
r0 = rf(ctx, workspaceID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.Workspace)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, workspaceID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UnassignSSHKey provides a mock function with given fields: ctx, workspaceID
|
||||
func (_m *Workspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
|
||||
ret := _m.Called(ctx, workspaceID)
|
||||
|
||||
var r0 *tfe.Workspace
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *tfe.Workspace); ok {
|
||||
r0 = rf(ctx, workspaceID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.Workspace)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, workspaceID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Unlock provides a mock function with given fields: ctx, workspaceID
|
||||
func (_m *Workspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
|
||||
ret := _m.Called(ctx, workspaceID)
|
||||
|
||||
var r0 *tfe.Workspace
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *tfe.Workspace); ok {
|
||||
r0 = rf(ctx, workspaceID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.Workspace)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, workspaceID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, organization, workspace, options
|
||||
func (_m *Workspaces) Update(ctx context.Context, organization string, workspace string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) {
|
||||
ret := _m.Called(ctx, organization, workspace, options)
|
||||
|
||||
var r0 *tfe.Workspace
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string, tfe.WorkspaceUpdateOptions) *tfe.Workspace); ok {
|
||||
r0 = rf(ctx, organization, workspace, options)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.Workspace)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, string, tfe.WorkspaceUpdateOptions) error); ok {
|
||||
r1 = rf(ctx, organization, workspace, options)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UpdateByID provides a mock function with given fields: ctx, workspaceID, options
|
||||
func (_m *Workspaces) UpdateByID(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) {
|
||||
ret := _m.Called(ctx, workspaceID, options)
|
||||
|
||||
var r0 *tfe.Workspace
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, tfe.WorkspaceUpdateOptions) *tfe.Workspace); ok {
|
||||
r0 = rf(ctx, workspaceID, options)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tfe.Workspace)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, tfe.WorkspaceUpdateOptions) error); ok {
|
||||
r1 = rf(ctx, workspaceID, options)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package test_tfe
|
||||
|
||||
import "github.com/hashicorp/go-tfe"
|
||||
|
||||
type Workspaces interface {
|
||||
tfe.Workspaces
|
||||
}
|
||||
|
||||
type StateVersions interface {
|
||||
tfe.StateVersions
|
||||
}
|
Loading…
Reference in New Issue