From 253461fb3872a777c1ee4c22ff04b2f70ca01ab6 Mon Sep 17 00:00:00 2001 From: Chris Mellard Date: Mon, 20 Jul 2020 23:04:04 +1200 Subject: [PATCH] fix: Adding in resourceGroup and bucketName to get Velero Azure plugin operational feat: Added in half implementation of AKSBucketProvider Encapsulated the storage account parsing down close the CLI which appears to be the only part that requires them as separate arguments ... to get Velero boot operational Signed-off-by: Chris Mellard Fixed up import Fixed imports --- pkg/cloud/aks/aks.go | 116 +++++++++++ pkg/cloud/aks/interface.go | 8 + pkg/cloud/aks/mocks/azurestorage.go | 197 ++++++++++++++++++ pkg/cloud/aks/storage/bucket_provider.go | 78 +++++++ pkg/cloud/aks/storage/bucket_provider_test.go | 50 +++++ pkg/cloud/factory/factory.go | 3 + pkg/config/install_requirements.go | 4 + 7 files changed, 456 insertions(+) create mode 100644 pkg/cloud/aks/interface.go create mode 100644 pkg/cloud/aks/mocks/azurestorage.go create mode 100644 pkg/cloud/aks/storage/bucket_provider.go create mode 100644 pkg/cloud/aks/storage/bucket_provider_test.go diff --git a/pkg/cloud/aks/aks.go b/pkg/cloud/aks/aks.go index 477e7924264..ef0cdb85a13 100644 --- a/pkg/cloud/aks/aks.go +++ b/pkg/cloud/aks/aks.go @@ -3,10 +3,13 @@ package aks import ( b64 "encoding/base64" "encoding/json" + "fmt" + "regexp" "strings" "github.com/jenkins-x/jx-logging/pkg/log" "github.com/jenkins-x/jx/v2/pkg/util" + "github.com/pkg/errors" ) // AzureRunner an Azure CLI runner to interact with Azure @@ -28,6 +31,10 @@ type acr struct { Name string `json:"name"` } +type containerExists struct { + Exists bool `json:"exists"` +} + type password struct { Name string `json:"name"` Value string `json:"value"` @@ -46,6 +53,10 @@ type config struct { Auths map[string]*auth `json:"auths,omitempty"` } +var ( + azureContainerURIRegExp = regexp.MustCompile(`https://(?P\w+)\.blob\.core\.windows\.net/(?P\w+)`) +) + // NewAzureRunnerWithCommander specific the command runner for Azure CLI. func NewAzureRunnerWithCommander(runner util.Commander) *AzureRunner { return &AzureRunner{ @@ -242,3 +253,108 @@ func (az *AzureRunner) azureCLI(args ...string) (string, error) { az.Runner.SetArgs(args) return az.Runner.RunWithoutRetry() } + +func parseContainerURL(bucketURL string) (string, string, error) { + match := azureContainerURIRegExp.FindStringSubmatch(bucketURL) + if len(match) == 3 { + return match[1], match[2], nil + } + return "", "", errors.New(fmt.Sprintf("Azure Blob Container Url %s could not be parsed to determine storage account and container name", bucketURL)) +} + +// ContainerExists checks if an Azure Storage Container exists +func (az *AzureRunner) ContainerExists(bucketURL string) (bool, error) { + storageAccount, bucketName, err := parseContainerURL(bucketURL) + if err != nil { + return false, err + } + + accessKey, err := az.GetStorageAccessKey(storageAccount) + if err != nil { + return false, err + } + + bucketExistsArgs := []string{ + "storage", + "container", + "exists", + "-n", + bucketName, + "--account-name", + storageAccount, + "--account-key", + accessKey, + } + + cmdResult, err := az.azureCLI(bucketExistsArgs...) + + if err != nil { + log.Logger().Infof("Error checking bucket exists: %s, %s", cmdResult, err) + return false, err + } + + containerExists := containerExists{} + err = json.Unmarshal([]byte(cmdResult), &containerExists) + if err != nil { + return false, errors.Wrap(err, "unmarshalling Azure container exists command") + } + return containerExists.Exists, nil + +} + +// CreateContainer creates a Blob container within Azure Storage +func (az *AzureRunner) CreateContainer(bucketURL string) error { + storageAccount, bucketName, err := parseContainerURL(bucketURL) + if err != nil { + return err + } + + accessKey, err := az.GetStorageAccessKey(storageAccount) + if err != nil { + return err + } + + createContainerArgs := []string{ + "storage", + "container", + "create", + "-n", + bucketName, + "--account-name", + storageAccount, + "--fail-on-exist", + "--account-key", + accessKey, + } + + cmdResult, err := az.azureCLI(createContainerArgs...) + + if err != nil { + log.Logger().Infof("Error creating bucket: %s, %s", cmdResult, err) + return err + } + + return nil +} + +// GetStorageAccessKey retrieves access keys for an Azure storage account +func (az *AzureRunner) GetStorageAccessKey(storageAccount string) (string, error) { + getStorageAccessKeyArgs := []string{ + "storage", + "account", + "keys", + "list", + "-n", + storageAccount, + "--query", + "[?keyName=='key1'].value | [0]", + } + + cmdResult, err := az.azureCLI(getStorageAccessKeyArgs...) + + if err != nil { + return "", err + } + + return cmdResult, nil +} diff --git a/pkg/cloud/aks/interface.go b/pkg/cloud/aks/interface.go new file mode 100644 index 00000000000..1723241accc --- /dev/null +++ b/pkg/cloud/aks/interface.go @@ -0,0 +1,8 @@ +package aks + +// AzureStorage Interface for Azure Storage commands +type AzureStorage interface { + ContainerExists(bucketURL string) (bool, error) + CreateContainer(bucketURL string) error + GetStorageAccessKey(storageAccount string) (string, error) +} diff --git a/pkg/cloud/aks/mocks/azurestorage.go b/pkg/cloud/aks/mocks/azurestorage.go new file mode 100644 index 00000000000..7edf9868171 --- /dev/null +++ b/pkg/cloud/aks/mocks/azurestorage.go @@ -0,0 +1,197 @@ +// Code generated by pegomock. DO NOT EDIT. +// Source: github.com/jenkins-x/jx/v2/pkg/cloud/aks (interfaces: AzureStorage) + +package aks_test + +import ( + "reflect" + "time" + + pegomock "github.com/petergtz/pegomock" +) + +type MockAzureStorage struct { + fail func(message string, callerSkip ...int) +} + +func NewMockAzureStorage(options ...pegomock.Option) *MockAzureStorage { + mock := &MockAzureStorage{} + for _, option := range options { + option.Apply(mock) + } + return mock +} + +func (mock *MockAzureStorage) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } +func (mock *MockAzureStorage) FailHandler() pegomock.FailHandler { return mock.fail } + +func (mock *MockAzureStorage) ContainerExists(_param0 string) (bool, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockAzureStorage().") + } + params := []pegomock.Param{_param0} + result := pegomock.GetGenericMockFrom(mock).Invoke("ContainerExists", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 bool + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(bool) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + +func (mock *MockAzureStorage) CreateContainer(_param0 string) error { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockAzureStorage().") + } + params := []pegomock.Param{_param0} + result := pegomock.GetGenericMockFrom(mock).Invoke("CreateContainer", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(error) + } + } + return ret0 +} + +func (mock *MockAzureStorage) GetStorageAccessKey(_param0 string) (string, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockAzureStorage().") + } + params := []pegomock.Param{_param0} + result := pegomock.GetGenericMockFrom(mock).Invoke("GetStorageAccessKey", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 string + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(string) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + +func (mock *MockAzureStorage) VerifyWasCalledOnce() *VerifierMockAzureStorage { + return &VerifierMockAzureStorage{ + mock: mock, + invocationCountMatcher: pegomock.Times(1), + } +} + +func (mock *MockAzureStorage) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockAzureStorage { + return &VerifierMockAzureStorage{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + } +} + +func (mock *MockAzureStorage) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockAzureStorage { + return &VerifierMockAzureStorage{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + inOrderContext: inOrderContext, + } +} + +func (mock *MockAzureStorage) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockAzureStorage { + return &VerifierMockAzureStorage{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + timeout: timeout, + } +} + +type VerifierMockAzureStorage struct { + mock *MockAzureStorage + invocationCountMatcher pegomock.Matcher + inOrderContext *pegomock.InOrderContext + timeout time.Duration +} + +func (verifier *VerifierMockAzureStorage) ContainerExists(_param0 string) *MockAzureStorage_ContainerExists_OngoingVerification { + params := []pegomock.Param{_param0} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ContainerExists", params, verifier.timeout) + return &MockAzureStorage_ContainerExists_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockAzureStorage_ContainerExists_OngoingVerification struct { + mock *MockAzureStorage + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockAzureStorage_ContainerExists_OngoingVerification) GetCapturedArguments() string { + _param0 := c.GetAllCapturedArguments() + return _param0[len(_param0)-1] +} + +func (c *MockAzureStorage_ContainerExists_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]string, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(string) + } + } + return +} + +func (verifier *VerifierMockAzureStorage) CreateContainer(_param0 string) *MockAzureStorage_CreateContainer_OngoingVerification { + params := []pegomock.Param{_param0} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CreateContainer", params, verifier.timeout) + return &MockAzureStorage_CreateContainer_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockAzureStorage_CreateContainer_OngoingVerification struct { + mock *MockAzureStorage + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockAzureStorage_CreateContainer_OngoingVerification) GetCapturedArguments() string { + _param0 := c.GetAllCapturedArguments() + return _param0[len(_param0)-1] +} + +func (c *MockAzureStorage_CreateContainer_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]string, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(string) + } + } + return +} + +func (verifier *VerifierMockAzureStorage) GetStorageAccessKey(_param0 string) *MockAzureStorage_GetStorageAccessKey_OngoingVerification { + params := []pegomock.Param{_param0} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetStorageAccessKey", params, verifier.timeout) + return &MockAzureStorage_GetStorageAccessKey_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockAzureStorage_GetStorageAccessKey_OngoingVerification struct { + mock *MockAzureStorage + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockAzureStorage_GetStorageAccessKey_OngoingVerification) GetCapturedArguments() string { + _param0 := c.GetAllCapturedArguments() + return _param0[len(_param0)-1] +} + +func (c *MockAzureStorage_GetStorageAccessKey_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]string, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(string) + } + } + return +} diff --git a/pkg/cloud/aks/storage/bucket_provider.go b/pkg/cloud/aks/storage/bucket_provider.go new file mode 100644 index 00000000000..e63c12f27ee --- /dev/null +++ b/pkg/cloud/aks/storage/bucket_provider.go @@ -0,0 +1,78 @@ +package storage + +import ( + "fmt" + "io" + "strings" + + "github.com/google/uuid" + "github.com/jenkins-x/jx-logging/pkg/log" + "github.com/jenkins-x/jx/v2/pkg/cloud/aks" + "github.com/jenkins-x/jx/v2/pkg/cloud/buckets" + "github.com/jenkins-x/jx/v2/pkg/config" + "github.com/jenkins-x/jx/v2/pkg/util" + "github.com/pkg/errors" +) + +// AKSBucketProvider the bucket provider for Azure +type AKSBucketProvider struct { + Requirements *config.RequirementsConfig + AzureStorage aks.AzureStorage +} + +// CreateNewBucketForCluster creates a new dynamic bucket +func (b *AKSBucketProvider) CreateNewBucketForCluster(clusterName string, bucketKind string) (string, error) { + uuid := uuid.New() + bucketName := fmt.Sprintf("%s-%s-%s", clusterName, bucketKind, uuid.String()) + + // Max length is 63, https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata + if len(bucketName) > 63 { + bucketName = bucketName[:63] + } + bucketName = strings.TrimRight(bucketName, "-") + bucketURL := fmt.Sprintf("https://%s.blob.core.windows.net/%s", b.Requirements.Velero.ServiceAccount, bucketName) + err := b.EnsureBucketIsCreated(bucketURL) + if err != nil { + return bucketURL, errors.Wrapf(err, "failed to create bucket %s", bucketURL) + } + + return bucketURL, nil +} + +// EnsureBucketIsCreated ensures the bucket URL is created +func (b *AKSBucketProvider) EnsureBucketIsCreated(bucketURL string) error { + + exists, err := b.AzureStorage.ContainerExists(bucketURL) + if err != nil { + return errors.Wrap(err, "checking if the provided container exists") + } + if exists { + return nil + } + + log.Logger().Infof("The bucket %s does not exist so lets create it", util.ColorInfo(bucketURL)) + err = b.AzureStorage.CreateContainer(bucketURL) + if err != nil { + return errors.Wrapf(err, "there was a problem creating the bucket with URL %s", + bucketURL) + } + return nil +} + +// UploadFileToBucket is yet to be implemented for this provider +func (b *AKSBucketProvider) UploadFileToBucket(r io.Reader, outputName string, bucketURL string) (string, error) { + return "", nil +} + +// DownloadFileFromBucket is yet to be implemented for this provider +func (b *AKSBucketProvider) DownloadFileFromBucket(bucketURL string) (io.ReadCloser, error) { + return nil, nil +} + +// NewAKSBucketProvider create a new provider for AKS +func NewAKSBucketProvider(requirements *config.RequirementsConfig) buckets.Provider { + return &AKSBucketProvider{ + Requirements: requirements, + AzureStorage: aks.NewAzureRunner(), + } +} diff --git a/pkg/cloud/aks/storage/bucket_provider_test.go b/pkg/cloud/aks/storage/bucket_provider_test.go new file mode 100644 index 00000000000..483a321b4ba --- /dev/null +++ b/pkg/cloud/aks/storage/bucket_provider_test.go @@ -0,0 +1,50 @@ +// +build unit + +package storage + +import ( + "strings" + "testing" + + aks_test "github.com/jenkins-x/jx/v2/pkg/cloud/aks/mocks" + "github.com/jenkins-x/jx/v2/pkg/config" + "github.com/petergtz/pegomock" + "github.com/stretchr/testify/assert" +) + +func TestCreateNewBucketForClusterWithLongClusterNameAndDashAtCharacterSixty(t *testing.T) { + p := AKSBucketProvider{ + AzureStorage: aks_test.NewMockAzureStorage(), + Requirements: &config.RequirementsConfig{ + Velero: config.VeleroConfig{ + ServiceAccount: "teststorage", + }, + }, + } + + pegomock.When(p.AzureStorage.ContainerExists(pegomock.AnyString())).ThenReturn(true, nil) + + bucketName, err := p.CreateNewBucketForCluster("rrehhhhhhhhhhhhhhhhhhhhhhhhhj3j3k2kwkdkjdbiwabduwabduoawbdb-dbwdbwaoud", "logs") + assert.NoError(t, err) + assert.NotNil(t, bucketName, "it should always generate a name") + assert.False(t, strings.HasSuffix(bucketName, "-"), "the bucket can't end with a dash") + assert.Equal(t, "https://teststorage.blob.core.windows.net/rrehhhhhhhhhhhhhhhhhhhhhhhhhj3j3k2kwkdkjdbiwabduwabduoawbdb-dbw", bucketName) +} + +func TestCreateNewBucketForClusterWithSmallClusterName(t *testing.T) { + p := AKSBucketProvider{ + AzureStorage: aks_test.NewMockAzureStorage(), + Requirements: &config.RequirementsConfig{ + Velero: config.VeleroConfig{ + ServiceAccount: "teststorage", + }, + }, + } + + pegomock.When(p.AzureStorage.ContainerExists(pegomock.AnyString())).ThenReturn(true, nil) + + bucketName, err := p.CreateNewBucketForCluster("cluster", "logs") + assert.NoError(t, err) + assert.NotNil(t, bucketName, "it should always generate a name") + assert.False(t, strings.HasSuffix(bucketName, "-"), "the bucket can't end with a dash") +} diff --git a/pkg/cloud/factory/factory.go b/pkg/cloud/factory/factory.go index fbe865a533c..e343568bae6 100644 --- a/pkg/cloud/factory/factory.go +++ b/pkg/cloud/factory/factory.go @@ -4,6 +4,7 @@ import ( v1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1" "github.com/jenkins-x/jx-logging/pkg/log" "github.com/jenkins-x/jx/v2/pkg/cloud" + aksStorage "github.com/jenkins-x/jx/v2/pkg/cloud/aks/storage" amazonStorage "github.com/jenkins-x/jx/v2/pkg/cloud/amazon/storage" "github.com/jenkins-x/jx/v2/pkg/cloud/buckets" "github.com/jenkins-x/jx/v2/pkg/cloud/gke/storage" @@ -23,6 +24,8 @@ func NewBucketProvider(requirements *config.RequirementsConfig) buckets.Provider fallthrough case cloud.AWS: return amazonStorage.NewAmazonBucketProvider(requirements) + case cloud.AKS: + return aksStorage.NewAKSBucketProvider(requirements) default: // we have an implementation for GKE / EKS but not for AKS so we should fall back to default // but we don't have every func implemented diff --git a/pkg/config/install_requirements.go b/pkg/config/install_requirements.go index 72af4f5b108..d58c565cac6 100644 --- a/pkg/config/install_requirements.go +++ b/pkg/config/install_requirements.go @@ -489,6 +489,10 @@ type VeleroConfig struct { Schedule string `json:"schedule" envconfig:"JX_REQUIREMENT_VELERO_SCHEDULE"` // TimeToLive period for backups to be retained TimeToLive string `json:"ttl" envconfig:"JX_REQUIREMENT_VELERO_TTL"` + // ResourceGroup for Velero Bucket + ResourceGroup string `json:"resourceGroup"` + // BucketName for Velero Bucket + BucketName string `json:"bucketName"` } // AutoUpdateConfig contains auto update config