Skip to content

Commit

Permalink
Enhancing etcd-backup-restore to support Azurite - the Azure Blob…
Browse files Browse the repository at this point in the history
… Storage emulator (#699)

* Added support to `etcd-backup-restore` to make use of `Azurite` - the Azure Blob Storage Emulator.

* Two environment variables `EMULATOR_ENABLED` and `AZURE_STORAGE_API_ENDPOINT` are introduced whose values change `etcd-backup-restore`'s behavior for Azure such that Azurite can be used as the object store.
  • Loading branch information
renormalize authored Feb 13, 2024
1 parent 178acc5 commit edff58c
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 6 deletions.
6 changes: 6 additions & 0 deletions chart/etcd-backup-restore/templates/etcd-backup-secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ data:
{{- else if eq .Values.backup.storageProvider "ABS" }}
storageAccount: {{ .Values.backup.abs.storageAccount | b64enc }}
storageKey : {{ .Values.backup.abs.storageKey | b64enc }}
{{- if .Values.backup.abs.emulatorEnabled }}
emulatorEnabled: {{ .Values.backup.abs.emulatorEnabled | b64enc}}
{{- end }}
{{- if .Values.backup.abs.storageAPIEndpoint }}
storageAPIEndpoint: {{ .Values.backup.abs.storageAPIEndpoint | b64enc}}
{{- end }}
{{- else if eq .Values.backup.storageProvider "GCS" }}
serviceaccount.json : {{ .Values.backup.gcs.serviceAccountJson | b64enc }}
{{- if .Values.backup.gcs.storageAPIEndpoint }}
Expand Down
14 changes: 14 additions & 0 deletions chart/etcd-backup-restore/templates/etcd-statefulset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,20 @@ spec:
secretKeyRef:
name: {{ .Release.Name }}-etcd-backup
key: "storageKey"
{{- if .Values.backup.abs.emulatorEnabled }}
- name: "EMULATOR_ENABLED"
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-etcd-backup
key: "emulatorEnabled"
{{- end }}
{{- if .Values.backup.abs.storageAPIEndpoint }}
- name: "AZURE_STORAGE_API_ENDPOINT"
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-etcd-backup
key: "storageAPIEndpoint"
{{- end }}
{{- else if eq .Values.backup.storageProvider "GCS" }}
- name: "GOOGLE_APPLICATION_CREDENTIALS"
value: "/root/.gcp/serviceaccount.json"
Expand Down
2 changes: 2 additions & 0 deletions chart/etcd-backup-restore/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ backup:
# abs:
# storageAccount: storage-account-with-object-storage-privileges
# storageKey: storage-key-with-object-storage-privileges
# emulatorEnabled: boolean-float-to-enable-e2e-tests-to-use-azure-emulator # optional
# storageAPIEndpoint: endpoint-override-for-storage-api # if emulatorEnabled is true
# swift:
# authURL: identity-server-url
# domainName: domain-name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ type: Opaque
data:
storageAccount: YWRtaW4= # admin
storageKey: YWRtaW4= # admin
# emulatorEnabled: dHJ1ZQ== # true (optional)
# storageAPIEndpoint: aHR0cDovL2F6dXJpdGUtc2VydmljZToxMDAwMA== # http://azurite-service:10000 (optional)

#### OR ####

Expand Down
46 changes: 40 additions & 6 deletions pkg/snapstore/abs_snapstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
Expand All @@ -38,6 +39,8 @@ import (
const (
absCredentialDirectory = "AZURE_APPLICATION_CREDENTIALS"
absCredentialJSONFile = "AZURE_APPLICATION_CREDENTIALS_JSON"
// AzuriteEndpoint is the environment variable which indicates the endpoint at which the Azurite emulator is hosted
AzuriteEndpoint = "AZURE_STORAGE_API_ENDPOINT"
)

// ABSSnapStore is an ABS backed snapstore.
Expand All @@ -56,32 +59,63 @@ type absCredentials struct {
StorageAccount string `json:"storageAccount"`
}

// NewABSSnapStore create new ABSSnapStore from shared configuration with specified bucket
// NewABSSnapStore creates a new ABSSnapStore using a shared configuration and a specified bucket
func NewABSSnapStore(config *brtypes.SnapstoreConfig) (*ABSSnapStore, error) {
storageAccount, storageKey, err := getCredentials(getEnvPrefixString(config.IsSource))
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to get credentials: %v", err)
}

credentials, err := azblob.NewSharedKeyCredential(storageAccount, storageKey)
if err != nil {
return nil, fmt.Errorf("failed to create shared key credentials: %v", err)
}

p := azblob.NewPipeline(credentials, azblob.PipelineOptions{
pipeline := azblob.NewPipeline(credentials, azblob.PipelineOptions{
Retry: azblob.RetryOptions{
TryTimeout: downloadTimeout,
}})
u, err := url.Parse(fmt.Sprintf("https://%s.%s", storageAccount, brtypes.AzureBlobStorageHostName))

blobURL, err := ConstructBlobServiceURL(credentials)
if err != nil {
return nil, fmt.Errorf("failed to parse service url: %v", err)
return nil, err
}
serviceURL := azblob.NewServiceURL(*u, p)

serviceURL := azblob.NewServiceURL(*blobURL, pipeline)
containerURL := serviceURL.NewContainerURL(config.Container)

return GetABSSnapstoreFromClient(config.Container, config.Prefix, config.TempDir, config.MaxParallelChunkUploads, config.MinChunkSize, &containerURL)
}

// ConstructBlobServiceURL constructs the Blob Service URL based on the activation status of the Azurite Emulator.
// It checks the environment variables for emulator configuration and constructs the URL accordingly.
// The function expects two environment variables:
// - EMULATOR_ENABLED: Indicates whether the Azurite Emulator is enabled (expects "true" or "false").
// - AZURE_STORAGE_API_ENDPOINT: Specifies the Azurite Emulator endpoint when the emulator is enabled.
func ConstructBlobServiceURL(credentials *azblob.SharedKeyCredential) (*url.URL, error) {
defaultURL, err := url.Parse(fmt.Sprintf("https://%s.%s", credentials.AccountName(), brtypes.AzureBlobStorageHostName))
if err != nil {
return nil, fmt.Errorf("failed to parse default service URL: %w", err)
}
emulatorEnabled, ok := os.LookupEnv(EnvEmulatorEnabled)
if !ok {
return defaultURL, nil
}
isEmulator, err := strconv.ParseBool(emulatorEnabled)
if err != nil {
return nil, fmt.Errorf("invalid value for %s: %s, error: %w", EnvEmulatorEnabled, emulatorEnabled, err)
}
if !isEmulator {
return defaultURL, nil
}
endpoint, ok := os.LookupEnv(AzuriteEndpoint)
if !ok {
return nil, fmt.Errorf("%s environment variable not set while %s is true", AzuriteEndpoint, EnvEmulatorEnabled)
}
// Application protocol (http or https) is determined by the user of the Azurite, not by this function.
return url.Parse(fmt.Sprintf("%s/%s", endpoint, credentials.AccountName()))
}

func getCredentials(prefixString string) (string, string, error) {

if filename, isSet := os.LookupEnv(prefixString + absCredentialJSONFile); isSet {
Expand Down
3 changes: 3 additions & 0 deletions pkg/snapstore/snapstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const (

backupVersionV1 = "v1"
backupVersionV2 = "v2"

// EnvEmulatorEnabled is the environment variable which indicates the usage of a storage emulator like Azurite, fake-gcs-server
EnvEmulatorEnabled = "EMULATOR_ENABLED"
)

type chunk struct {
Expand Down
50 changes: 50 additions & 0 deletions pkg/snapstore/snapstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"strings"
"time"

"github.com/Azure/azure-storage-blob-go/azblob"
. "github.com/gardener/etcd-backup-restore/pkg/snapstore"
brtypes "github.com/gardener/etcd-backup-restore/pkg/types"
fake "github.com/gophercloud/gophercloud/testhelper/client"
Expand Down Expand Up @@ -459,6 +460,55 @@ var _ = Describe("Dynamic access credential rotation test for each provider", fu
}
})

var _ = Describe("Blob Service URL construction for Azure", func() {
var credentials *azblob.SharedKeyCredential
BeforeEach(func() {
var err error
// test strings
storageAccount, storageKey := "testAccountName", "dGVzdEFjY291bnRLZXk="
credentials, err = azblob.NewSharedKeyCredential(storageAccount, storageKey)
Expect(err).ShouldNot(HaveOccurred())
})
Context(fmt.Sprintf("when the environment variable %q is not set", EnvEmulatorEnabled), func() {
It("should return the default blob service URL", func() {
blobServiceURL, err := ConstructBlobServiceURL(credentials)
Expect(err).ShouldNot(HaveOccurred())
Expect(blobServiceURL.String()).Should(Equal(fmt.Sprintf("https://%s.%s", credentials.AccountName(), brtypes.AzureBlobStorageHostName)))
})
})
Context(fmt.Sprintf("when the environment variable %q is set", EnvEmulatorEnabled), func() {
Context("to values which are not \"true\"", func() {
It("should error when the environment variable is not \"true\" or \"false\"", func() {
GinkgoT().Setenv(EnvEmulatorEnabled, "")
_, err := ConstructBlobServiceURL(credentials)
Expect(err).Should(HaveOccurred())
})
It("should return the default blob service URL when the environment variable is set to \"false\"", func() {
GinkgoT().Setenv(EnvEmulatorEnabled, "false")
blobServiceURL, err := ConstructBlobServiceURL(credentials)
Expect(err).ShouldNot(HaveOccurred())
Expect(blobServiceURL.String()).Should(Equal(fmt.Sprintf("https://%s.%s", credentials.AccountName(), brtypes.AzureBlobStorageHostName)))
})
})
Context("to \"true\"", func() {
const endpoint string = "http://localhost:12345"
BeforeEach(func() {
GinkgoT().Setenv(EnvEmulatorEnabled, "true")
})
It(fmt.Sprintf("should error when the %q environment variable is not set", AzuriteEndpoint), func() {
_, err := ConstructBlobServiceURL(credentials)
Expect(err).Should(HaveOccurred())
})
It(fmt.Sprintf("should return the Azurite blob service URL when the %q environment variable is set to %q", AzuriteEndpoint, endpoint), func() {
GinkgoT().Setenv(AzuriteEndpoint, endpoint)
blobServiceURL, err := ConstructBlobServiceURL(credentials)
Expect(err).ShouldNot(HaveOccurred())
Expect(blobServiceURL.String()).Should(Equal(fmt.Sprintf("%s/%s", endpoint, credentials.AccountName())))
})
})
})
})

// createCredentialFilesInDirectory creates access credential files in the
// specified directory and returns the timestamp of the last modified file.
func createCredentialFilesInDirectory(directory string, filenames []string) (time.Time, error) {
Expand Down

0 comments on commit edff58c

Please sign in to comment.