Compare commits

...

4 Commits

Author SHA1 Message Date
Rasmus Kock Thygesen
5de7885218
Merge 2a12a52211 into e30154b938 2026-04-23 09:17:35 +02:00
Marco Nenciarini
e30154b938
fix: add lz4 compression support for base backups (#868)
Some checks failed
Deploy Docusaurus to GitHub Pages / build (push) Failing after 2s
Deploy Docusaurus to GitHub Pages / deploy (push) Has been skipped
release-please / release-please (push) Failing after 1s
Upgrade barman-cloud library to v0.5.1 which includes lz4 support for
base backups. Update CRD schema and manifest to expose lz4 as a valid
compression option, and clarify docs that lz4 is no longer WAL-only.

Closes #867

Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
Signed-off-by: Leonardo Cecchi <leonardo.cecchi@gmail.com>
Co-authored-by: Leonardo Cecchi <leonardo.cecchi@gmail.com>
2026-04-21 19:23:45 +02:00
Rasmus Kock Thygesen
2a12a52211
Merge branch 'main' into rkth/azure-workload-identity 2026-04-14 15:29:07 +02:00
rkthtrifork
2a70f54ef8
Add Azure workload identity support
Signed-off-by: rkthtrifork <rkth@trifork.com>
2026-03-17 09:29:04 +01:00
9 changed files with 256 additions and 20 deletions

View File

@ -15,6 +15,7 @@ DigitalOcean
Docusaurus
EDB
EKS
EnterpriseDB
Enum
EnvVar
GCP
@ -76,6 +77,7 @@ backends
barmanObjectName
barmanObjectStore
barmancloud
benchmarked
boto
bzip
cd

View File

@ -144,10 +144,11 @@ spec:
description: |-
Compress a backup file (a tar file per tablespace) while streaming it
to the object store. Available options are empty string (no
compression, default), `gzip`, `bzip2`, and `snappy`.
compression, default), `gzip`, `bzip2`, `lz4`, and `snappy`.
enum:
- bzip2
- gzip
- lz4
- snappy
type: string
encryption:

2
go.mod
View File

