Skip to content

Commit bc29ba5

Browse files
Merge pull request #2241 from kaovilai/cherry-pick-oadp-641-to-1.4
OADP-4817, OADP-1945, OADP-641: Add AWS_CA_BUNDLE support for custom CA certificates in BSLs
2 parents 1dfe2da + db8145c commit bc29ba5

4 files changed

Lines changed: 472 additions & 0 deletions

File tree

controllers/bsl.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,3 +487,69 @@ func (r *DPAReconciler) ensureSecretDataExists(dpa *oadpv1alpha1.DataProtectionA
487487
}
488488
return nil
489489
}
490+
491+
// processCACertForBSLs creates a ConfigMap containing CA certificates from BackupStorageLocations.
492+
// Returns the ConfigMap name if certificates were found, empty string otherwise.
493+
func (r *DPAReconciler) processCACertForBSLs(dpa *oadpv1alpha1.DataProtectionApplication) (string, error) {
494+
var caCertData []byte
495+
496+
for _, bslSpec := range dpa.Spec.BackupLocations {
497+
var caCert []byte
498+
499+
if bslSpec.Velero != nil && bslSpec.Velero.ObjectStorage != nil {
500+
caCert = bslSpec.Velero.ObjectStorage.CACert
501+
}
502+
if bslSpec.CloudStorage != nil {
503+
caCert = bslSpec.CloudStorage.CACert
504+
}
505+
506+
if len(caCert) > 0 {
507+
caCertData = append(caCertData, caCert...)
508+
caCertData = append(caCertData, '\n')
509+
}
510+
}
511+
512+
if len(caCertData) == 0 {
513+
return "", nil
514+
}
515+
516+
configMapName := caBundleConfigMapName
517+
configMap := &corev1.ConfigMap{
518+
ObjectMeta: metav1.ObjectMeta{
519+
Name: configMapName,
520+
Namespace: dpa.Namespace,
521+
},
522+
}
523+
524+
op, err := controllerutil.CreateOrPatch(r.Context, r.Client, configMap, func() error {
525+
if configMap.Labels == nil {
526+
configMap.Labels = make(map[string]string)
527+
}
528+
configMap.Labels["app.kubernetes.io/name"] = common.Velero
529+
configMap.Labels["app.kubernetes.io/managed-by"] = common.OADPOperator
530+
configMap.Labels["app.kubernetes.io/component"] = "ca-bundle"
531+
configMap.Labels[oadpv1alpha1.OadpOperatorLabel] = "True"
532+
533+
if configMap.Data == nil {
534+
configMap.Data = make(map[string]string)
535+
}
536+
configMap.Data[caBundleFileName] = string(caCertData)
537+
538+
return nil
539+
})
540+
541+
if err != nil {
542+
return "", fmt.Errorf("failed to create/update CA bundle ConfigMap: %w", err)
543+
}
544+
545+
if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated {
546+
r.Log.Info("CA certificate ConfigMap processed", "configMap", configMapName, "operation", op)
547+
r.EventRecorder.Event(configMap,
548+
corev1.EventTypeNormal,
549+
"CACertificateConfigMapReconciled",
550+
fmt.Sprintf("performed %s on CA certificate ConfigMap %s/%s", op, configMap.Namespace, configMap.Name),
551+
)
552+
}
553+
554+
return configMapName, nil
555+
}

controllers/bsl_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2591,3 +2591,146 @@ func TestDPAReconciler_ReconcileBackupStorageLocations(t *testing.T) {
25912591
})
25922592
}
25932593
}
2594+
2595+
func TestProcessCACertForBSLs(t *testing.T) {
2596+
testCACertPEM := `-----BEGIN CERTIFICATE-----
2597+
MIIDNzCCAh+gAwIBAgIJAJ7qAHESwpNwMA0GCSqGSIb3DQEBCwUAMDMxMTAvBgNV
2598+
BAMMKGVjMi01NC0yMTEtOC0yNDguY29tcHV0ZS0xLmFtYXpvbmF3cy5jb20wHhcN
2599+
MjUwMTI0MTcxNjQyWhcNMjYwMTI0MTcxNjQyWjAzMTEwLwYDVQQDDChlYzItNTQt
2600+
-----END CERTIFICATE-----`
2601+
2602+
tests := []struct {
2603+
name string
2604+
backupLocations []oadpv1alpha1.BackupLocation
2605+
wantConfigMapName string
2606+
wantError bool
2607+
}{
2608+
{
2609+
name: "BSL with Velero CA certificate",
2610+
backupLocations: []oadpv1alpha1.BackupLocation{
2611+
{
2612+
Velero: &velerov1.BackupStorageLocationSpec{
2613+
Provider: "aws",
2614+
StorageType: velerov1.StorageType{
2615+
ObjectStorage: &velerov1.ObjectStorageLocation{
2616+
Bucket: "test-bucket",
2617+
CACert: []byte(testCACertPEM),
2618+
},
2619+
},
2620+
},
2621+
},
2622+
},
2623+
wantConfigMapName: caBundleConfigMapName,
2624+
wantError: false,
2625+
},
2626+
{
2627+
name: "BSL with CloudStorage CA certificate",
2628+
backupLocations: []oadpv1alpha1.BackupLocation{
2629+
{
2630+
CloudStorage: &oadpv1alpha1.CloudStorageLocation{
2631+
CloudStorageRef: corev1.LocalObjectReference{Name: "test-bucket"},
2632+
CACert: []byte(testCACertPEM),
2633+
},
2634+
},
2635+
},
2636+
wantConfigMapName: caBundleConfigMapName,
2637+
wantError: false,
2638+
},
2639+
{
2640+
name: "BSL without CA certificate",
2641+
backupLocations: []oadpv1alpha1.BackupLocation{
2642+
{
2643+
Velero: &velerov1.BackupStorageLocationSpec{
2644+
Provider: "aws",
2645+
StorageType: velerov1.StorageType{
2646+
ObjectStorage: &velerov1.ObjectStorageLocation{
2647+
Bucket: "test-bucket",
2648+
},
2649+
},
2650+
},
2651+
},
2652+
},
2653+
wantConfigMapName: "",
2654+
wantError: false,
2655+
},
2656+
{
2657+
name: "No backup locations",
2658+
backupLocations: []oadpv1alpha1.BackupLocation{},
2659+
wantConfigMapName: "",
2660+
wantError: false,
2661+
},
2662+
}
2663+
2664+
for _, tt := range tests {
2665+
t.Run(tt.name, func(t *testing.T) {
2666+
dpa := &oadpv1alpha1.DataProtectionApplication{
2667+
ObjectMeta: metav1.ObjectMeta{
2668+
Name: "test-dpa",
2669+
Namespace: "test-ns",
2670+
},
2671+
Spec: oadpv1alpha1.DataProtectionApplicationSpec{
2672+
BackupLocations: tt.backupLocations,
2673+
},
2674+
}
2675+
2676+
fakeClient, err := getFakeClientFromObjects(dpa)
2677+
if err != nil {
2678+
t.Fatalf("error creating fake client: %v", err)
2679+
}
2680+
2681+
r := &DPAReconciler{
2682+
Client: fakeClient,
2683+
Scheme: fakeClient.Scheme(),
2684+
Log: logr.Discard(),
2685+
Context: context.Background(),
2686+
EventRecorder: record.NewFakeRecorder(10),
2687+
NamespacedName: types.NamespacedName{
2688+
Name: dpa.Name,
2689+
Namespace: dpa.Namespace,
2690+
},
2691+
}
2692+
2693+
gotConfigMapName, err := r.processCACertForBSLs(dpa)
2694+
2695+
if tt.wantError {
2696+
if err == nil {
2697+
t.Errorf("expected error but got nil")
2698+
}
2699+
return
2700+
}
2701+
if err != nil {
2702+
t.Errorf("unexpected error: %v", err)
2703+
return
2704+
}
2705+
2706+
if gotConfigMapName != tt.wantConfigMapName {
2707+
t.Errorf("expected ConfigMap name %q, got %q", tt.wantConfigMapName, gotConfigMapName)
2708+
}
2709+
2710+
if tt.wantConfigMapName != "" {
2711+
configMap := &corev1.ConfigMap{}
2712+
err := fakeClient.Get(context.Background(), types.NamespacedName{
2713+
Name: tt.wantConfigMapName,
2714+
Namespace: dpa.Namespace,
2715+
}, configMap)
2716+
if err != nil {
2717+
t.Fatalf("expected ConfigMap to exist: %v", err)
2718+
}
2719+
2720+
if _, ok := configMap.Data[caBundleFileName]; !ok {
2721+
t.Error("ConfigMap missing ca-bundle.pem data key")
2722+
}
2723+
2724+
if configMap.Labels["app.kubernetes.io/name"] != "velero" {
2725+
t.Errorf("expected label app.kubernetes.io/name=velero, got %s", configMap.Labels["app.kubernetes.io/name"])
2726+
}
2727+
if configMap.Labels["app.kubernetes.io/component"] != "ca-bundle" {
2728+
t.Errorf("expected label app.kubernetes.io/component=ca-bundle, got %s", configMap.Labels["app.kubernetes.io/component"])
2729+
}
2730+
if configMap.Labels[oadpv1alpha1.OadpOperatorLabel] != "True" {
2731+
t.Errorf("expected OADP operator label to be True")
2732+
}
2733+
}
2734+
})
2735+
}
2736+
}

controllers/velero.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ const (
4646

4747
TrueVal = "true"
4848
FalseVal = "false"
49+
50+
// CA certificate related constants
51+
caCertVolumeName = "ca-certificate-bundle"
52+
caCertMountPath = "/etc/velero/ca-certs"
53+
caBundleFileName = "ca-bundle.pem"
54+
caBundleConfigMapName = "velero-ca-bundle"
4955
)
5056

5157
var (
@@ -411,6 +417,11 @@ func (r *DPAReconciler) customizeVeleroDeployment(dpa *oadpv1alpha1.DataProtecti
411417
}
412418
}
413419

420+
// Process CA certificates from BackupStorageLocations
421+
if err := r.processCACertificatesForVelero(dpa, veleroDeployment, veleroContainer); err != nil {
422+
return fmt.Errorf("failed to process CA certificates: %w", err)
423+
}
424+
414425
return nil
415426
}
416427

@@ -819,3 +830,45 @@ func (r DPAReconciler) noDefaultCredentials(dpa oadpv1alpha1.DataProtectionAppli
819830
return providerNeedsDefaultCreds, hasCloudStorage, nil
820831

821832
}
833+
834+
// processCACertificatesForVelero processes CA certificates from BSLs and configures Velero deployment
835+
func (r *DPAReconciler) processCACertificatesForVelero(dpa *oadpv1alpha1.DataProtectionApplication, veleroDeployment *appsv1.Deployment, veleroContainer *corev1.Container) error {
836+
configMapName, err := r.processCACertForBSLs(dpa)
837+
if err != nil {
838+
return fmt.Errorf("failed to process CA certificates from BSLs: %w", err)
839+
}
840+
841+
if configMapName == "" {
842+
return nil
843+
}
844+
845+
caCertVolume := corev1.Volume{
846+
Name: caCertVolumeName,
847+
VolumeSource: corev1.VolumeSource{
848+
ConfigMap: &corev1.ConfigMapVolumeSource{
849+
LocalObjectReference: corev1.LocalObjectReference{
850+
Name: configMapName,
851+
},
852+
},
853+
},
854+
}
855+
veleroDeployment.Spec.Template.Spec.Volumes = append(veleroDeployment.Spec.Template.Spec.Volumes, caCertVolume)
856+
857+
caCertVolumeMount := corev1.VolumeMount{
858+
Name: caCertVolumeName,
859+
MountPath: caCertMountPath,
860+
ReadOnly: true,
861+
}
862+
veleroContainer.VolumeMounts = append(veleroContainer.VolumeMounts, caCertVolumeMount)
863+
864+
// AWS_CA_BUNDLE is a standard AWS SDK environment variable that specifies a custom CA bundle
865+
// for TLS certificate validation. When set, the AWS SDK for Go automatically uses this bundle
866+
// for all S3 API calls, enabling imagestream backup in air-gapped environments with custom CAs.
867+
awsCABundleEnv := corev1.EnvVar{
868+
Name: "AWS_CA_BUNDLE",
869+
Value: caCertMountPath + "/" + caBundleFileName,
870+
}
871+
veleroContainer.Env = common.AppendUniqueEnvVars(veleroContainer.Env, []corev1.EnvVar{awsCABundleEnv})
872+
873+
return nil
874+
}

0 commit comments

Comments
 (0)