feat: add azure blob backend

main
Elie 2022-02-22 14:41:10 +01:00
parent 5dd9162923
commit 7778462ade
No known key found for this signature in database
GPG Key ID: 399AF69092C727B6
7 changed files with 178 additions and 8 deletions

View File

@ -82,7 +82,7 @@ func TestDriftctlCmd_Scan(t *testing.T) {
env: map[string]string{
"DCTL_FROM": "test",
},
err: fmt.Errorf("Unable to parse from flag 'test': \nAccepted schemes are: tfstate://,tfstate+s3://,tfstate+http://,tfstate+https://,tfstate+tfcloud://,tfstate+gs://"),
err: fmt.Errorf("Unable to parse from flag 'test': \nAccepted schemes are: tfstate://,tfstate+s3://,tfstate+http://,tfstate+https://,tfstate+tfcloud://,tfstate+gs://,tfstate+azurerm://"),
},
{
env: map[string]string{

View File

@ -80,14 +80,14 @@ func TestScanCmd_Invalid(t *testing.T) {
{args: []string{"scan", "-f"}, expected: `flag needs an argument: 'f' in -f`},
{args: []string{"scan", "--from"}, expected: `flag needs an argument: --from`},
{args: []string{"scan", "--from"}, expected: `flag needs an argument: --from`},
{args: []string{"scan", "--from", "tosdgjhgsdhgkjs"}, expected: "Unable to parse from flag 'tosdgjhgsdhgkjs': \nAccepted schemes are: tfstate://,tfstate+s3://,tfstate+http://,tfstate+https://,tfstate+tfcloud://,tfstate+gs://"},
{args: []string{"scan", "--from", "://"}, expected: "Unable to parse from flag '://': \nAccepted schemes are: tfstate://,tfstate+s3://,tfstate+http://,tfstate+https://,tfstate+tfcloud://,tfstate+gs://"},
{args: []string{"scan", "--from", "://test"}, expected: "Unable to parse from flag '://test': \nAccepted schemes are: tfstate://,tfstate+s3://,tfstate+http://,tfstate+https://,tfstate+tfcloud://,tfstate+gs://"},
{args: []string{"scan", "--from", "tosdgjhgsdhgkjs://"}, expected: "Unable to parse from flag 'tosdgjhgsdhgkjs://': \nAccepted schemes are: tfstate://,tfstate+s3://,tfstate+http://,tfstate+https://,tfstate+tfcloud://,tfstate+gs://"},
{args: []string{"scan", "--from", "terraform+foo+bar://test"}, expected: "Unable to parse from scheme 'terraform+foo+bar': \nAccepted schemes are: tfstate://,tfstate+s3://,tfstate+http://,tfstate+https://,tfstate+tfcloud://,tfstate+gs://"},
{args: []string{"scan", "--from", "tosdgjhgsdhgkjs"}, expected: "Unable to parse from flag 'tosdgjhgsdhgkjs': \nAccepted schemes are: tfstate://,tfstate+s3://,tfstate+http://,tfstate+https://,tfstate+tfcloud://,tfstate+gs://,tfstate+azurerm://"},
{args: []string{"scan", "--from", "://"}, expected: "Unable to parse from flag '://': \nAccepted schemes are: tfstate://,tfstate+s3://,tfstate+http://,tfstate+https://,tfstate+tfcloud://,tfstate+gs://,tfstate+azurerm://"},
{args: []string{"scan", "--from", "://test"}, expected: "Unable to parse from flag '://test': \nAccepted schemes are: tfstate://,tfstate+s3://,tfstate+http://,tfstate+https://,tfstate+tfcloud://,tfstate+gs://,tfstate+azurerm://"},
{args: []string{"scan", "--from", "tosdgjhgsdhgkjs://"}, expected: "Unable to parse from flag 'tosdgjhgsdhgkjs://': \nAccepted schemes are: tfstate://,tfstate+s3://,tfstate+http://,tfstate+https://,tfstate+tfcloud://,tfstate+gs://,tfstate+azurerm://"},
{args: []string{"scan", "--from", "terraform+foo+bar://test"}, expected: "Unable to parse from scheme 'terraform+foo+bar': \nAccepted schemes are: tfstate://,tfstate+s3://,tfstate+http://,tfstate+https://,tfstate+tfcloud://,tfstate+gs://,tfstate+azurerm://"},
{args: []string{"scan", "--from", "unsupported://test"}, expected: "Unsupported IaC source 'unsupported': \nAccepted values are: tfstate"},
{args: []string{"scan", "--from", "tfstate+foobar://test"}, expected: "Unsupported IaC backend 'foobar': \nAccepted values are: s3,http,https,tfcloud,gs"},
{args: []string{"scan", "--from", "tfstate:///tmp/test", "--from", "tfstate+toto://test"}, expected: "Unsupported IaC backend 'toto': \nAccepted values are: s3,http,https,tfcloud,gs"},
{args: []string{"scan", "--from", "tfstate+foobar://test"}, expected: "Unsupported IaC backend 'foobar': \nAccepted values are: s3,http,https,tfcloud,gs,azurerm"},
{args: []string{"scan", "--from", "tfstate:///tmp/test", "--from", "tfstate+toto://test"}, expected: "Unsupported IaC backend 'toto': \nAccepted values are: s3,http,https,tfcloud,gs,azurerm"},
{args: []string{"scan", "--filter", "Type='test'"}, expected: "unable to parse filter expression: SyntaxError: Expected tRbracket, received: tUnknown"},
{args: []string{"scan", "--filter", "Type='test'", "--filter", "Type='test2'"}, expected: "Filter flag should be specified only once"},
{args: []string{"scan", "--tf-provider-version", ".30.2"}, expected: "Invalid version argument .30.2, expected a valid semver string (e.g. 2.13.4)"},

27
pkg/helpers/azure/blob.go Normal file
View File

@ -0,0 +1,27 @@
package azure
import (
"os"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/pkg/errors"
)
func GetBlobSharedKey() (*azblob.SharedKeyCredential, error) {
storageAccountName, exist := os.LookupEnv("AZURE_STORAGE_ACCOUNT")
if !exist {
return nil, errors.New("AZURE_STORAGE_ACCOUNT should be defined to be able to read state from azure backend")
}
storageAccountKey, exist := os.LookupEnv("AZURE_STORAGE_KEY")
if !exist {
return nil, errors.New("AZURE_STORAGE_KEY should be defined to be able to read state from azure backend")
}
credential, err := azblob.NewSharedKeyCredential(storageAccountName, storageAccountKey)
if err != nil {
return nil, err
}
return credential, nil
}

View File

@ -114,6 +114,7 @@ func TestGetSupportedSchemes(t *testing.T) {
"tfstate+https://",
"tfstate+tfcloud://",
"tfstate+gs://",
"tfstate+azurerm://",
}
if got := GetSupportedSchemes(); !reflect.DeepEqual(got, want) {

View File

@ -0,0 +1,69 @@
package backend
import (
"context"
"fmt"
"io"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/pkg/errors"
"github.com/snyk/driftctl/pkg/helpers/azure"
)
const BackendKeyAzureRM = "azurerm"
type AzureRMBackend struct {
reader io.ReadCloser
storageClient azblob.BlockBlobClient
}
func NewAzureRMReader(path string) (*AzureRMBackend, error) {
bucketPath := strings.Split(path, "/")
if len(bucketPath) < 2 || bucketPath[1] == "" {
return nil, errors.Errorf("Unable to parse azurerm backend storage path: %s. Must be CONTAINER/PATH/TO/OBJECT", path)
}
containerName := bucketPath[0]
objectPath := strings.Join(bucketPath[1:], "/")
credential, err := azure.GetBlobSharedKey()
if err != nil {
return nil, err
}
blobClient, err := azblob.NewBlockBlobClientWithSharedKey(
fmt.Sprintf(
"https://%s.blob.core.windows.net/%s/%s",
credential.AccountName(),
containerName,
objectPath,
),
credential,
nil,
)
if err != nil {
return nil, err
}
return &AzureRMBackend{
storageClient: blobClient,
}, nil
}
func (s *AzureRMBackend) Read(p []byte) (int, error) {
if s.reader == nil {
ctx := context.Background()
data, err := s.storageClient.Download(ctx, nil)
if err != nil {
return 0, err
}
s.reader = data.Body(azblob.RetryReaderOptions{})
}
return s.reader.Read(p)
}
func (s *AzureRMBackend) Close() error {
if s.reader != nil {
return s.reader.Close()
}
return errors.New("Unable to close reader as nothing was opened")
}

View File

@ -0,0 +1,70 @@
package backend
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewAzureRMReader(t *testing.T) {
tests := []struct {
name string
path string
preTest func(t *testing.T)
wantErr assert.ErrorAssertionFunc
}{
{
name: "invalid path",
path: "containerName/",
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
assert.Equal(t, "Unable to parse azurerm backend storage path: containerName/. Must be CONTAINER/PATH/TO/OBJECT", err.Error())
return true
},
},
{
name: "valid path but missing AZURE_STORAGE_ACCOUNT",
path: "containerName/valid.tfstate",
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
assert.Equal(t, "AZURE_STORAGE_ACCOUNT should be defined to be able to read state from azure backend", err.Error())
return true
},
},
{
name: "valid path but missing AZURE_STORAGE_KEY",
path: "containerName/valid.tfstate",
preTest: func(t *testing.T) {
t.Setenv("AZURE_STORAGE_ACCOUNT", "foobar")
},
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
assert.Equal(t, "AZURE_STORAGE_KEY should be defined to be able to read state from azure backend", err.Error())
return true
},
},
// This is not supposed to do any network call during azure client init
// It this behavior change that logic should be moved in the Read function like we already
// did for some other backend
{
name: "valid",
path: "containerName/valid.tfstate",
preTest: func(t *testing.T) {
t.Setenv("AZURE_STORAGE_ACCOUNT", "foobar")
t.Setenv("AZURE_STORAGE_KEY", "barfoo")
},
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
return false
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.preTest != nil {
tt.preTest(t)
}
_, err := NewAzureRMReader(tt.path)
if !tt.wantErr(t, err, fmt.Sprintf("NewAzureRMReader(%v)", tt.path)) {
return
}
})
}
}

View File

@ -16,6 +16,7 @@ var supportedBackends = []string{
BackendKeyHTTPS,
BackendKeyTFCloud,
BackendKeyGS,
BackendKeyAzureRM,
}
type Backend io.ReadCloser
@ -56,6 +57,8 @@ func GetBackend(config config.SupplierConfig, opts *Options) (Backend, error) {
return NewTFCloudReader(config.Path, opts), nil
case BackendKeyGS:
return NewGSReader(config.Path)
case BackendKeyAzureRM:
return NewAzureRMReader(config.Path)
default:
return nil, errors.Errorf("Unsupported backend '%s'", backend)
}