commit
1b9f8891f6
|
@ -0,0 +1,56 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
|
)
|
||||||
|
|
||||||
|
type container struct {
|
||||||
|
Credentials struct {
|
||||||
|
TerraformCloud struct {
|
||||||
|
Token string
|
||||||
|
} `json:"app.terraform.io"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type tfCloudConfigReader struct {
|
||||||
|
reader io.ReadCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTFCloudConfigReader(reader io.ReadCloser) *tfCloudConfigReader {
|
||||||
|
return &tfCloudConfigReader{reader}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *tfCloudConfigReader) GetToken() (string, error) {
|
||||||
|
b, err := ioutil.ReadAll(r.reader)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("unable to read file")
|
||||||
|
}
|
||||||
|
|
||||||
|
var container container
|
||||||
|
if err := json.Unmarshal(b, &container); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if container.Credentials.TerraformCloud.Token == "" {
|
||||||
|
return "", errors.New("driftctl could not read your Terraform configuration file, please check that this is a valid Terraform credentials file")
|
||||||
|
}
|
||||||
|
return container.Credentials.TerraformCloud.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTerraformConfigFile() (string, error) {
|
||||||
|
homeDir, err := homedir.Dir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
tfConfigDir := ".terraform.d"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
tfConfigDir = "terraform.d"
|
||||||
|
}
|
||||||
|
return filepath.Join(homeDir, tfConfigDir, "credentials.tfrc.json"), nil
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTFCloudConfigReader_GetToken(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
src string
|
||||||
|
want string
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "get terraform cloud creds with config file",
|
||||||
|
src: `{"credentials": {"app.terraform.io": {"token": "token.creds.test"}}}`,
|
||||||
|
want: "token.creds.test",
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test with wrong credentials key in config file",
|
||||||
|
src: `{"test": {"app.terraform.io": {"token": "token.creds.test"}}}`,
|
||||||
|
want: "",
|
||||||
|
wantErr: fmt.Errorf("driftctl could not read your Terraform configuration file, please check that this is a valid Terraform credentials file"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test with wrong terraform cloud hostname key in config file",
|
||||||
|
src: `{"credentials": {"test": {"token": "token.creds.test"}}}`,
|
||||||
|
want: "",
|
||||||
|
wantErr: fmt.Errorf("driftctl could not read your Terraform configuration file, please check that this is a valid Terraform credentials file"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test with wrong terraform cloud token key in config file",
|
||||||
|
src: `{"credentials": {"app.terraform.io": {"test": "token.creds.test"}}}`,
|
||||||
|
want: "",
|
||||||
|
wantErr: fmt.Errorf("driftctl could not read your Terraform configuration file, please check that this is a valid Terraform credentials file"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
readerCloser := ioutil.NopCloser(strings.NewReader(tt.src))
|
||||||
|
defer readerCloser.Close()
|
||||||
|
r := NewTFCloudConfigReader(readerCloser)
|
||||||
|
got, err := r.GetToken()
|
||||||
|
if err != nil && err.Error() != tt.wantErr.Error() {
|
||||||
|
t.Errorf("GetToken() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("GetToken() got = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,8 +3,10 @@ package backend
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
pkghttp "github.com/cloudskiff/driftctl/pkg/http"
|
pkghttp "github.com/cloudskiff/driftctl/pkg/http"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -26,38 +28,80 @@ type TFCloudBody struct {
|
||||||
Data TFCloudData `json:"data"`
|
Data TFCloudData `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTFCloudReader(client pkghttp.HTTPClient, workspaceId string, opts *Options) (*HTTPBackend, error) {
|
type TFCloudBackend struct {
|
||||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/workspaces/%s/current-state-version", TFCloudAPI, workspaceId), nil)
|
request *http.Request
|
||||||
|
client pkghttp.HTTPClient
|
||||||
if err != nil {
|
reader io.ReadCloser
|
||||||
return nil, err
|
opts *Options
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Add("Content-Type", "application/vnd.api+json")
|
func NewTFCloudReader(client pkghttp.HTTPClient, workspaceId string, opts *Options) (*TFCloudBackend, error) {
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", opts.TFCloudToken))
|
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/workspaces/%s/current-state-version", TFCloudAPI, workspaceId), nil)
|
||||||
|
if err != nil {
|
||||||
res, err := client.Do(req)
|
return nil, err
|
||||||
|
}
|
||||||
if err != nil {
|
req.Header.Add("Content-Type", "application/vnd.api+json")
|
||||||
return nil, err
|
return &TFCloudBackend{req, client, nil, opts}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode < 200 || res.StatusCode >= 400 {
|
func (t *TFCloudBackend) authorize() error {
|
||||||
return nil, errors.Errorf("error requesting terraform cloud backend state: status code: %d", res.StatusCode)
|
token := t.opts.TFCloudToken
|
||||||
}
|
if token == "" {
|
||||||
|
tfConfigFile, err := getTerraformConfigFile()
|
||||||
bodyBytes, _ := ioutil.ReadAll(res.Body)
|
if err != nil {
|
||||||
|
return err
|
||||||
body := TFCloudBody{}
|
}
|
||||||
err = json.Unmarshal(bodyBytes, &body)
|
file, err := os.Open(tfConfigFile)
|
||||||
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return nil, err
|
}
|
||||||
}
|
defer file.Close()
|
||||||
|
reader := NewTFCloudConfigReader(file)
|
||||||
rawURL := body.Data.Attributes.HostedStateDownloadUrl
|
token, err = reader.GetToken()
|
||||||
logrus.WithFields(logrus.Fields{"hosted-state-download-url": rawURL}).Trace("Terraform Cloud backend response")
|
if err != nil {
|
||||||
|
return err
|
||||||
opt := Options{}
|
}
|
||||||
return NewHTTPReader(client, rawURL, &opt)
|
}
|
||||||
|
t.request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
res, err := t.client.Do(t.request)
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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{})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
t.reader = h
|
||||||
|
}
|
||||||
|
return t.reader.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TFCloudBackend) Close() error {
|
||||||
|
if t.reader != nil {
|
||||||
|
return t.reader.Close()
|
||||||
|
}
|
||||||
|
return errors.New("Unable to close reader as nothing was opened")
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewTFCloudReader(t *testing.T) {
|
func TestTFCloudBackend_Read(t *testing.T) {
|
||||||
httpmock.Activate()
|
httpmock.Activate()
|
||||||
defer httpmock.DeactivateAndReset()
|
defer httpmock.DeactivateAndReset()
|
||||||
type args struct {
|
type args struct {
|
||||||
|
@ -17,12 +17,12 @@ func TestNewTFCloudReader(t *testing.T) {
|
||||||
options *Options
|
options *Options
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
args args
|
args args
|
||||||
url string
|
url string
|
||||||
wantURL string
|
wantErr error
|
||||||
wantErr error
|
expected string
|
||||||
mock func()
|
mock func()
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Should fetch URL with auth header",
|
name: "Should fetch URL with auth header",
|
||||||
|
@ -32,9 +32,9 @@ func TestNewTFCloudReader(t *testing.T) {
|
||||||
TFCloudToken: "TOKEN",
|
TFCloudToken: "TOKEN",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
url: "https://app.terraform.io/api/v2/workspaces/workspaceId/current-state-version",
|
url: "https://app.terraform.io/api/v2/workspaces/workspaceId/current-state-version",
|
||||||
wantURL: "https://archivist.terraform.io/v1/object/test",
|
wantErr: nil,
|
||||||
wantErr: nil,
|
expected: "{}",
|
||||||
mock: func() {
|
mock: func() {
|
||||||
httpmock.Reset()
|
httpmock.Reset()
|
||||||
httpmock.RegisterResponder(
|
httpmock.RegisterResponder(
|
||||||
|
@ -57,8 +57,7 @@ func TestNewTFCloudReader(t *testing.T) {
|
||||||
TFCloudToken: "TOKEN",
|
TFCloudToken: "TOKEN",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
url: "https://app.terraform.io/api/v2/workspaces/wrong_workspaceId/current-state-version",
|
url: "https://app.terraform.io/api/v2/workspaces/wrong_workspaceId/current-state-version",
|
||||||
wantURL: "",
|
|
||||||
mock: func() {
|
mock: func() {
|
||||||
httpmock.Reset()
|
httpmock.Reset()
|
||||||
httpmock.RegisterResponder(
|
httpmock.RegisterResponder(
|
||||||
|
@ -77,8 +76,7 @@ func TestNewTFCloudReader(t *testing.T) {
|
||||||
TFCloudToken: "TOKEN",
|
TFCloudToken: "TOKEN",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
url: "https://app.terraform.io/api/v2/workspaces/workspaceId/current-state-version",
|
url: "https://app.terraform.io/api/v2/workspaces/workspaceId/current-state-version",
|
||||||
wantURL: "",
|
|
||||||
mock: func() {
|
mock: func() {
|
||||||
httpmock.Reset()
|
httpmock.Reset()
|
||||||
httpmock.RegisterResponder(
|
httpmock.RegisterResponder(
|
||||||
|
@ -93,7 +91,12 @@ func TestNewTFCloudReader(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
tt.mock()
|
tt.mock()
|
||||||
got, err := NewTFCloudReader(&http.Client{}, tt.args.workspaceId, tt.args.options)
|
|
||||||
|
reader, err := NewTFCloudReader(&http.Client{}, tt.args.workspaceId, tt.args.options)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
got := make([]byte, len(tt.expected))
|
||||||
|
_, err = reader.Read(got)
|
||||||
if tt.wantErr != nil {
|
if tt.wantErr != nil {
|
||||||
assert.EqualError(t, err, tt.wantErr.Error())
|
assert.EqualError(t, err, tt.wantErr.Error())
|
||||||
return
|
return
|
||||||
|
@ -101,7 +104,7 @@ func TestNewTFCloudReader(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
assert.NotNil(t, got)
|
assert.NotNil(t, got)
|
||||||
assert.Equal(t, tt.wantURL, got.request.URL.String())
|
assert.Equal(t, tt.expected, string(got))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue