Files
velero/pkg/cmd/util/cacert/bsl_cacert_test.go
Tiger Kaovilai 61bf2ef777 feat: Enhance BackupStorageLocation with Secret-based CA certificate support
- Introduced `CACertRef` field in `ObjectStorageLocation` to reference a Secret containing the CA certificate, replacing the deprecated `CACert` field.
- Implemented validation logic to ensure mutual exclusivity between `CACert` and `CACertRef`.
- Updated BSL controller and repository provider to handle the new certificate resolution logic.
- Enhanced CLI to support automatic certificate discovery from BSL configurations.
- Added unit and integration tests to validate new functionality and ensure backward compatibility.
- Documented migration strategy for users transitioning from inline certificates to Secret-based management.

Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com>
2025-12-12 21:07:37 +07:00

647 lines
17 KiB
Go

/*
Copyright the Velero contributors.
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.
*/
package cacert
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/builder"
"github.com/vmware-tanzu/velero/pkg/util"
)
func TestGetCACertFromBackup(t *testing.T) {
testCases := []struct {
name string
backup *velerov1api.Backup
bsl *velerov1api.BackupStorageLocation
expectedCACert string
expectedError bool
}{
{
name: "backup with BSL containing cacert",
backup: builder.ForBackup("test-ns", "test-backup").
StorageLocation("test-bsl").
Result(),
bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
Provider("aws").
Bucket("test-bucket").
CACert([]byte("test-cacert-content")).
Result(),
expectedCACert: "test-cacert-content",
expectedError: false,
},
{
name: "backup with BSL without cacert",
backup: builder.ForBackup("test-ns", "test-backup").
StorageLocation("test-bsl").
Result(),
bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
Provider("aws").
Bucket("test-bucket").
Result(),
expectedCACert: "",
expectedError: false,
},
{
name: "backup without storage location",
backup: builder.ForBackup("test-ns", "test-backup").
Result(),
bsl: nil,
expectedCACert: "",
expectedError: false,
},
{
name: "BSL not found",
backup: builder.ForBackup("test-ns", "test-backup").
StorageLocation("missing-bsl").
Result(),
bsl: nil,
expectedCACert: "",
expectedError: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var objs []runtime.Object
objs = append(objs, tc.backup)
if tc.bsl != nil {
objs = append(objs, tc.bsl)
}
fakeClient := fake.NewClientBuilder().
WithScheme(util.VeleroScheme).
WithRuntimeObjects(objs...).
Build()
cacert, err := GetCACertFromBackup(t.Context(), fakeClient, "test-ns", tc.backup)
if tc.expectedError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expectedCACert, cacert)
}
})
}
}
func TestGetCACertFromRestore(t *testing.T) {
testCases := []struct {
name string
restore *velerov1api.Restore
backup *velerov1api.Backup
bsl *velerov1api.BackupStorageLocation
expectedCACert string
expectedError bool
}{
{
name: "restore with backup having BSL containing cacert",
restore: builder.ForRestore("test-ns", "test-restore").
Backup("test-backup").
Result(),
backup: builder.ForBackup("test-ns", "test-backup").
StorageLocation("test-bsl").
Result(),
bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
Provider("aws").
Bucket("test-bucket").
CACert([]byte("test-cacert-content")).
Result(),
expectedCACert: "test-cacert-content",
expectedError: false,
},
{
name: "restore with backup not found",
restore: builder.ForRestore("test-ns", "test-restore").
Backup("missing-backup").
Result(),
backup: nil,
bsl: nil,
expectedCACert: "",
expectedError: false,
},
{
name: "restore with backup having BSL without cacert",
restore: builder.ForRestore("test-ns", "test-restore").
Backup("test-backup").
Result(),
backup: builder.ForBackup("test-ns", "test-backup").
StorageLocation("test-bsl").
Result(),
bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
Provider("aws").
Bucket("test-bucket").
Result(),
expectedCACert: "",
expectedError: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var objs []runtime.Object
objs = append(objs, tc.restore)
if tc.backup != nil {
objs = append(objs, tc.backup)
}
if tc.bsl != nil {
objs = append(objs, tc.bsl)
}
fakeClient := fake.NewClientBuilder().
WithScheme(util.VeleroScheme).
WithRuntimeObjects(objs...).
Build()
cacert, err := GetCACertFromRestore(t.Context(), fakeClient, "test-ns", tc.restore)
if tc.expectedError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expectedCACert, cacert)
}
})
}
}
func TestGetCACertFromBSL(t *testing.T) {
testCases := []struct {
name string
bslName string
bsl *velerov1api.BackupStorageLocation
expectedCACert string
expectedError bool
}{
{
name: "BSL with cacert",
bslName: "test-bsl",
bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
Provider("aws").
Bucket("test-bucket").
CACert([]byte("test-cacert-content")).
Result(),
expectedCACert: "test-cacert-content",
expectedError: false,
},
{
name: "BSL without cacert",
bslName: "test-bsl",
bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
Provider("aws").
Bucket("test-bucket").
Result(),
expectedCACert: "",
expectedError: false,
},
{
name: "empty BSL name",
bslName: "",
bsl: nil,
expectedCACert: "",
expectedError: false,
},
{
name: "BSL not found",
bslName: "missing-bsl",
bsl: nil,
expectedCACert: "",
expectedError: false,
},
{
name: "BSL with invalid CA cert format",
bslName: "test-bsl",
bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
Provider("aws").
Bucket("test-bucket").
CACert([]byte("INVALID CERT DATA WITHOUT PEM HEADERS")).
Result(),
expectedCACert: "INVALID CERT DATA WITHOUT PEM HEADERS", // We still return it, validation happens during TLS handshake
expectedError: false,
},
{
name: "BSL with malformed PEM certificate",
bslName: "test-bsl",
bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
Provider("aws").
Bucket("test-bucket").
CACert([]byte("-----BEGIN CERTIFICATE-----\nINVALID BASE64 DATA!!!\n-----END CERTIFICATE-----\n")).
Result(),
expectedCACert: "-----BEGIN CERTIFICATE-----\nINVALID BASE64 DATA!!!\n-----END CERTIFICATE-----\n",
expectedError: false,
},
{
name: "BSL with nil config",
bslName: "test-bsl",
bsl: &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-bsl",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
Config: nil,
},
},
expectedCACert: "",
expectedError: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var objs []runtime.Object
if tc.bsl != nil {
objs = append(objs, tc.bsl)
}
fakeClient := fake.NewClientBuilder().
WithScheme(util.VeleroScheme).
WithRuntimeObjects(objs...).
Build()
cacert, err := GetCACertFromBSL(t.Context(), fakeClient, "test-ns", tc.bslName)
if tc.expectedError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expectedCACert, cacert)
}
})
}
}
// TestGetCACertFromBSL_WithCACertRef tests the new caCertRef functionality
func TestGetCACertFromBSL_WithCACertRef(t *testing.T) {
testCases := []struct {
name string
bslName string
bsl *velerov1api.BackupStorageLocation
secret *corev1api.Secret
expectedCACert string
expectedError bool
errorContains string
}{
{
name: "BSL with caCertRef pointing to valid secret",
bslName: "test-bsl",
bsl: &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-bsl",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
CACertRef: &corev1api.SecretKeySelector{
LocalObjectReference: corev1api.LocalObjectReference{
Name: "test-secret",
},
Key: "ca-bundle.crt",
},
},
},
},
},
secret: &corev1api.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-secret",
},
Data: map[string][]byte{
"ca-bundle.crt": []byte("test-cacert-from-secret"),
},
},
expectedCACert: "test-cacert-from-secret",
expectedError: false,
},
{
name: "BSL with both caCertRef and caCert - caCertRef takes precedence",
bslName: "test-bsl",
bsl: &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-bsl",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
CACert: []byte("inline-cacert-deprecated"),
CACertRef: &corev1api.SecretKeySelector{
LocalObjectReference: corev1api.LocalObjectReference{
Name: "test-secret",
},
Key: "ca-bundle.crt",
},
},
},
},
},
secret: &corev1api.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-secret",
},
Data: map[string][]byte{
"ca-bundle.crt": []byte("cacert-from-secret-takes-precedence"),
},
},
expectedCACert: "cacert-from-secret-takes-precedence",
expectedError: false,
},
{
name: "BSL with caCertRef but secret not found",
bslName: "test-bsl",
bsl: &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-bsl",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
CACertRef: &corev1api.SecretKeySelector{
LocalObjectReference: corev1api.LocalObjectReference{
Name: "missing-secret",
},
Key: "ca-bundle.crt",
},
},
},
},
},
secret: nil,
expectedCACert: "",
expectedError: true,
errorContains: "certificate secret missing-secret not found",
},
{
name: "BSL with caCertRef but key not found in secret",
bslName: "test-bsl",
bsl: &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-bsl",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
CACertRef: &corev1api.SecretKeySelector{
LocalObjectReference: corev1api.LocalObjectReference{
Name: "test-secret",
},
Key: "missing-key",
},
},
},
},
},
secret: &corev1api.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-secret",
},
Data: map[string][]byte{
"ca-bundle.crt": []byte("test-cacert"),
},
},
expectedCACert: "",
expectedError: true,
errorContains: "key missing-key not found in secret test-secret",
},
{
name: "BSL with caCertRef but empty key",
bslName: "test-bsl",
bsl: &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-bsl",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
CACertRef: &corev1api.SecretKeySelector{
LocalObjectReference: corev1api.LocalObjectReference{
Name: "test-secret",
},
Key: "",
},
},
},
},
},
secret: &corev1api.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-secret",
},
Data: map[string][]byte{
"ca-bundle.crt": []byte("test-cacert"),
},
},
expectedCACert: "",
expectedError: true,
errorContains: "caCertRef key is empty",
},
{
name: "BSL with caCertRef containing multi-line PEM certificate",
bslName: "test-bsl",
bsl: &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-bsl",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
CACertRef: &corev1api.SecretKeySelector{
LocalObjectReference: corev1api.LocalObjectReference{
Name: "test-secret",
},
Key: "ca.pem",
},
},
},
},
},
secret: &corev1api.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-secret",
},
Data: map[string][]byte{
"ca.pem": []byte("-----BEGIN CERTIFICATE-----\nMIIDETC...\n-----END CERTIFICATE-----\n"),
},
},
expectedCACert: "-----BEGIN CERTIFICATE-----\nMIIDETC...\n-----END CERTIFICATE-----\n",
expectedError: false,
},
{
name: "BSL falls back to inline caCert when caCertRef is nil",
bslName: "test-bsl",
bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
Provider("aws").
Bucket("test-bucket").
CACert([]byte("fallback-inline-cacert")).
Result(),
secret: nil,
expectedCACert: "fallback-inline-cacert",
expectedError: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var objs []runtime.Object
if tc.bsl != nil {
objs = append(objs, tc.bsl)
}
if tc.secret != nil {
objs = append(objs, tc.secret)
}
scheme := runtime.NewScheme()
_ = velerov1api.AddToScheme(scheme)
_ = corev1api.AddToScheme(scheme)
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithRuntimeObjects(objs...).
Build()
cacert, err := GetCACertFromBSL(t.Context(), fakeClient, "test-ns", tc.bslName)
if tc.expectedError {
require.Error(t, err)
if tc.errorContains != "" {
assert.Contains(t, err.Error(), tc.errorContains)
}
} else {
require.NoError(t, err)
assert.Equal(t, tc.expectedCACert, cacert)
}
})
}
}
// TestGetCACertFromBackup_ClientError tests error scenarios where client.Get returns non-NotFound errors
func TestGetCACertFromBackup_ClientError(t *testing.T) {
testCases := []struct {
name string
backup *velerov1api.Backup
bsl *velerov1api.BackupStorageLocation
expectedError string
}{
{
name: "client error getting BSL",
backup: builder.ForBackup("test-ns", "test-backup").
StorageLocation("test-bsl").
Result(),
bsl: builder.ForBackupStorageLocation("different-ns", "test-bsl"). // Different namespace to trigger error
Provider("aws").
Bucket("test-bucket").
CACert([]byte("test-cacert-content")).
Result(),
expectedError: "not found",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var objs []runtime.Object
objs = append(objs, tc.backup)
if tc.bsl != nil {
objs = append(objs, tc.bsl)
}
fakeClient := fake.NewClientBuilder().
WithScheme(util.VeleroScheme).
WithRuntimeObjects(objs...).
Build()
// Try to get BSL from wrong namespace to simulate error
_, err := GetCACertFromBSL(t.Context(), fakeClient, "wrong-ns", tc.backup.Spec.StorageLocation)
require.NoError(t, err) // Not found errors are handled gracefully
})
}
}
// TestGetCACertFromRestore_ClientError tests error scenarios for GetCACertFromRestore
func TestGetCACertFromRestore_ClientError(t *testing.T) {
testCases := []struct {
name string
restore *velerov1api.Restore
backup *velerov1api.Backup
expectedError string
}{
{
name: "backup in different namespace",
restore: builder.ForRestore("test-ns", "test-restore").
Backup("test-backup").
Result(),
backup: builder.ForBackup("different-ns", "test-backup"). // Different namespace
StorageLocation("test-bsl").
Result(),
expectedError: "not found",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var objs []runtime.Object
objs = append(objs, tc.restore)
if tc.backup != nil {
objs = append(objs, tc.backup)
}
fakeClient := fake.NewClientBuilder().
WithScheme(util.VeleroScheme).
WithRuntimeObjects(objs...).
Build()
// This should not find the backup in the wrong namespace
cacert, err := GetCACertFromRestore(t.Context(), fakeClient, "test-ns", tc.restore)
require.NoError(t, err) // Not found errors are handled gracefully, returning empty string
assert.Empty(t, cacert)
})
}
}