feat: add azure blob backend
parent
5dd9162923
commit
7778462ade
|
@ -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{
|
||||
|
|
|
@ -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)"},
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -114,6 +114,7 @@ func TestGetSupportedSchemes(t *testing.T) {
|
|||
"tfstate+https://",
|
||||
"tfstate+tfcloud://",
|
||||
"tfstate+gs://",
|
||||
"tfstate+azurerm://",
|
||||
}
|
||||
|
||||
if got := GetSupportedSchemes(); !reflect.DeepEqual(got, want) {
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue