Skip to content

Commit 8d34b1b

Browse files
leonardocesxd
andcommitted
feat: add native support for Azure Blob Storage
Barman Cloud now support Azure Blob Storage natively, without having to deal with MinIO Gateway. This patch makes Cloud Native PostgreSQL use that new feature and backup/restore clusters and WALs directly to Azure Blob Storage. The operator now support authenticating via: 1. connection string 2. storage account name and key 3. storage account name and SAS token Co-authored-by: Leonardo Cecchi <leonardo.cecchi@enterprisedb.com> Co-authored-by: Jonathan Gonzalez V <jonathan.gonzalez@enterprisedb.com>
1 parent ca68f72 commit 8d34b1b

32 files changed

+1675
-204
lines changed

.wordlist-en-custom.txt

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ AdditionalPodAntiAffinity
77
AffinityConfiguration
88
Anand
99
Autoscaler
10+
AzureCredentials
11+
Azurite
1012
BackupConfiguration
1113
BackupList
1214
BackupPhase
@@ -153,6 +155,7 @@ ReplicationTLSSecret
153155
ResourceRequirements
154156
RoleBinding
155157
RollingUpdateStatus
158+
SAS
156159
SCC
157160
SCCs
158161
SDK
@@ -523,6 +526,8 @@ stopDelay
523526
stoppedAt
524527
storageClass
525528
storageClassName
529+
storageKey
530+
storageSasToken
526531
storageclass
527532
storageconfiguration
528533
subcommand

api/v1/backup_types.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ type BackupSpec struct {
3636

3737
// BackupStatus defines the observed state of Backup
3838
type BackupStatus struct {
39-
// The credentials to use to upload data to S3
40-
S3Credentials S3Credentials `json:"s3Credentials"`
39+
// The credentials to be used to upload data to S3
40+
S3Credentials *S3Credentials `json:"s3Credentials,omitempty"`
41+
42+
// The credentials to be used to upload data to Azure Blob Storage
43+
AzureCredentials *AzureCredentials `json:"azureCredentials,omitempty"`
4144

4245
// Endpoint to be used to upload data to the cloud,
4346
// overriding the automatic endpoint discovery

api/v1/cluster_types.go

+27-1
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,10 @@ const (
621621
// using Barman against an S3-compatible object storage
622622
type BarmanObjectStoreConfiguration struct {
623623
// The credentials to use to upload data to S3
624-
S3Credentials S3Credentials `json:"s3Credentials"`
624+
S3Credentials *S3Credentials `json:"s3Credentials,omitempty"`
625+
626+
// The credentials to use to upload data in Azure Blob Storage
627+
AzureCredentials *AzureCredentials `json:"azureCredentials,omitempty"`
625628

626629
// Endpoint to be used to upload data to the cloud,
627630
// overriding the automatic endpoint discovery
@@ -718,6 +721,29 @@ type S3Credentials struct {
718721
SecretAccessKeyReference SecretKeySelector `json:"secretAccessKey"`
719722
}
720723

724+
// AzureCredentials is the type for the credentials to be used to upload
725+
// files to Azure Blob Storage. The connection string contains every needed
726+
// information. If the connection string is not specified, we'll need the
727+
// storage account name and also one (and only one) of:
728+
//
729+
// - storageKey
730+
// - storageSasToken
731+
type AzureCredentials struct {
732+
// The connection string to be used
733+
ConnectionString *SecretKeySelector `json:"connectionString,omitempty"`
734+
735+
// The storage account where to upload data
736+
StorageAccount *SecretKeySelector `json:"storageAccount,omitempty"`
737+
738+
// The storage account key to be used in conjunction
739+
// with the storage account name
740+
StorageKey *SecretKeySelector `json:"storageKey,omitempty"`
741+
742+
// A shared-access-signature to be used in conjunction with
743+
// the storage account name
744+
StorageSasToken *SecretKeySelector `json:"storageSasToken,omitempty"`
745+
}
746+
721747
// MonitoringConfiguration is the type containing all the monitoring
722748
// configuration for a certain cluster
723749
type MonitoringConfiguration struct {

api/v1/cluster_webhook.go

+65
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ func (r *Cluster) Validate() (allErrs field.ErrorList) {
132132
allErrs = append(allErrs, r.validateTolerations()...)
133133
allErrs = append(allErrs, r.validateAntiAffinity()...)
134134
allErrs = append(allErrs, r.validateReplicaMode()...)
135+
allErrs = append(allErrs, r.validateBackupConfiguration()...)
135136

136137
return allErrs
137138
}
@@ -865,3 +866,67 @@ func (r *Cluster) validateAntiAffinity() field.ErrorList {
865866
}
866867
return allErrors
867868
}
869+
870+
// validateBackupConfiguration validates the backup configuration
871+
func (r *Cluster) validateBackupConfiguration() field.ErrorList {
872+
allErrors := field.ErrorList{}
873+
874+
if r.Spec.Backup == nil || r.Spec.Backup.BarmanObjectStore == nil {
875+
return nil
876+
}
877+
878+
credentialsCount := 0
879+
if r.Spec.Backup.BarmanObjectStore.AzureCredentials != nil {
880+
credentialsCount++
881+
allErrors = r.Spec.Backup.BarmanObjectStore.AzureCredentials.validateAzureCredentials(
882+
field.NewPath("spec", "backupConfiguration", "azureCredentials"))
883+
}
884+
if r.Spec.Backup.BarmanObjectStore.S3Credentials != nil {
885+
credentialsCount++
886+
}
887+
888+
if credentialsCount != 1 {
889+
allErrors = append(allErrors, field.Invalid(
890+
field.NewPath("spec", "backupConfiguration"),
891+
r.Spec.Backup.BarmanObjectStore,
892+
"one and only one of azureCredentials and s3Credentials are required",
893+
))
894+
}
895+
896+
return allErrors
897+
}
898+
899+
// validateAzureCredentials checks and validates the azure credentials
900+
func (azure *AzureCredentials) validateAzureCredentials(path *field.Path) field.ErrorList {
901+
allErrors := field.ErrorList{}
902+
903+
secrets := 0
904+
if azure.StorageKey != nil {
905+
secrets++
906+
}
907+
if azure.StorageSasToken != nil {
908+
secrets++
909+
}
910+
911+
if secrets != 1 && azure.ConnectionString == nil {
912+
allErrors = append(
913+
allErrors,
914+
field.Invalid(
915+
path,
916+
azure,
917+
"when connection string is not specified, one and only one of "+
918+
"storage key and storage SAS token is allowed"))
919+
}
920+
921+
if secrets != 0 && azure.ConnectionString != nil {
922+
allErrors = append(
923+
allErrors,
924+
field.Invalid(
925+
path,
926+
azure,
927+
"when connection string is specified, the other parameters "+
928+
"must be empty"))
929+
}
930+
931+
return allErrors
932+
}

api/v1/cluster_webhook_test.go

+109
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
. "github.com/onsi/gomega"
1414
v1 "k8s.io/api/core/v1"
1515
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"k8s.io/apimachinery/pkg/util/validation/field"
1617

1718
"github.com/EnterpriseDB/cloud-native-postgresql/internal/configuration"
1819
)
@@ -62,6 +63,114 @@ var _ = Describe("bootstrap methods validation", func() {
6263
})
6364
})
6465

66+
var _ = Describe("azure credentials", func() {
67+
path := field.NewPath("spec", "backupConfiguration", "azureCredentials")
68+
69+
It("contain only one of storage account key and SAS token", func() {
70+
azureCredentials := AzureCredentials{
71+
StorageAccount: &SecretKeySelector{
72+
LocalObjectReference: LocalObjectReference{
73+
Name: "azure-config",
74+
},
75+
Key: "storageAccount",
76+
},
77+
StorageKey: &SecretKeySelector{
78+
LocalObjectReference: LocalObjectReference{
79+
Name: "azure-config",
80+
},
81+
Key: "storageKey",
82+
},
83+
StorageSasToken: &SecretKeySelector{
84+
LocalObjectReference: LocalObjectReference{
85+
Name: "azure-config",
86+
},
87+
Key: "sasToken",
88+
},
89+
}
90+
Expect(azureCredentials.validateAzureCredentials(path)).ToNot(BeEmpty())
91+
92+
azureCredentials = AzureCredentials{
93+
StorageAccount: &SecretKeySelector{
94+
LocalObjectReference: LocalObjectReference{
95+
Name: "azure-config",
96+
},
97+
Key: "storageAccount",
98+
},
99+
StorageKey: nil,
100+
StorageSasToken: nil,
101+
}
102+
Expect(azureCredentials.validateAzureCredentials(path)).ToNot(BeEmpty())
103+
})
104+
105+
It("is correct when the storage key is used", func() {
106+
azureCredentials := AzureCredentials{
107+
StorageAccount: &SecretKeySelector{
108+
LocalObjectReference: LocalObjectReference{
109+
Name: "azure-config",
110+
},
111+
Key: "storageAccount",
112+
},
113+
StorageKey: &SecretKeySelector{
114+
LocalObjectReference: LocalObjectReference{
115+
Name: "azure-config",
116+
},
117+
Key: "storageKey",
118+
},
119+
StorageSasToken: nil,
120+
}
121+
Expect(azureCredentials.validateAzureCredentials(path)).To(BeEmpty())
122+
})
123+
124+
It("is correct when the sas token is used", func() {
125+
azureCredentials := AzureCredentials{
126+
StorageAccount: &SecretKeySelector{
127+
LocalObjectReference: LocalObjectReference{
128+
Name: "azure-config",
129+
},
130+
Key: "storageAccount",
131+
},
132+
StorageKey: nil,
133+
StorageSasToken: &SecretKeySelector{
134+
LocalObjectReference: LocalObjectReference{
135+
Name: "azure-config",
136+
},
137+
Key: "sasToken",
138+
},
139+
}
140+
Expect(azureCredentials.validateAzureCredentials(path)).To(BeEmpty())
141+
})
142+
143+
It("is correct even if only the connection string is specified", func() {
144+
azureCredentials := AzureCredentials{
145+
ConnectionString: &SecretKeySelector{
146+
LocalObjectReference: LocalObjectReference{
147+
Name: "azure-config",
148+
},
149+
Key: "connectionString",
150+
},
151+
}
152+
Expect(azureCredentials.validateAzureCredentials(path)).To(BeEmpty())
153+
})
154+
155+
It("it is not correct when the connection string is specified with other parameters", func() {
156+
azureCredentials := AzureCredentials{
157+
ConnectionString: &SecretKeySelector{
158+
LocalObjectReference: LocalObjectReference{
159+
Name: "azure-config",
160+
},
161+
Key: "connectionString",
162+
},
163+
StorageAccount: &SecretKeySelector{
164+
LocalObjectReference: LocalObjectReference{
165+
Name: "azure-config",
166+
},
167+
Key: "storageAccount",
168+
},
169+
}
170+
Expect(azureCredentials.validateAzureCredentials(path)).To(BeEmpty())
171+
})
172+
})
173+
65174
var _ = Describe("certificates options validation", func() {
66175
It("doesn't complain if there isn't a configuration", func() {
67176
emptyCluster := &Cluster{}

api/v1/zz_generated.deepcopy.go

+55-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)