Merge pull request #885 from cloudskiff/fea/tfcloud-creds

Should read terraform config file
main
Elie 2021-08-30 18:10:31 +02:00 committed by GitHub
commit 1b9f8891f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 210 additions and 50 deletions

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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")
} }

View File

@ -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))
}) })
} }
} }