mirror of
https://github.com/cloudnative-pg/plugin-barman-cloud.git
synced 2026-07-09 11:12:21 +02:00
Compare commits
5 Commits
f251b5cf68
...
1ae9d01f49
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ae9d01f49 | ||
|
|
0111ed9f4d | ||
|
|
742a0c0810 | ||
|
|
2a12a52211 | ||
|
|
2a70f54ef8 |
@ -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.
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -43,9 +43,10 @@
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"webpackbar": "^7.0.0"
|
||||
"webpackbar": "^7.0.0",
|
||||
"serialize-javascript": ">=7.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@ -3389,12 +3389,12 @@ array-union@^2.1.0:
|
||||
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
|
||||
|
||||
asn1js@^3.0.6:
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.7.tgz#15f1f2f59e60f80d5b43ef14047a294a969f824f"
|
||||
integrity sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==
|
||||
version "3.0.10"
|
||||
resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.10.tgz#df26c874c8a8b41ca605efea47b2ad07551013dd"
|
||||
integrity sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==
|
||||
dependencies:
|
||||
pvtsutils "^1.3.6"
|
||||
pvutils "^1.1.3"
|
||||
pvutils "^1.1.5"
|
||||
tslib "^2.8.1"
|
||||
|
||||
astring@^1.8.0:
|
||||
@ -5849,9 +5849,9 @@ json5@^2.1.2, json5@^2.2.3:
|
||||
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
|
||||
|
||||
jsonfile@^6.0.1:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.0.tgz#7c265bd1b65de6977478300087c99f1c84383f62"
|
||||
integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.1.tgz#b6e31717f22cc37330b081ce0051ed5de53af2f6"
|
||||
integrity sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==
|
||||
dependencies:
|
||||
universalify "^2.0.0"
|
||||
optionalDependencies:
|
||||
@ -7832,7 +7832,7 @@ pvtsutils@^1.3.6:
|
||||
dependencies:
|
||||
tslib "^2.8.1"
|
||||
|
||||
pvutils@^1.1.3:
|
||||
pvutils@^1.1.3, pvutils@^1.1.5:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.5.tgz#84b0dea4a5d670249aa9800511804ee0b7c2809c"
|
||||
integrity sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==
|
||||
@ -7854,13 +7854,6 @@ quick-lru@^5.1.1:
|
||||
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
|
||||
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
|
||||
|
||||
randombytes@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
||||
integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
|
||||
dependencies:
|
||||
safe-buffer "^5.1.0"
|
||||
|
||||
range-parser@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
|
||||
@ -8296,7 +8289,7 @@ run-parallel@^1.1.9:
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
|
||||
safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@~5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
@ -8402,12 +8395,10 @@ send@~0.19.0, send@~0.19.1:
|
||||
range-parser "~1.2.1"
|
||||
statuses "~2.0.2"
|
||||
|
||||
serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
|
||||
integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
|
||||
dependencies:
|
||||
randombytes "^2.1.0"
|
||||
serialize-javascript@>=7.0.5, serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-7.0.5.tgz#c798cc0552ffbb08981914a42a8756e339d0d5b1"
|
||||
integrity sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==
|
||||
|
||||
serve-handler@^6.1.7:
|
||||
version "6.1.7"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user