@ -7,7 +7,7 @@ toolchain go1.26.2
require (
github.com/cert-manager/cert-manager v1.20.2
github.com/cloudnative-pg/api v1.29.0
github.com/cloudnative-pg/barman-cloud v0.5.0
github.com/cloudnative-pg/barman-cloud v0.5.1
github.com/cloudnative-pg/cloudnative-pg v1.29.0
github.com/cloudnative-pg/cnpg-i v0.5.0
github.com/cloudnative-pg/cnpg-i-machinery v0.4.2

4
go.sum
View File

@ -20,8 +20,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudnative-pg/api v1.29.0 h1:mNx6yJ5qi+Xrjs0NYrUy6V4MlXBkVJxGKwvTJZIuTX4=
github.com/cloudnative-pg/api v1.29.0/go.mod h1:bF3HI8UVVcllZ7M8CfBufnb+Us8FyQArhD+4qtX0qhM=
github.com/cloudnative-pg/barman-cloud v0.5.0 h1:DykSaX4o7ee2vyu5FQoG1RJsntHd+EIttIKZbJPlB1Q=
github.com/cloudnative-pg/barman-cloud v0.5.0/go.mod h1:SO2HzLa+GWlSIpGyxnISoJAFPIcaa/qDa33Bb3jefac=
github.com/cloudnative-pg/barman-cloud v0.5.1 h1:vjkXrrxo2DQXHT9u9usqhtaHiPZ/lTfDVs/pIWYTepQ=
github.com/cloudnative-pg/barman-cloud v0.5.1/go.mod h1:XPc5IUFP1y4cZX1sg+Pd8j9V4tmUEVnv3BGCpfQOOg8=
github.com/cloudnative-pg/cloudnative-pg v1.29.0 h1:49Dm8+y4va7RODspJjeaK8uMWP3OGAD0gMsxhjm16Mo=
github.com/cloudnative-pg/cloudnative-pg v1.29.0/go.mod h1:0Sgb/50VyaCnQm3IbWqgnhQG8Kb6mgqo8Jo1J+KtkSI=
github.com/cloudnative-pg/cnpg-i v0.5.0 h1:/TOzpNT6cwNgrpftTtrnLKdoHgMwd+88vZgXjlVgXeE=

View File

@ -145,18 +145,25 @@ func (impl LifecycleImplementation) reconcileJob(
return nil, err
}
useAzureWorkloadIdentity, err := impl.collectAzureWorkloadIdentityUsage(ctx, pluginConfiguration)
if err != nil {
return nil, err
}
return reconcileJob(ctx, cluster, request, sidecarConfiguration{
env: env,
certificates: certificates,
resources: resources,
env: env,
certificates: certificates,
resources: resources,
useAzureWorkloadIdentity: useAzureWorkloadIdentity,
})
}
type sidecarConfiguration struct {
env []corev1.EnvVar
certificates []corev1.VolumeProjection
resources corev1.ResourceRequirements
additionalArgs []string
env []corev1.EnvVar
certificates []corev1.VolumeProjection
resources corev1.ResourceRequirements
additionalArgs []string
useAzureWorkloadIdentity bool
}
func reconcileJob(
@ -243,11 +250,17 @@ func (impl LifecycleImplementation) reconcilePod(
return nil, err
}
useAzureWorkloadIdentity, err := impl.collectAzureWorkloadIdentityUsage(ctx, pluginConfiguration)
if err != nil {
return nil, err
}
return reconcileInstancePod(ctx, cluster, request, pluginConfiguration, sidecarConfiguration{
env: env,
certificates: certificates,
resources: resources,
additionalArgs: additionalArgs,
env: env,
certificates: certificates,
resources: resources,
additionalArgs: additionalArgs,
useAzureWorkloadIdentity: useAzureWorkloadIdentity,
})
}
@ -301,6 +314,25 @@ func (impl LifecycleImplementation) collectAdditionalInstanceArgs(
return nil, nil
}
func (impl LifecycleImplementation) collectAzureWorkloadIdentityUsage(
ctx context.Context,
pluginConfiguration *config.PluginConfiguration,
) (bool, error) {
for _, objectKey := range pluginConfiguration.GetReferredBarmanObjectsKey() {
var objectStore barmancloudv1.ObjectStore
if err := impl.Client.Get(ctx, objectKey, &objectStore); err != nil {
return false, fmt.Errorf("while getting object store %s: %w", objectKey.String(), err)
}
if objectStore.Spec.Configuration.Azure != nil &&
objectStore.Spec.Configuration.Azure.UseDefaultAzureCredentials {
return true, nil
}
}
return false, nil
}
func reconcileInstancePod(
ctx context.Context,
cluster *cnpgv1.Cluster,
@ -379,6 +411,13 @@ func reconcilePodSpec(
},
)
if config.useAzureWorkloadIdentity {
envs = append(envs, corev1.EnvVar{
Name: "AZURE_FEDERATED_TOKEN_FILE",
Value: azureFederatedTokenFilePath,
})
}
envs = append(envs, config.env...)
baseProbe := &corev1.Probe{
@ -466,6 +505,34 @@ func reconcilePodSpec(
spec.Volumes = removeVolume(spec.Volumes, barmanCertificatesVolumeName)
}
if config.useAzureWorkloadIdentity {
sidecarTemplate.VolumeMounts = ensureVolumeMount(
sidecarTemplate.VolumeMounts,
corev1.VolumeMount{
Name: azureFederatedTokenVolumeName,
MountPath: azureFederatedTokenMountPath,
ReadOnly: true,
},
)
spec.Volumes = ensureVolume(spec.Volumes, corev1.Volume{
Name: azureFederatedTokenVolumeName,
VolumeSource: corev1.VolumeSource{
Projected: &corev1.ProjectedVolumeSource{
Sources: []corev1.VolumeProjection{
{
ServiceAccountToken: &corev1.ServiceAccountTokenProjection{
Path: azureFederatedTokenFileName,
Audience: azureFederatedTokenAudience,
ExpirationSeconds: ptr.To[int64](azureFederatedTokenExpirationSeconds),
},
},
},
},
},
})
}
if err := injectPluginSidecarPodSpec(spec, &sidecarTemplate, mainContainerName); err != nil {
return err
}
@ -473,6 +540,15 @@ func reconcilePodSpec(
return nil
}
const (
azureFederatedTokenVolumeName = "azure-identity-token"
azureFederatedTokenMountPath = "/var/run/secrets/azure/tokens"
azureFederatedTokenFileName = "azure-identity-token"
azureFederatedTokenFilePath = azureFederatedTokenMountPath + "/" + azureFederatedTokenFileName
azureFederatedTokenAudience = "api://AzureADTokenExchange"
azureFederatedTokenExpirationSeconds = 3600
)
// TODO: move to machinery once the logic is finalized
// InjectPluginVolumePodSpec injects the plugin volume into a CNPG Pod spec.

View File

@ -22,6 +22,7 @@ package operator
import (
"encoding/json"
barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api"
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
"github.com/cloudnative-pg/cloudnative-pg/pkg/utils"
"github.com/cloudnative-pg/cnpg-i/pkg/lifecycle"
@ -387,6 +388,134 @@ var _ = Describe("LifecycleImplementation", func() {
Expect(args).To(Equal([]string{"--log-level=info"}))
})
})
Describe("collectAzureWorkloadIdentityUsage", func() {
It("returns true when any referred object store uses default Azure credentials", func(ctx SpecContext) {
ns := "test-ns"
cluster := &cnpgv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "c", Namespace: ns}}
pc := &config.PluginConfiguration{
Cluster: cluster,
BarmanObjectName: "primary-store",
RecoveryBarmanObjectName: "recovery-store",
}
primaryStore := &barmancloudv1.ObjectStore{
ObjectMeta: metav1.ObjectMeta{Name: pc.BarmanObjectName, Namespace: ns},
Spec: barmancloudv1.ObjectStoreSpec{
Configuration: barmanapi.BarmanObjectStoreConfiguration{
BarmanCredentials: barmanapi.BarmanCredentials{
Azure: &barmanapi.AzureCredentials{UseDefaultAzureCredentials: true},
},
},
},
}
recoveryStore := &barmancloudv1.ObjectStore{
ObjectMeta: metav1.ObjectMeta{Name: pc.RecoveryBarmanObjectName, Namespace: ns},
}
cli := buildClientFunc(primaryStore, recoveryStore).Build()
impl := LifecycleImplementation{Client: cli}
useWorkloadIdentity, err := impl.collectAzureWorkloadIdentityUsage(ctx, pc)
Expect(err).NotTo(HaveOccurred())
Expect(useWorkloadIdentity).To(BeTrue())
})
It("returns false when none of the referred object stores use default Azure credentials", func(ctx SpecContext) {
ns := "test-ns"
cluster := &cnpgv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "c", Namespace: ns}}
pc := &config.PluginConfiguration{
Cluster: cluster,
BarmanObjectName: "primary-store",
}
primaryStore := &barmancloudv1.ObjectStore{
ObjectMeta: metav1.ObjectMeta{Name: pc.BarmanObjectName, Namespace: ns},
Spec: barmancloudv1.ObjectStoreSpec{
Configuration: barmanapi.BarmanObjectStoreConfiguration{
BarmanCredentials: barmanapi.BarmanCredentials{
Azure: &barmanapi.AzureCredentials{InheritFromAzureAD: true},
},
},
},
}
cli := buildClientFunc(primaryStore).Build()
impl := LifecycleImplementation{Client: cli}
useWorkloadIdentity, err := impl.collectAzureWorkloadIdentityUsage(ctx, pc)
Expect(err).NotTo(HaveOccurred())
Expect(useWorkloadIdentity).To(BeFalse())
})
})
})
var _ = Describe("reconcilePodSpec", func() {
It("injects the Azure federated token volume and env when workload identity is enabled", func() {
cluster := &cnpgv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "cluster-1", Namespace: "ns-1"}}
spec := &corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "postgres",
Env: []corev1.EnvVar{
{Name: "AZURE_CLIENT_ID", Value: "client-id"},
},
},
},
}
err := reconcilePodSpec(
cluster,
spec,
"postgres",
corev1.Container{Args: []string{"instance"}},
sidecarConfiguration{useAzureWorkloadIdentity: true},
)
Expect(err).NotTo(HaveOccurred())
Expect(spec.Volumes).To(ContainElement(HaveField("Name", azureFederatedTokenVolumeName)))
Expect(spec.InitContainers).To(HaveLen(1))
Expect(spec.InitContainers[0].Env).To(ContainElement(corev1.EnvVar{
Name: "AZURE_FEDERATED_TOKEN_FILE",
Value: azureFederatedTokenFilePath,
}))
Expect(spec.InitContainers[0].Env).To(ContainElement(corev1.EnvVar{
Name: "AZURE_CLIENT_ID",
Value: "client-id",
}))
Expect(spec.InitContainers[0].VolumeMounts).To(ContainElement(corev1.VolumeMount{
Name: azureFederatedTokenVolumeName,
MountPath: azureFederatedTokenMountPath,
ReadOnly: true,
}))
})
It("does not override an existing federated token file env", func() {
cluster := &cnpgv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "cluster-1", Namespace: "ns-1"}}
spec := &corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "postgres",
Env: []corev1.EnvVar{
{Name: "AZURE_FEDERATED_TOKEN_FILE", Value: "/custom/token"},
},
},
},
}
err := reconcilePodSpec(
cluster,
spec,
"postgres",
corev1.Container{Args: []string{"instance"}},
sidecarConfiguration{useAzureWorkloadIdentity: true},
)
Expect(err).NotTo(HaveOccurred())
Expect(spec.InitContainers).To(HaveLen(1))
Expect(spec.InitContainers[0].Env).To(ContainElement(corev1.EnvVar{
Name: "AZURE_FEDERATED_TOKEN_FILE",
Value: "/custom/token",
}))
Expect(spec.InitContainers[0].Env).NotTo(ContainElement(corev1.EnvVar{
Name: "AZURE_FEDERATED_TOKEN_FILE",
Value: azureFederatedTokenFilePath,
}))
})
})
var _ = Describe("Volume utilities", func() {

View File

@ -143,10 +143,11 @@ spec:
description: |-
Compress a backup file (a tar file per tablespace) while streaming it
to the object store. Available options are empty string (no
compression, default), `gzip`, `bzip2`, and `snappy`.
compression, default), `gzip`, `bzip2`, `lz4`, and `snappy`.
enum:
- bzip2
- gzip
- lz4
- snappy
type: string
encryption:

View File

@ -15,7 +15,7 @@ for space, speed, or a balance of both.
- `bzip2`
- `gzip`
- `lz4` (WAL only)
- `lz4`
- `snappy`
- `xz` (WAL only)
- `zstd` (WAL only)
@ -41,3 +41,5 @@ network throughput.
| bzip2 | 25,404 | 13,886 | 395 | 67 | 5.9:1 |
| gzip | 116,281 | 3,077 | 395 | 91 | 4.3:1 |
| snappy | 8,134 | 8,341 | 395 | 166 | 2.4:1 |
Numbers come from a 2021 Barman proof of concept ([EnterpriseDB/barman#344](https://github.com/EnterpriseDB/barman/issues/344#issuecomment-992547396)), which predates `lz4` support for base backups. `lz4` is not yet benchmarked.

View File

@ -272,9 +272,10 @@ flow, which uses [`DefaultAzureCredential`](https://learn.microsoft.com/en-us/py
to automatically discover and use available credentials in the following order:
1. **Environment Variables**`AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, and `AZURE_TENANT_ID` for Service Principal authentication
2. **Managed Identity** — Uses the managed identity assigned to the pod
3. **Azure CLI** — Uses credentials from the Azure CLI if available
4. **Azure PowerShell** — Uses credentials from Azure PowerShell if available
2. **Workload Identity** — Uses `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, and a federated service account token
3. **Managed Identity** — Uses the managed identity assigned to the pod
4. **Azure CLI** — Uses credentials from the Azure CLI if available
5. **Azure PowerShell** — Uses credentials from Azure PowerShell if available
This approach is particularly useful for getting started with development and testing; it allows
the SDK to attempt multiple authentication mechanisms seamlessly across different environments.
@ -295,6 +296,30 @@ spec:
[...]
```
When `useDefaultAzureCredentials: true` is set, the plugin sidecar projects a
service account token with the Azure workload identity audience and exposes it
as `AZURE_FEDERATED_TOKEN_FILE`. If your platform does not already inject
`AZURE_CLIENT_ID` and `AZURE_TENANT_ID`, you can provide them through
`.spec.instanceSidecarConfiguration.env`:
```yaml
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
name: azure-store
spec:
configuration:
destinationPath: "<destination path here>"
azureCredentials:
useDefaultAzureCredentials: true
instanceSidecarConfiguration:
env:
- name: AZURE_CLIENT_ID
value: "<managed-identity-client-id>"
- name: AZURE_TENANT_ID
value: "<tenant-id>"
```
### Access Key, SAS Token, or Connection String
Store credentials in a Kubernetes secret: