Compare commits

...

9 Commits

Author SHA1 Message Date
Armando Ruocco
d297c0fbea
Merge 6a55a361a3 into b3bcf6d9c1 2026-01-12 08:08:40 +01:00
renovate[bot]
b3bcf6d9c1
fix(deps): update k8s.io/utils digest to 914a6e7 (#715)
Some checks failed
release-please / release-please (push) Failing after 2s
Barman Base Image / build (push) Failing after 2s
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-10 16:08:17 +01:00
renovate[bot]
757ca11304
chore(deps): update dependency dagger/dagger to v0.19.9 (#718)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-10 16:03:47 +01:00
renovate[bot]
31acf7ce0f
chore(deps): update dependency barman to v3.17.0 (#720)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-10 16:01:49 +01:00
Marco Nenciarini
95a26f5236
docs(authentication): add disclaimer about authentication methods testing (#716)
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 2s
This PR adds a disclaimer to the object stores documentation clarifying
that the Barman Cloud Plugin does not independently test all
authentication methods supported by barman-cloud. The plugin's
responsibility is limited to passing the provided credentials to
barman-cloud, which then handles authentication according to its own
implementation.

This documentation change was decided by the maintainers as part of the
discussion around Azure Default Credentials support (#662).

Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
2026-01-09 14:29:06 +01:00
Armando Ruocco
2c134eafe4
feat: add support for DefaultAzureCredential authentication mechanism (#681)
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
This commit adds support for the DefaultAzureCredential authentication
mechanism in Azure Blob Storage. Users can now use the
`useDefaultAzureCredentials` option to enable Azure's default credential
chain, which automatically discovers and uses available credentials in
the following order

1. Environment Variables (Service Principal)
2. Managed Identity
3. Azure CLI
4. Azure PowerShell

This is particularly useful when running on Azure Kubernetes Service
(AKS) with Workload Identity, eliminating the need to explicitly store
credentials in Kubernetes Secrets.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
Signed-off-by: Gabriele Fedi <gabriele.fedi@enterprisedb.com>
Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
Co-authored-by: Gabriele Fedi <gabriele.fedi@enterprisedb.com>
Co-authored-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
2026-01-08 14:58:27 +01:00
Marco Nenciarini
0153abba82
fix(azure): update barman-cloud with Azure validation fix (#710)
Some checks failed
release-please / release-please (push) Failing after 2s
This change fixes Azure credentials validation to require a storage
account when using explicit credentials such as a storage key or SAS
token, and ensures the connection string is mutually exclusive with
other authentication parameters.

Closes #705

Related: cloudnative-pg/barman-cloud#177

Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
2026-01-08 09:25:15 +01:00
Marco Nenciarini
6a55a361a3 test: replace sleep-based test with deterministic channel verification
The cleanup routine test used time.Sleep() without actually verifying
the goroutine stopped. Added a done channel to provide deterministic
verification of goroutine termination.

Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
2025-12-23 17:06:00 +01:00
Armando Ruocco
62b579101f fix: prevent memory leak by periodically cleaning up expired cache entries
Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
2025-12-23 17:06:00 +01:00
19 changed files with 571 additions and 36 deletions

View File

@ -27,7 +27,7 @@ jobs:
- name: Install Dagger
env:
# renovate: datasource=github-tags depName=dagger/dagger versioning=semver
DAGGER_VERSION: 0.19.8
DAGGER_VERSION: 0.19.9
run: |
curl -L https://dl.dagger.io/dagger/install.sh | BIN_DIR=$HOME/.local/bin sh
- name: Publish a barman-base

View File

@ -44,7 +44,7 @@ jobs:
- name: Install Dagger
env:
# renovate: datasource=github-tags depName=dagger/dagger versioning=semver
DAGGER_VERSION: 0.19.8
DAGGER_VERSION: 0.19.9
run: |
curl -L https://dl.dagger.io/dagger/install.sh | BIN_DIR=$HOME/.local/bin sh
- name: Run CI task

View File

@ -31,7 +31,7 @@ jobs:
- name: Install Dagger
env:
# renovate: datasource=github-tags depName=dagger/dagger versioning=semver
DAGGER_VERSION: 0.19.8
DAGGER_VERSION: 0.19.9
run: |
curl -L https://dl.dagger.io/dagger/install.sh | BIN_DIR=$HOME/.local/bin sh
- name: Create image and manifest

View File

@ -21,7 +21,7 @@ jobs:
- name: Install Dagger
env:
# renovate: datasource=github-tags depName=dagger/dagger versioning=semver
DAGGER_VERSION: 0.19.8
DAGGER_VERSION: 0.19.9
run: |
curl -L https://dl.dagger.io/dagger/install.sh | BIN_DIR=$HOME/.local/bin sh
- name: Create image and manifest

View File

@ -1,3 +1,4 @@
AKS
AccessDenied
AdditionalContainerArgs
Akamai
@ -5,6 +6,7 @@ Azurite
BarmanObjectStore
BarmanObjectStoreConfiguration
BarmanObjectStores
CLI
CNCF
CRD
CloudNativePG
@ -38,6 +40,7 @@ PITR
PoR
PostgreSQL
Postgres
PowerShell
README
RPO
RTO
@ -45,6 +48,7 @@ RecoveryWindow
ResourceRequirements
RetentionPolicy
SAS
SDK
SFO
SPDX
SPDX

View File

@ -202,7 +202,7 @@ tasks:
- start-build-network
vars:
# renovate: datasource=github-tags depName=dagger/dagger versioning=semver
DAGGER_VERSION: 0.19.8
DAGGER_VERSION: 0.19.9
DAGGER_ENGINE_IMAGE: registry.dagger.io/engine:v{{ .DAGGER_VERSION }}
cmds:
- >

View File

@ -108,6 +108,11 @@ spec:
- key
- name
type: object
useDefaultAzureCredentials:
description: |-
Use the default Azure authentication flow, which includes DefaultAzureCredential.
This allows authentication using environment variables and managed identities.
type: boolean
type: object
data:
description: |-

View File

@ -1,3 +1,3 @@
barman[azure,cloud,google,snappy,zstandard,lz4]==3.16.2
barman[azure,cloud,google,snappy,zstandard,lz4]==3.17.0
setuptools==80.9.0
zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability

View File

@ -18,9 +18,9 @@ azure-storage-blob==12.27.1 \
--hash=sha256:65d1e25a4628b7b6acd20ff7902d8da5b4fde8e46e19c8f6d213a3abc3ece272 \
--hash=sha256:a1596cc4daf5dac9be115fcb5db67245eae894cf40e4248243754261f7b674a6
# via barman
barman==3.16.2 \
--hash=sha256:0549f451a1b928647c75c5a2977526233ad7a976bb83e9a4379c33ce61443515 \
--hash=sha256:ab0c6f4f5cfc0cc12b087335bdd5def2edbca32bc1bf553cc5a9e78cd83df43a
barman==3.17.0 \
--hash=sha256:07b033da14e72f103de44261c31bd0c3169bbb2e4de3481c6bb3510e9870d38e \
--hash=sha256:d6618990a6dbb31af3286d746a278a038534b7e3cc617c2b379ef7ebdeb7ed5a
# via -r sidecar-requirements.in
boto3==1.42.14 \
--hash=sha256:a5d005667b480c844ed3f814a59f199ce249d0f5669532a17d06200c0a93119c \

4
go.mod
View File

@ -7,7 +7,7 @@ toolchain go1.25.5
require (
github.com/cert-manager/cert-manager v1.19.2
github.com/cloudnative-pg/api v1.28.0
github.com/cloudnative-pg/barman-cloud v0.4.0
github.com/cloudnative-pg/barman-cloud v0.4.1-0.20260108104508-ced266c145f5
github.com/cloudnative-pg/cloudnative-pg v1.28.0
github.com/cloudnative-pg/cnpg-i v0.3.1
github.com/cloudnative-pg/cnpg-i-machinery v0.4.2
@ -22,7 +22,7 @@ require (
k8s.io/apiextensions-apiserver v0.35.0
k8s.io/apimachinery v0.35.0
k8s.io/client-go v0.35.0
k8s.io/utils v0.0.0-20251222233032-718f0e51e6d2
k8s.io/utils v0.0.0-20260108192941-914a6e750570
sigs.k8s.io/controller-runtime v0.22.4
sigs.k8s.io/kustomize/api v0.21.0
sigs.k8s.io/kustomize/kyaml v0.21.0

8
go.sum
View File

@ -18,8 +18,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.28.0 h1:xElzHliO0eKkVQafkfMhDJo0aIRCmB1ItEt+SGh6B58=
github.com/cloudnative-pg/api v1.28.0/go.mod h1:puXJBOsEaJd8JLgvCtxgl2TO/ZANap/z7bPepKRUgrk=
github.com/cloudnative-pg/barman-cloud v0.4.0 h1:V4ajM5yDWq2m+TxmnDtCBGmfMXAxbXr9k7lfR4jM+eE=
github.com/cloudnative-pg/barman-cloud v0.4.0/go.mod h1:AWdyNP2jvMO1c7eOOwT8kT+QGyK5O7lEBZX12LEZ1Ic=
github.com/cloudnative-pg/barman-cloud v0.4.1-0.20260108104508-ced266c145f5 h1:wPB7VTNgTv6t9sl4QYOBakmVTqHnOdKUht7Q3aL+uns=
github.com/cloudnative-pg/barman-cloud v0.4.1-0.20260108104508-ced266c145f5/go.mod h1:qD0NtJOllNQbRB0MaleuHsZjFYaXtXfdg0HbFTbuHn0=
github.com/cloudnative-pg/cloudnative-pg v1.28.0 h1:vkv0a0ewDSfJOPJrsyUr4uczsxheReAWf/k171V0Dm0=
github.com/cloudnative-pg/cloudnative-pg v1.28.0/go.mod h1:209fkRR6m0vXUVQ9Q498eAPQqN2UlXECbXXtpGsZz3I=
github.com/cloudnative-pg/cnpg-i v0.3.1 h1:fKj8NoToWI11HUL2UWYJBpkVzmaTvbs3kDMo7wQF8RU=
@ -326,8 +326,8 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251222233032-718f0e51e6d2 h1:OfgiEo21hGiwx1oJUU5MpEaeOEg6coWndBkZF/lkFuE=
k8s.io/utils v0.0.0-20251222233032-718f0e51e6d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
k8s.io/utils v0.0.0-20260108192941-914a6e750570 h1:JT4W8lsdrGENg9W+YwwdLJxklIuKWdRm+BC+xt33FOY=
k8s.io/utils v0.0.0-20260108192941-914a6e750570/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 h1:qPrZsv1cwQiFeieFlRqT627fVZ+tyfou/+S5S0H5ua0=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=

View File

@ -36,6 +36,9 @@ import (
// DefaultTTLSeconds is the default TTL in seconds of cache entries
const DefaultTTLSeconds = 10
// DefaultCleanupIntervalSeconds is the default interval in seconds for cache cleanup
const DefaultCleanupIntervalSeconds = 30
type cachedEntry struct {
entry client.Object
fetchUnixTime int64
@ -49,18 +52,30 @@ func (e *cachedEntry) isExpired() bool {
// ExtendedClient is an extended client that is capable of caching multiple secrets without relying on informers
type ExtendedClient struct {
client.Client
cachedObjects []cachedEntry
mux *sync.Mutex
cachedObjects []cachedEntry
mux *sync.Mutex
cleanupInterval time.Duration
cleanupDone chan struct{} // Signals when cleanup routine exits
}
// NewExtendedClient returns an extended client capable of caching secrets on the 'Get' operation
// NewExtendedClient returns an extended client capable of caching secrets on the 'Get' operation.
// It starts a background goroutine that periodically cleans up expired cache entries.
// The cleanup routine will stop when the provided context is cancelled.
func NewExtendedClient(
ctx context.Context,
baseClient client.Client,
) client.Client {
return &ExtendedClient{
Client: baseClient,
mux: &sync.Mutex{},
ec := &ExtendedClient{
Client: baseClient,
mux: &sync.Mutex{},
cleanupInterval: DefaultCleanupIntervalSeconds * time.Second,
cleanupDone: make(chan struct{}),
}
// Start the background cleanup routine
go ec.startCleanupRoutine(ctx)
return ec
}
func (e *ExtendedClient) isObjectCached(obj client.Object) bool {
@ -208,3 +223,55 @@ func (e *ExtendedClient) Patch(
return e.Client.Patch(ctx, obj, patch, opts...)
}
// startCleanupRoutine periodically removes expired entries from the cache.
// It runs until the context is cancelled.
func (e *ExtendedClient) startCleanupRoutine(ctx context.Context) {
defer close(e.cleanupDone)
contextLogger := log.FromContext(ctx).WithName("extended_client_cleanup")
ticker := time.NewTicker(e.cleanupInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
contextLogger.Debug("stopping cache cleanup routine")
return
case <-ticker.C:
// Check context before cleanup to avoid unnecessary work during shutdown
if ctx.Err() != nil {
return
}
e.cleanupExpiredEntries(ctx)
}
}
}
// cleanupExpiredEntries removes all expired entries from the cache.
func (e *ExtendedClient) cleanupExpiredEntries(ctx context.Context) {
contextLogger := log.FromContext(ctx).WithName("extended_client_cleanup")
e.mux.Lock()
defer e.mux.Unlock()
initialCount := len(e.cachedObjects)
if initialCount == 0 {
return
}
// Create a new slice with only non-expired entries
validEntries := make([]cachedEntry, 0, initialCount)
for _, entry := range e.cachedObjects {
if !entry.isExpired() {
validEntries = append(validEntries, entry)
}
}
removedCount := initialCount - len(validEntries)
if removedCount > 0 {
e.cachedObjects = validEntries
contextLogger.Debug("cleaned up expired cache entries",
"removedCount", removedCount,
"remainingCount", len(validEntries))
}
}

View File

@ -20,6 +20,7 @@ SPDX-License-Identifier: Apache-2.0
package client
import (
"context"
"time"
corev1 "k8s.io/api/core/v1"
@ -59,6 +60,7 @@ var _ = Describe("ExtendedClient Get", func() {
extendedClient *ExtendedClient
secretInClient *corev1.Secret
objectStore *barmancloudv1.ObjectStore
cancelCtx context.CancelFunc
)
BeforeEach(func() {
@ -79,7 +81,14 @@ var _ = Describe("ExtendedClient Get", func() {
baseClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(secretInClient, objectStore).Build()
extendedClient = NewExtendedClient(baseClient).(*ExtendedClient)
ctx, cancel := context.WithCancel(context.Background())
cancelCtx = cancel
extendedClient = NewExtendedClient(ctx, baseClient).(*ExtendedClient)
})
AfterEach(func() {
// Cancel the context to stop the cleanup routine
cancelCtx()
})
It("returns secret from cache if not expired", func(ctx SpecContext) {
@ -164,3 +173,141 @@ var _ = Describe("ExtendedClient Get", func() {
Expect(objectStore.GetResourceVersion()).To(Equal("from cache"))
})
})
var _ = Describe("ExtendedClient Cache Cleanup", func() {
var (
extendedClient *ExtendedClient
cancelCtx context.CancelFunc
)
BeforeEach(func() {
baseClient := fake.NewClientBuilder().
WithScheme(scheme).
Build()
ctx, cancel := context.WithCancel(context.Background())
cancelCtx = cancel
extendedClient = NewExtendedClient(ctx, baseClient).(*ExtendedClient)
})
AfterEach(func() {
cancelCtx()
})
It("cleans up expired entries", func(ctx SpecContext) {
// Add some expired entries
expiredSecret1 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "expired-secret-1",
},
}
expiredSecret2 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "expired-secret-2",
},
}
validSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "valid-secret",
},
}
// Add expired entries (2 minutes ago)
addToCache(extendedClient, expiredSecret1, time.Now().Add(-2*time.Minute).Unix())
addToCache(extendedClient, expiredSecret2, time.Now().Add(-2*time.Minute).Unix())
// Add valid entry (just now)
addToCache(extendedClient, validSecret, time.Now().Unix())
Expect(extendedClient.cachedObjects).To(HaveLen(3))
// Trigger cleanup
extendedClient.cleanupExpiredEntries(ctx)
// Only the valid entry should remain
Expect(extendedClient.cachedObjects).To(HaveLen(1))
Expect(extendedClient.cachedObjects[0].entry.GetName()).To(Equal("valid-secret"))
})
It("does nothing when all entries are valid", func(ctx SpecContext) {
validSecret1 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "valid-secret-1",
},
}
validSecret2 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "valid-secret-2",
},
}
addToCache(extendedClient, validSecret1, time.Now().Unix())
addToCache(extendedClient, validSecret2, time.Now().Unix())
Expect(extendedClient.cachedObjects).To(HaveLen(2))
// Trigger cleanup
extendedClient.cleanupExpiredEntries(ctx)
// Both entries should remain
Expect(extendedClient.cachedObjects).To(HaveLen(2))
})
It("does nothing when cache is empty", func(ctx SpecContext) {
Expect(extendedClient.cachedObjects).To(BeEmpty())
// Trigger cleanup
extendedClient.cleanupExpiredEntries(ctx)
Expect(extendedClient.cachedObjects).To(BeEmpty())
})
It("removes all entries when all are expired", func(ctx SpecContext) {
expiredSecret1 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "expired-secret-1",
},
}
expiredSecret2 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "expired-secret-2",
},
}
addToCache(extendedClient, expiredSecret1, time.Now().Add(-2*time.Minute).Unix())
addToCache(extendedClient, expiredSecret2, time.Now().Add(-2*time.Minute).Unix())
Expect(extendedClient.cachedObjects).To(HaveLen(2))
// Trigger cleanup
extendedClient.cleanupExpiredEntries(ctx)
Expect(extendedClient.cachedObjects).To(BeEmpty())
})
It("stops cleanup routine when context is cancelled", func() {
// Create a new client with a short cleanup interval for testing
baseClient := fake.NewClientBuilder().
WithScheme(scheme).
Build()
ctx, cancel := context.WithCancel(context.Background())
ec := NewExtendedClient(ctx, baseClient).(*ExtendedClient)
ec.cleanupInterval = 10 * time.Millisecond
// Cancel the context immediately
cancel()
// Verify the cleanup routine actually stops by waiting for the done channel
select {
case <-ec.cleanupDone:
// Success: cleanup routine exited as expected
case <-time.After(1 * time.Second):
Fail("cleanup routine did not stop within timeout")
}
})
})

View File

@ -84,7 +84,7 @@ func Start(ctx context.Context) error {
return err
}
customCacheClient := extendedclient.NewExtendedClient(mgr.GetClient())
customCacheClient := extendedclient.NewExtendedClient(ctx, mgr.GetClient())
if err := mgr.Add(&CNPGI{
Client: customCacheClient,

View File

@ -37,13 +37,17 @@ func CollectSecretNamesFromCredentials(barmanCredentials *barmanapi.BarmanCreden
)
}
if barmanCredentials.Azure != nil {
references = append(
references,
barmanCredentials.Azure.ConnectionString,
barmanCredentials.Azure.StorageAccount,
barmanCredentials.Azure.StorageKey,
barmanCredentials.Azure.StorageSasToken,
)
// When using default Azure credentials or managed identity, no secrets are required
if !barmanCredentials.Azure.UseDefaultAzureCredentials &&
!barmanCredentials.Azure.InheritFromAzureAD {
references = append(
references,
barmanCredentials.Azure.ConnectionString,
barmanCredentials.Azure.StorageAccount,
barmanCredentials.Azure.StorageKey,
barmanCredentials.Azure.StorageSasToken,
)
}
}
if barmanCredentials.Google != nil {
references = append(

View File

@ -0,0 +1,227 @@
/*
Copyright © contributors to CloudNativePG, established as
CloudNativePG a Series of LF Projects, LLC.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
SPDX-License-Identifier: Apache-2.0
*/
package specs
import (
barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api"
machineryapi "github.com/cloudnative-pg/machinery/pkg/api"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("CollectSecretNamesFromCredentials", func() {
Context("when collecting secrets from AWS credentials", func() {
It("should return secret names from S3 credentials", func() {
credentials := &barmanapi.BarmanCredentials{
AWS: &barmanapi.S3Credentials{
AccessKeyIDReference: &machineryapi.SecretKeySelector{
LocalObjectReference: machineryapi.LocalObjectReference{
Name: "aws-secret",
},
Key: "access-key-id",
},
SecretAccessKeyReference: &machineryapi.SecretKeySelector{
LocalObjectReference: machineryapi.LocalObjectReference{
Name: "aws-secret",
},
Key: "secret-access-key",
},
},
}
secrets := CollectSecretNamesFromCredentials(credentials)
Expect(secrets).To(ContainElement("aws-secret"))
})
It("should handle nil AWS credentials", func() {
credentials := &barmanapi.BarmanCredentials{}
secrets := CollectSecretNamesFromCredentials(credentials)
Expect(secrets).To(BeEmpty())
})
})
Context("when collecting secrets from Azure credentials", func() {
It("should return secret names when using explicit credentials", func() {
credentials := &barmanapi.BarmanCredentials{
Azure: &barmanapi.AzureCredentials{
ConnectionString: &machineryapi.SecretKeySelector{
LocalObjectReference: machineryapi.LocalObjectReference{
Name: "azure-secret",
},
Key: "connection-string",
},
},
}
secrets := CollectSecretNamesFromCredentials(credentials)
Expect(secrets).To(ContainElement("azure-secret"))
})
It("should return empty list when using UseDefaultAzureCredentials", func() {
credentials := &barmanapi.BarmanCredentials{
Azure: &barmanapi.AzureCredentials{
UseDefaultAzureCredentials: true,
ConnectionString: &machineryapi.SecretKeySelector{
LocalObjectReference: machineryapi.LocalObjectReference{
Name: "azure-secret",
},
Key: "connection-string",
},
},
}
secrets := CollectSecretNamesFromCredentials(credentials)
Expect(secrets).To(BeEmpty())
})
It("should return empty list when using InheritFromAzureAD", func() {
credentials := &barmanapi.BarmanCredentials{
Azure: &barmanapi.AzureCredentials{
InheritFromAzureAD: true,
},
}
secrets := CollectSecretNamesFromCredentials(credentials)
Expect(secrets).To(BeEmpty())
})
It("should return secret names for storage account and key", func() {
credentials := &barmanapi.BarmanCredentials{
Azure: &barmanapi.AzureCredentials{
StorageAccount: &machineryapi.SecretKeySelector{
LocalObjectReference: machineryapi.LocalObjectReference{
Name: "azure-storage",
},
Key: "account-name",
},
StorageKey: &machineryapi.SecretKeySelector{
LocalObjectReference: machineryapi.LocalObjectReference{
Name: "azure-storage",
},
Key: "account-key",
},
},
}
secrets := CollectSecretNamesFromCredentials(credentials)
Expect(secrets).To(ContainElement("azure-storage"))
})
})
Context("when collecting secrets from Google credentials", func() {
It("should return secret names from Google credentials", func() {
credentials := &barmanapi.BarmanCredentials{
Google: &barmanapi.GoogleCredentials{
ApplicationCredentials: &machineryapi.SecretKeySelector{
LocalObjectReference: machineryapi.LocalObjectReference{
Name: "google-secret",
},
Key: "credentials.json",
},
},
}
secrets := CollectSecretNamesFromCredentials(credentials)
Expect(secrets).To(ContainElement("google-secret"))
})
})
Context("when collecting secrets from multiple cloud providers", func() {
It("should return secret names from all providers", func() {
credentials := &barmanapi.BarmanCredentials{
AWS: &barmanapi.S3Credentials{
AccessKeyIDReference: &machineryapi.SecretKeySelector{
LocalObjectReference: machineryapi.LocalObjectReference{
Name: "aws-secret",
},
Key: "access-key-id",
},
},
Azure: &barmanapi.AzureCredentials{
ConnectionString: &machineryapi.SecretKeySelector{
LocalObjectReference: machineryapi.LocalObjectReference{
Name: "azure-secret",
},
Key: "connection-string",
},
},
Google: &barmanapi.GoogleCredentials{
ApplicationCredentials: &machineryapi.SecretKeySelector{
LocalObjectReference: machineryapi.LocalObjectReference{
Name: "google-secret",
},
Key: "credentials.json",
},
},
}
secrets := CollectSecretNamesFromCredentials(credentials)
Expect(secrets).To(ContainElements("aws-secret", "azure-secret", "google-secret"))
})
It("should skip Azure secrets when using UseDefaultAzureCredentials with other providers", func() {
credentials := &barmanapi.BarmanCredentials{
AWS: &barmanapi.S3Credentials{
AccessKeyIDReference: &machineryapi.SecretKeySelector{
LocalObjectReference: machineryapi.LocalObjectReference{
Name: "aws-secret",
},
Key: "access-key-id",
},
},
Azure: &barmanapi.AzureCredentials{
UseDefaultAzureCredentials: true,
ConnectionString: &machineryapi.SecretKeySelector{
LocalObjectReference: machineryapi.LocalObjectReference{
Name: "azure-secret",
},
Key: "connection-string",
},
},
}
secrets := CollectSecretNamesFromCredentials(credentials)
Expect(secrets).To(ContainElement("aws-secret"))
Expect(secrets).NotTo(ContainElement("azure-secret"))
})
})
Context("when handling nil references", func() {
It("should skip nil secret references", func() {
credentials := &barmanapi.BarmanCredentials{
AWS: &barmanapi.S3Credentials{
AccessKeyIDReference: &machineryapi.SecretKeySelector{
LocalObjectReference: machineryapi.LocalObjectReference{
Name: "aws-secret",
},
Key: "access-key-id",
},
SecretAccessKeyReference: nil,
},
}
secrets := CollectSecretNamesFromCredentials(credentials)
Expect(secrets).To(ContainElement("aws-secret"))
Expect(len(secrets)).To(Equal(1))
})
})
})

View File

@ -0,0 +1,32 @@
/*
Copyright © contributors to CloudNativePG, established as
CloudNativePG a Series of LF Projects, LLC.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
SPDX-License-Identifier: Apache-2.0
*/
package specs
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestSpecs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Specs Suite")
}

View File

@ -107,6 +107,11 @@ spec:
- key
- name
type: object
useDefaultAzureCredentials:
description: |-
Use the default Azure authentication flow, which includes DefaultAzureCredential.
This allows authentication using environment variables and managed identities.
type: boolean
type: object
data:
description: |-

View File

@ -29,6 +29,16 @@ the specific object storage provider you are using.
The following sections detail the setup for each.
:::note Authentication Methods
The Barman Cloud Plugin does not independently test all authentication methods
supported by `barman-cloud`. The plugin's responsibility is limited to passing
the provided credentials to `barman-cloud`, which then handles authentication
according to its own implementation. Users should refer to the
[Barman Cloud documentation](https://docs.pgbarman.org/release/latest/) to
verify that their chosen authentication method is supported and properly
configured.
:::
---
## AWS S3
@ -230,14 +240,18 @@ is Microsofts cloud-based object storage solution.
Barman Cloud supports the following authentication methods:
- [Connection String](https://learn.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string)
- Storage Account Name + [Access Key](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-keys-manage)
- Storage Account Name + [SAS Token](https://learn.microsoft.com/en-us/azure/storage/blobs/sas-service-create)
- [Azure AD Workload Identity](https://azure.github.io/azure-workload-identity/docs/introduction.html)
- Storage Account Name + [Storage Account Access Key](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-keys-manage)
- Storage Account Name + [Storage Account SAS Token](https://learn.microsoft.com/en-us/azure/storage/blobs/sas-service-create)
- [Azure AD Managed Identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview)
- [Default Azure Credentials](https://learn.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet)
### Azure AD Workload Identity
### Azure AD Managed Identity
This method avoids storing credentials in Kubernetes via the
`.spec.configuration.inheritFromAzureAD` option:
This method avoids storing credentials in Kubernetes by enabling the
usage of [Azure Managed Identities](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) authentication mechanism.
This can be enabled by setting the `inheritFromAzureAD` option to `true`.
Managed Identity can be configured for the AKS Cluster by following
the [Azure documentation](https://learn.microsoft.com/en-us/azure/aks/use-managed-identity?pivots=system-assigned).
```yaml
apiVersion: barmancloud.cnpg.io/v1
@ -252,6 +266,36 @@ spec:
[...]
```
### Default Azure Credentials
The `useDefaultAzureCredentials` option enables the default Azure credentials
flow, which uses [`DefaultAzureCredential`](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.defaultazurecredential)
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
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.
However, this is not recommended for production. Please refer to the
[official Azure guidance](https://learn.microsoft.com/en-us/dotnet/azure/sdk/authentication/credential-chains?tabs=dac#usage-guidance-for-defaultazurecredential)
for a comprehensive understanding of `DefaultAzureCredential`.
```yaml
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
name: azure-store
spec:
configuration:
destinationPath: "<destination path here>"
azureCredentials:
useDefaultAzureCredentials: true
[...]
```
### Access Key, SAS Token, or Connection String
Store credentials in a Kubernetes secret: