Compare commits

...

3 Commits

Author SHA1 Message Date
Rasmus Kock Thygesen
f2c4f235bf
Merge 2a70f54ef8 into 4a94cb9dae 2026-04-01 19:27:57 +02:00
Kenny Root
4a94cb9dae
fix(metrics): announce sidecar injection capability (#776)
Some checks failed
release-please / release-please (push) Failing after 5s
The operator was not announcing the TYPE_INSTANCE_SIDECAR_INJECTION
capability so the CNPG operator did not consider the plugin enabled
for instance pods. The instance manager never queried the plugin's
metrics endpoint, and the barman_cloud_cloudnative_pg_io_* metrics
were missing entirely.

This bug was masked when isWALArchiver was set to true in the plugin
configuration, because the backward compatibility code in CNPG would
mark the plugin as enabled as a side-effect. Users with isWALArchiver
set to false (or omitted) never saw the new metrics.

Closes #682 

Signed-off-by: Kenny Root <kenny@the-b.org>
2026-03-31 15:36:09 +02:00
rkthtrifork
2a70f54ef8
Add Azure workload identity support
Signed-off-by: rkthtrifork <rkth@trifork.com>
2026-03-17 09:29:04 +01:00
4 changed files with 251 additions and 14 deletions

View File

@ -62,6 +62,13 @@ func (i IdentityImplementation) GetPluginCapabilities(
},
},
},
{
Type: &identity.PluginCapability_Service_{
Service: &identity.PluginCapability_Service{
Type: identity.PluginCapability_Service_TYPE_INSTANCE_SIDECAR_INJECTION,
},
},
},
},
}, nil
}

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

@ -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: