diff --git a/api/v1alpha1/dataprotectiontest_types.go b/api/v1alpha1/dataprotectiontest_types.go index 593580cca2..632a9b2bce 100644 --- a/api/v1alpha1/dataprotectiontest_types.go +++ b/api/v1alpha1/dataprotectiontest_types.go @@ -46,6 +46,11 @@ type DataProtectionTestSpec struct { // +kubebuilder:default=false // +optional ForceRun bool `json:"forceRun,omitempty"` + + // skipTLSVerify controls whether to bypass TLS certificate validation + // +kubebuilder:default=false + // +optional + SkipTLSVerify bool `json:"skipTLSVerify,omitempty"` } // UploadSpeedTestConfig contains configuration for testing object storage upload performance. diff --git a/bundle/manifests/oadp.openshift.io_dataprotectiontests.yaml b/bundle/manifests/oadp.openshift.io_dataprotectiontests.yaml index b44bbf9ee7..b9a2429304 100644 --- a/bundle/manifests/oadp.openshift.io_dataprotectiontests.yaml +++ b/bundle/manifests/oadp.openshift.io_dataprotectiontests.yaml @@ -189,6 +189,11 @@ spec: default: false description: forceRun will re-trigger the DPT even if it already completed type: boolean + skipTLSVerify: + default: false + description: skipTLSVerify controls whether to bypass TLS certificate + validation + type: boolean uploadSpeedTestConfig: description: uploadSpeedTestConfig specifies parameters for an object storage upload speed test. diff --git a/config/crd/bases/oadp.openshift.io_dataprotectiontests.yaml b/config/crd/bases/oadp.openshift.io_dataprotectiontests.yaml index e1bed3d256..1706cf3aa5 100644 --- a/config/crd/bases/oadp.openshift.io_dataprotectiontests.yaml +++ b/config/crd/bases/oadp.openshift.io_dataprotectiontests.yaml @@ -189,6 +189,11 @@ spec: default: false description: forceRun will re-trigger the DPT even if it already completed type: boolean + skipTLSVerify: + default: false + description: skipTLSVerify controls whether to bypass TLS certificate + validation + type: boolean uploadSpeedTestConfig: description: uploadSpeedTestConfig specifies parameters for an object storage upload speed test. diff --git a/internal/controller/dataprotectiontest_controller.go b/internal/controller/dataprotectiontest_controller.go index d33efe93dc..ac2be5cf37 100644 --- a/internal/controller/dataprotectiontest_controller.go +++ b/internal/controller/dataprotectiontest_controller.go @@ -24,6 +24,7 @@ import ( "sync" "time" + "github.com/aws/aws-sdk-go/aws/credentials" "github.com/go-logr/logr" "github.com/hashicorp/go-multierror" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" @@ -240,7 +241,13 @@ func (r *DataProtectionTestReconciler) determineVendor(ctx context.Context, dpt return fmt.Errorf("failed to create HEAD request: %w", err) } - resp, err := http.DefaultClient.Do(req) + // Build HTTP client with TLS configuration + httpClient, err := buildHTTPClientWithTLS(dpt, backupLocationSpec, r.Log) + if err != nil { + return fmt.Errorf("failed to build HTTP client with TLS: %w", err) + } + + resp, err := httpClient.Do(req) if err != nil { return fmt.Errorf("HEAD request to %s failed: %w", s3Url, err) } @@ -345,13 +352,22 @@ func (r *DataProtectionTestReconciler) initializeAWSProvider(ctx context.Context s3Url = "" } - // Initialize the AWS provider - awsProvider := cloudprovider.NewAWSProvider(region, s3Url, accessKey, secretKey) + // Create AWS session with TLS configuration + sess, err := buildAWSSessionWithTLS(r.dpt, backupLocationSpec, region, s3Url, r.Log) + if err != nil { + return nil, fmt.Errorf("failed to create AWS session with TLS: %w", err) + } + + // Set credentials on the session + sess.Config.Credentials = credentials.NewStaticCredentials(accessKey, secretKey, "") + + // Initialize the AWS provider with the TLS-configured session + awsProvider := cloudprovider.NewAWSProviderWithSession(sess) if awsProvider == nil { return nil, fmt.Errorf("failed to create AWS provider") } - r.Log.Info("Successfully initialized AWS provider", "region", region, "s3Url", s3Url) + r.Log.Info("Successfully initialized AWS provider with TLS", "region", region, "s3Url", s3Url, "skipTLSVerify", r.dpt.Spec.SkipTLSVerify) return awsProvider, nil } diff --git a/internal/controller/dataprotectiontest_controller_test.go b/internal/controller/dataprotectiontest_controller_test.go index 0e82bed39c..62c89b7a42 100644 --- a/internal/controller/dataprotectiontest_controller_test.go +++ b/internal/controller/dataprotectiontest_controller_test.go @@ -32,6 +32,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client/fake" oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" @@ -301,6 +302,7 @@ aws_secret_access_key = test-secret Client: k8sClient, Context: ctx, NamespacedName: types.NamespacedName{Name: "dummy", Namespace: "openshift-adp"}, + dpt: &oadpv1alpha1.DataProtectionTest{}, } spec := &velerov1.BackupStorageLocationSpec{ @@ -525,3 +527,660 @@ func TestCreateVolumeSnapshot(t *testing.T) { require.NotNil(t, vs.Spec.Source.PersistentVolumeClaimName) require.Equal(t, cfg.SnapshotClassName, *vs.Spec.VolumeSnapshotClassName) } + +func TestBuildTLSConfig(t *testing.T) { + tests := []struct { + name string + dpt *oadpv1alpha1.DataProtectionTest + bsl *velerov1.BackupStorageLocationSpec + expectInsecure bool + expectCustomCA bool + expectError bool + description string + }{ + { + name: "skipTLSVerify is true", + dpt: &oadpv1alpha1.DataProtectionTest{ + Spec: oadpv1alpha1.DataProtectionTestSpec{ + SkipTLSVerify: true, + }, + }, + bsl: &velerov1.BackupStorageLocationSpec{}, + expectInsecure: true, + expectCustomCA: false, + expectError: false, + description: "Should set InsecureSkipVerify when skipTLSVerify is true", + }, + { + name: "skipTLSVerify takes precedence over CA cert", + dpt: &oadpv1alpha1.DataProtectionTest{ + Spec: oadpv1alpha1.DataProtectionTestSpec{ + SkipTLSVerify: true, + }, + }, + bsl: &velerov1.BackupStorageLocationSpec{ + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{ + Bucket: "test-bucket", + CACert: []byte("some-ca-cert"), // Should be ignored due to skipTLSVerify + }, + }, + }, + expectInsecure: true, + expectCustomCA: false, // Should not set custom CA when skipTLSVerify is true + expectError: false, + description: "SkipTLSVerify should take precedence over CA cert", + }, + { + name: "neither skipTLS nor custom CA", + dpt: &oadpv1alpha1.DataProtectionTest{ + Spec: oadpv1alpha1.DataProtectionTestSpec{ + SkipTLSVerify: false, + }, + }, + bsl: &velerov1.BackupStorageLocationSpec{ + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{ + Bucket: "test-bucket", + }, + }, + }, + expectInsecure: false, + expectCustomCA: false, + expectError: false, + description: "Should use system certs when no custom config", + }, + { + name: "invalid base64 CA cert", + dpt: &oadpv1alpha1.DataProtectionTest{ + Spec: oadpv1alpha1.DataProtectionTestSpec{ + SkipTLSVerify: false, + }, + }, + bsl: &velerov1.BackupStorageLocationSpec{ + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{ + Bucket: "test-bucket", + CACert: []byte("invalid-base64!"), + }, + }, + }, + expectError: true, + description: "Should error on invalid base64 CA cert", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger := logr.Discard() + + tlsConfig, err := buildTLSConfig(tt.dpt, tt.bsl, logger) + + if tt.expectError { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.NotNil(t, tlsConfig) + require.Equal(t, tt.expectInsecure, tlsConfig.InsecureSkipVerify) + + if tt.expectCustomCA { + require.NotNil(t, tlsConfig.RootCAs) + } else if !tt.expectInsecure { + // System certs case - RootCAs should be nil (uses system) + require.Nil(t, tlsConfig.RootCAs) + } + }) + } +} + +func TestBuildHTTPClientWithTLS(t *testing.T) { + tests := []struct { + name string + dpt *oadpv1alpha1.DataProtectionTest + bsl *velerov1.BackupStorageLocationSpec + expectError bool + }{ + { + name: "valid TLS config", + dpt: &oadpv1alpha1.DataProtectionTest{ + Spec: oadpv1alpha1.DataProtectionTestSpec{ + SkipTLSVerify: true, + }, + }, + bsl: &velerov1.BackupStorageLocationSpec{}, + expectError: false, + }, + { + name: "invalid CA cert", + dpt: &oadpv1alpha1.DataProtectionTest{ + Spec: oadpv1alpha1.DataProtectionTestSpec{ + SkipTLSVerify: false, + }, + }, + bsl: &velerov1.BackupStorageLocationSpec{ + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{ + CACert: []byte("invalid-base64!"), + }, + }, + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger := logr.Discard() + + client, err := buildHTTPClientWithTLS(tt.dpt, tt.bsl, logger) + + if tt.expectError { + require.Error(t, err) + require.Nil(t, client) + } else { + require.NoError(t, err) + require.NotNil(t, client) + require.NotNil(t, client.Transport) + } + }) + } +} + +func TestBuildAWSSessionWithTLS(t *testing.T) { + tests := []struct { + name string + dpt *oadpv1alpha1.DataProtectionTest + bsl *velerov1.BackupStorageLocationSpec + region string + endpoint string + expectError bool + }{ + { + name: "valid AWS session with TLS", + dpt: &oadpv1alpha1.DataProtectionTest{ + Spec: oadpv1alpha1.DataProtectionTestSpec{ + SkipTLSVerify: true, + }, + }, + bsl: &velerov1.BackupStorageLocationSpec{}, + region: "us-east-1", + endpoint: "", + expectError: false, + }, + { + name: "with custom endpoint", + dpt: &oadpv1alpha1.DataProtectionTest{ + Spec: oadpv1alpha1.DataProtectionTestSpec{ + SkipTLSVerify: false, + }, + }, + bsl: &velerov1.BackupStorageLocationSpec{}, + region: "us-west-2", + endpoint: "https://minio.example.com", + expectError: false, + }, + { + name: "invalid TLS config", + dpt: &oadpv1alpha1.DataProtectionTest{ + Spec: oadpv1alpha1.DataProtectionTestSpec{ + SkipTLSVerify: false, + }, + }, + bsl: &velerov1.BackupStorageLocationSpec{ + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{ + CACert: []byte("invalid-base64!"), + }, + }, + }, + region: "us-east-1", + endpoint: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger := logr.Discard() + + session, err := buildAWSSessionWithTLS(tt.dpt, tt.bsl, tt.region, tt.endpoint, logger) + + if tt.expectError { + require.Error(t, err) + require.Nil(t, session) + } else { + require.NoError(t, err) + require.NotNil(t, session) + require.NotNil(t, session.Config) + require.Equal(t, tt.region, *session.Config.Region) + if tt.endpoint != "" { + require.Equal(t, tt.endpoint, *session.Config.Endpoint) + require.True(t, *session.Config.S3ForcePathStyle) + } + } + }) + } +} + +func TestInitializeGCPProvider(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, oadpv1alpha1.AddToScheme(scheme)) + require.NoError(t, velerov1.AddToScheme(scheme)) + require.NoError(t, corev1.AddToScheme(scheme)) + + ctx := context.Background() + + gcpSecretData := `{ + "type": "service_account", + "project_id": "test-project", + "private_key_id": "test-key-id", + "private_key": "-----BEGIN PRIVATE KEY-----\ntest-key\n-----END PRIVATE KEY-----\n", + "client_email": "test@test-project.iam.gserviceaccount.com", + "client_id": "123456789", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token" + }` + + gcpSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gcp-secret", + Namespace: "openshift-adp", + }, + Data: map[string][]byte{ + "cloud": []byte(gcpSecretData), + }, + } + + tests := []struct { + name string + setupSecrets bool + expectError bool + }{ + { + name: "missing secret", + setupSecrets: false, + expectError: true, + }, + { + name: "missing bucket", + setupSecrets: true, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + builder := fake.NewClientBuilder().WithScheme(scheme) + if tt.setupSecrets { + builder.WithObjects(gcpSecret) + } + k8sClient := builder.Build() + + reconciler := &DataProtectionTestReconciler{ + Client: k8sClient, + Context: ctx, + NamespacedName: types.NamespacedName{Name: "dummy", Namespace: "openshift-adp"}, + dpt: &oadpv1alpha1.DataProtectionTest{}, + } + + bslSpec := &velerov1.BackupStorageLocationSpec{ + Provider: "gcp", + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{ + Bucket: "", + }, + }, + Credential: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "gcp-secret", + }, + Key: "cloud", + }, + } + + if tt.name == "missing bucket" { + bslSpec.ObjectStorage.Bucket = "" + } + + _, err := reconciler.initializeGCPProvider(ctx, bslSpec) + if tt.expectError { + require.Error(t, err) + } + }) + } +} + +func TestUpdateDPTErrorStatus(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, oadpv1alpha1.AddToScheme(scheme)) + + ctx := context.Background() + dpt := &oadpv1alpha1.DataProtectionTest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dpt", + Namespace: "openshift-adp", + }, + Status: oadpv1alpha1.DataProtectionTestStatus{ + Phase: "InProgress", + }, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(dpt).Build() + + reconciler := &DataProtectionTestReconciler{ + Client: fakeClient, + NamespacedName: types.NamespacedName{Name: "test-dpt", Namespace: "openshift-adp"}, + } + + // Test that the function doesn't panic or error - this is a fire-and-forget function + errorMsg := "test error message" + reconciler.updateDPTErrorStatus(ctx, errorMsg) + + // This test mainly verifies the function can be called without errors + // The actual status update verification is complex due to retry logic +} + +func TestWaitForSnapshotReady(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, snapshotv1api.AddToScheme(scheme)) + + // Create a test VolumeSnapshot + vs := &snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-snapshot", + Namespace: "test-ns", + }, + Status: &snapshotv1api.VolumeSnapshotStatus{ + ReadyToUse: &[]bool{false}[0], // Not ready initially + }, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(vs).Build() + + reconciler := &DataProtectionTestReconciler{ + ClusterWideClient: fakeClient, + } + + // Test timeout case + ctx := context.Background() + err := reconciler.waitForSnapshotReady(ctx, vs, 1*time.Millisecond) // Very short timeout + require.Error(t, err) + require.Contains(t, err.Error(), "timed out") +} + +func TestInitializeAzureProvider(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, oadpv1alpha1.AddToScheme(scheme)) + require.NoError(t, velerov1.AddToScheme(scheme)) + require.NoError(t, corev1.AddToScheme(scheme)) + + ctx := context.Background() + + azureSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "azure-secret", + Namespace: "openshift-adp", + }, + Data: map[string][]byte{ + "AZURE_SUBSCRIPTION_ID": []byte("test-subscription-id"), + "AZURE_TENANT_ID": []byte("test-tenant-id"), + "AZURE_CLIENT_ID": []byte("test-client-id"), + "AZURE_CLIENT_SECRET": []byte("test-client-secret"), + }, + } + + tests := []struct { + name string + setupSecrets bool + expectError bool + }{ + { + name: "valid Azure config", + setupSecrets: true, + expectError: false, + }, + { + name: "missing secret", + setupSecrets: false, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + builder := fake.NewClientBuilder().WithScheme(scheme) + if tt.setupSecrets { + builder.WithObjects(azureSecret) + } + k8sClient := builder.Build() + + reconciler := &DataProtectionTestReconciler{ + Client: k8sClient, + Context: ctx, + NamespacedName: types.NamespacedName{Name: "dummy", Namespace: "openshift-adp"}, + dpt: &oadpv1alpha1.DataProtectionTest{}, + } + + bslSpec := &velerov1.BackupStorageLocationSpec{ + Provider: "azure", + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{ + Bucket: "test-azure-container", + }, + }, + Credential: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "azure-secret", + }, + Key: "cloud", + }, + } + + cp, err := reconciler.initializeAzureProvider(ctx, bslSpec) + + if tt.expectError { + require.Error(t, err) + require.Nil(t, cp) + } else { + require.NoError(t, err) + require.NotNil(t, cp) + } + }) + } +} + +func TestReconcile(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, oadpv1alpha1.AddToScheme(scheme)) + require.NoError(t, velerov1.AddToScheme(scheme)) + require.NoError(t, corev1.AddToScheme(scheme)) + + tests := []struct { + name string + dpt *oadpv1alpha1.DataProtectionTest + expectPhase string + expectRequeue bool + expectError bool + description string + }{ + { + name: "DPT not found", + dpt: nil, // DPT not created + expectRequeue: false, + expectError: false, + description: "Should handle missing DPT gracefully", + }, + { + name: "DPT already completed", + dpt: &oadpv1alpha1.DataProtectionTest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dpt", + Namespace: "openshift-adp", + }, + Spec: oadpv1alpha1.DataProtectionTestSpec{ + ForceRun: false, + }, + Status: oadpv1alpha1.DataProtectionTestStatus{ + Phase: "Complete", + }, + }, + expectRequeue: false, + expectError: false, + description: "Should skip completed DPT when forceRun is false", + }, + { + name: "DPT with missing backup location", + dpt: &oadpv1alpha1.DataProtectionTest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dpt", + Namespace: "openshift-adp", + }, + Spec: oadpv1alpha1.DataProtectionTestSpec{ + // Missing both BackupLocationSpec and BackupLocationName + }, + }, + expectRequeue: false, + expectError: true, + description: "Should fail when backup location is not specified", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + builder := fake.NewClientBuilder().WithScheme(scheme) + + if tt.dpt != nil { + builder.WithObjects(tt.dpt) + } + + k8sClient := builder.Build() + + reconciler := &DataProtectionTestReconciler{ + Client: k8sClient, + Scheme: scheme, + Log: logr.Discard(), + } + + req := ctrl.Request{ + NamespacedName: types.NamespacedName{ + Name: "test-dpt", + Namespace: "openshift-adp", + }, + } + + result, err := reconciler.Reconcile(ctx, req) + + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + require.Equal(t, tt.expectRequeue, result.Requeue) + }) + } +} + +func TestRunSnapshotTests(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, oadpv1alpha1.AddToScheme(scheme)) + require.NoError(t, snapshotv1api.AddToScheme(scheme)) + + tests := []struct { + name string + dpt *oadpv1alpha1.DataProtectionTest + expectedSnapshotCount int + expectedSummary string + expectError bool + description string + }{ + { + name: "no snapshot tests configured", + dpt: &oadpv1alpha1.DataProtectionTest{ + Spec: oadpv1alpha1.DataProtectionTestSpec{ + CSIVolumeSnapshotTestConfigs: []oadpv1alpha1.CSIVolumeSnapshotTestConfig{}, + }, + }, + expectedSnapshotCount: 0, + expectedSummary: "0/0 passed", + expectError: false, + description: "Should handle empty snapshot test configs", + }, + { + name: "single snapshot test with incomplete config", + dpt: &oadpv1alpha1.DataProtectionTest{ + Spec: oadpv1alpha1.DataProtectionTestSpec{ + CSIVolumeSnapshotTestConfigs: []oadpv1alpha1.CSIVolumeSnapshotTestConfig{ + { + // Missing required fields - should be skipped + SnapshotClassName: "", + VolumeSnapshotSource: oadpv1alpha1.VolumeSnapshotSource{ + PersistentVolumeClaimName: "", + }, + }, + }, + }, + }, + expectedSnapshotCount: 0, + expectedSummary: "0/0 passed", + expectError: false, + description: "Should skip snapshot tests with missing required fields", + }, + { + name: "multiple snapshot tests with valid config", + dpt: &oadpv1alpha1.DataProtectionTest{ + Spec: oadpv1alpha1.DataProtectionTestSpec{ + CSIVolumeSnapshotTestConfigs: []oadpv1alpha1.CSIVolumeSnapshotTestConfig{ + { + SnapshotClassName: "csi-snapshot-class", + VolumeSnapshotSource: oadpv1alpha1.VolumeSnapshotSource{ + PersistentVolumeClaimName: "pvc-1", + PersistentVolumeClaimNamespace: "test-ns", + }, + Timeout: metav1.Duration{Duration: 10 * time.Second}, + }, + { + SnapshotClassName: "csi-snapshot-class", + VolumeSnapshotSource: oadpv1alpha1.VolumeSnapshotSource{ + PersistentVolumeClaimName: "pvc-2", + PersistentVolumeClaimNamespace: "test-ns", + }, + Timeout: metav1.Duration{Duration: 10 * time.Second}, + }, + }, + }, + }, + expectedSnapshotCount: 2, + expectedSummary: "0/2 passed", // Will fail due to fake client + expectError: true, // Will have errors creating snapshots + description: "Should attempt to create multiple snapshot tests", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + + reconciler := &DataProtectionTestReconciler{ + Client: fakeClient, + ClusterWideClient: fakeClient, + Log: logr.Discard(), + } + + err := reconciler.runSnapshotTests(ctx, tt.dpt) + + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + require.Equal(t, tt.expectedSnapshotCount, len(tt.dpt.Status.SnapshotTests)) + require.Equal(t, tt.expectedSummary, tt.dpt.Status.SnapshotSummary) + }) + } +} diff --git a/internal/controller/tls_config.go b/internal/controller/tls_config.go new file mode 100644 index 0000000000..8926bda596 --- /dev/null +++ b/internal/controller/tls_config.go @@ -0,0 +1,118 @@ +/* +Copyright 2021. + +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 controller + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/go-logr/logr" + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + + oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" +) + +// buildTLSConfig creates a TLS configuration based on the DPT spec and BSL spec. +// Priority order: +// 1. If skipTLSVerify is true → InsecureSkipVerify: true +// 2. If BSL has caCert → Use custom CA cert +// 3. Otherwise → Use system certs (default) +func buildTLSConfig(dpt *oadpv1alpha1.DataProtectionTest, bsl *velerov1.BackupStorageLocationSpec, logger logr.Logger) (*tls.Config, error) { + tlsConfig := &tls.Config{} + + // Priority 1: Check if skipTLSVerify is set + if dpt.Spec.SkipTLSVerify { + logger.Info("TLS verification disabled via skipTLSVerify") + tlsConfig.InsecureSkipVerify = true + return tlsConfig, nil + } + + // Priority 2: Check for custom CA cert in BSL + if bsl != nil && bsl.ObjectStorage != nil && bsl.ObjectStorage.CACert != nil { + logger.Info("Custom CA certificate found in BSL") + + // Use the PEM certificate directly (already decoded by Kubernetes) + caCertPEM := bsl.ObjectStorage.CACert + + // Create certificate pool with custom CA + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caCertPEM) { + return nil, fmt.Errorf("failed to parse CA certificate") + } + + tlsConfig.RootCAs = caCertPool + logger.Info("Successfully configured custom CA certificate") + return tlsConfig, nil + } + + // Priority 3: Use system certificates (default behavior) + logger.Info("Using system default certificates") + return tlsConfig, nil +} + +// buildHTTPClientWithTLS creates an HTTP client with the appropriate TLS configuration +func buildHTTPClientWithTLS(dpt *oadpv1alpha1.DataProtectionTest, bsl *velerov1.BackupStorageLocationSpec, logger logr.Logger) (*http.Client, error) { + tlsConfig, err := buildTLSConfig(dpt, bsl, logger) + if err != nil { + return nil, fmt.Errorf("failed to build TLS config: %w", err) + } + + transport := &http.Transport{ + TLSClientConfig: tlsConfig, + } + + client := &http.Client{ + Transport: transport, + } + + return client, nil +} + +// buildAWSSessionWithTLS creates an AWS session with the appropriate TLS configuration +func buildAWSSessionWithTLS(dpt *oadpv1alpha1.DataProtectionTest, bsl *velerov1.BackupStorageLocationSpec, region, endpoint string, logger logr.Logger) (*session.Session, error) { + tlsConfig, err := buildTLSConfig(dpt, bsl, logger) + if err != nil { + return nil, fmt.Errorf("failed to build TLS config: %w", err) + } + + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + + awsConfig := &aws.Config{ + Region: aws.String(region), + HTTPClient: httpClient, + } + + if endpoint != "" { + awsConfig.Endpoint = aws.String(endpoint) + awsConfig.S3ForcePathStyle = aws.Bool(true) + } + + sess, err := session.NewSession(awsConfig) + if err != nil { + return nil, fmt.Errorf("failed to create AWS session: %w", err) + } + + return sess, nil +} diff --git a/pkg/cloudprovider/aws.go b/pkg/cloudprovider/aws.go index f36cb00abd..d61003fc39 100644 --- a/pkg/cloudprovider/aws.go +++ b/pkg/cloudprovider/aws.go @@ -43,6 +43,15 @@ func NewAWSProvider(region, endpoint, accessKey, secretKey string) *AWSProvider } } +// NewAWSProviderWithSession creates an AWSProvider with a pre-configured session. +// This allows for custom configurations like TLS settings. +func NewAWSProviderWithSession(sess *session.Session) *AWSProvider { + s3Client := s3.New(sess) + return &AWSProvider{ + s3Client: s3Client, + } +} + func (a *AWSProvider) UploadTest(ctx context.Context, config oadpv1alpha1.UploadSpeedTestConfig, bucket string, log logr.Logger) (int64, time.Duration, error) { log.Info("Starting upload speed test", "fileSize", config.FileSize, "timeout", config.Timeout.Duration.String())