Skip to content

s3: Read S3 secrets from live hub cluster instead of gathered data#382

Merged
nirs merged 8 commits intoRamenDR:mainfrom
parikshithb:s3_online
Feb 17, 2026
Merged

s3: Read S3 secrets from live hub cluster instead of gathered data#382
nirs merged 8 commits intoRamenDR:mainfrom
parikshithb:s3_online

Conversation

@parikshithb
Copy link
Member

@parikshithb parikshithb commented Jan 30, 2026

With secret sanitization (#374), gathered secrets will contain hashes
instead of actual credentials, breaking S3 operations.

  • Add GetSecret to backend interfaces to fetch secrets from live cluster
  • Update ClusterProfiles to return ramenapi.S3StoreProfile directly
  • Add S3ProfileFromStore helper to convert profile with credentials
  • Add cancellation support when fetching S3 secrets

Add helper functions for S3 operations:

  • gatherApplicationS3Data: inspect and gather, stop only on cancellation
  • checkClustersS3: inspect and check, stop only on cancellation

Both helpers return false only on cancellation, allowing validation to
continue on other errors and report them in results.

This ensures S3 operations (GatherS3, CheckS3) have real credentials
even when gathered data contains sanitized secrets.

Testing

Pass:

gather application:

./ramenctl gather application --name appset-deploy-rbd --namespace argocd -o out/gather_app_fresh
⭐ Using config "config.yaml"
⭐ Using report "out/gather_app_fresh"

🔎 Validate config ...
   ✅ Config validated

🔎 Gather application data ...
   ✅ Inspected application
   ✅ Gathered data from cluster "hub"
   ✅ Gathered data from cluster "dr2"
   ✅ Gathered data from cluster "dr1"
   ✅ Inspected S3 profiles
   ✅ Gathered S3 profile "minio-on-dr2"
   ✅ Gathered S3 profile "minio-on-dr1"

validate application:

./ramenctl validate application --namespace argocd --name appset-deploy-rbd --output out/v_app_fresh
⭐ Using config "config.yaml"
⭐ Using report "out/v_app_fresh"

🔎 Validate config ...
   ✅ Config validated

🔎 Validate application ...
   ✅ Inspected application
   ✅ Gathered data from cluster "hub"
   ✅ Gathered data from cluster "dr1"
   ✅ Gathered data from cluster "dr2"
   ✅ Inspected S3 profiles
   ✅ Gathered S3 profile "minio-on-dr2"
   ✅ Gathered S3 profile "minio-on-dr1"
   ✅ Application validated

✅ Validation completed (24 ok, 0 stale, 0 problem)

validate clusters:

./ramenctl validate clusters --output out/v_clu_fresh
⭐ Using config "config.yaml"
⭐ Using report "out/v_clu_fresh"

🔎 Validate config ...
   ✅ Config validated

🔎 Validate clusters ...
   ✅ Gathered data from cluster "hub"
   ✅ Gathered data from cluster "dr2"
   ✅ Gathered data from cluster "dr1"
   ✅ Inspected S3 profiles
   ✅ Checked S3 profile "minio-on-dr2"
   ✅ Checked S3 profile "minio-on-dr1"
   ✅ Clusters validated

✅ Validation completed (90 ok, 0 stale, 0 problem)

Failure:

  1. Delete ramen-s3-secret-dr1 secret on hub.

gather application:

./ramenctl gather application --name appset-deploy-rbd --namespace argocd -o out/gather_app_fail
⭐ Using config "config.yaml"
⭐ Using report "out/gather_app_fail"

🔎 Validate config ...
   ✅ Config validated

🔎 Gather application data ...
   ✅ Inspected application
   ✅ Gathered data from cluster "hub"
   ✅ Gathered data from cluster "dr2"
   ✅ Gathered data from cluster "dr1"
   ✅ Inspected S3 profiles
   ❌ Failed to gather S3 profile "minio-on-dr1"
   ✅ Gathered S3 profile "minio-on-dr2"

❌ Gather failed

validate application:

./ramenctl validate application --namespace argocd --name appset-deploy-rbd --output out/v_app_fail
⭐ Using config "config.yaml"
⭐ Using report "out/v_app_fail"

🔎 Validate config ...
   ✅ Config validated

🔎 Validate application ...
   ✅ Inspected application
   ✅ Gathered data from cluster "hub"
   ✅ Gathered data from cluster "dr2"
   ✅ Gathered data from cluster "dr1"
   ✅ Inspected S3 profiles
   ❌ Failed to gather S3 profile "minio-on-dr1"
   ✅ Gathered S3 profile "minio-on-dr2"
   ❌ Issues found during validation

❌ validation failed (23 ok, 0 stale, 1 problem)

report:

  s3:
    profiles:
      state: ok ✅
      value:
      - gathered:
          description: 'failed to download objects from profile "minio-on-dr1": failed
            to list objects in bucket "bucket" with prefix "test-appset-deploy-rbd/appset-deploy-rbd/":
            operation error S3: ListObjectsV2, get identity: get credentials: failed
            to refresh cached credentials, static credentials are empty'
          state: problem ❌
        name: minio-on-dr1
      - gathered:
          state: ok ✅
          value: true
        name: minio-on-dr2

validate clusters:

 ./ramenctl validate clusters --output out/v_clu_fail

⭐ Using config "config.yaml"
⭐ Using report "out/v_clu_fail"

🔎 Validate config ...
   ✅ Config validated

🔎 Validate clusters ...
   ✅ Gathered data from cluster "hub"
   ✅ Gathered data from cluster "dr1"
   ✅ Gathered data from cluster "dr2"
   ✅ Inspected S3 profiles
   ❌ Failed to check S3 profile "minio-on-dr1"
   ✅ Checked S3 profile "minio-on-dr2"
   ❌ Issues found during validation

❌ validation failed (81 ok, 0 stale, 7 problem)

report:

s3:
    profiles:
      state: ok ✅
      value:
      - accessible:
          description: 'failed to access bucket "bucket" for profile "minio-on-dr1":
            operation error S3: HeadBucket, get identity: get credentials: failed
            to refresh cached credentials, static credentials are empty'
          state: problem ❌
        name: minio-on-dr1
      - accessible:
          state: ok ✅
          value: true
        name: minio-on-dr2
  1. Invalid access key in dr1 profile:

Gather application

./ramenctl gather application --name appset-deploy-rbd --namespace argocd -o out/gather_app_fresh_fail
⭐ Using config "config.yaml"
⭐ Using report "out/gather_app_fresh_fail"

🔎 Validate config ...
   ✅ Config validated

🔎 Gather application data ...
   ✅ Inspected application
   ✅ Gathered data from cluster "hub"
   ✅ Gathered data from cluster "dr2"
   ✅ Gathered data from cluster "dr1"
   ✅ Inspected S3 profiles
   ❌ Failed to gather S3 profile "minio-on-dr1"
   ✅ Gathered S3 profile "minio-on-dr2"

❌ Gather failed

Validate Application

 ./ramenctl validate application --namespace argocd --name appset-deploy-rbd --output out/v_app_fresh_fail
⭐ Using config "config.yaml"
⭐ Using report "out/v_app_fresh_fail"

🔎 Validate config ...
   ✅ Config validated

🔎 Validate application ...
   ✅ Inspected application
   ✅ Gathered data from cluster "hub"
   ✅ Gathered data from cluster "dr2"
   ✅ Gathered data from cluster "dr1"
   ✅ Inspected S3 profiles
   ❌ Failed to gather S3 profile "minio-on-dr1"
   ✅ Gathered S3 profile "minio-on-dr2"
   ❌ Issues found during validation

❌ validation failed (21 ok, 0 stale, 3 problem)

report:

s3:
    profiles:
      state: ok ✅
      value:
      - gathered:
          description: 'failed to download objects from profile "minio-on-dr1": failed
            to list objects in bucket "bucket" with prefix "test-appset-deploy-rbd/appset-deploy-rbd/":
            operation error S3: ListObjectsV2, https response error StatusCode: 403,
            RequestID: 1895057ADE595B8A, HostID: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8,
            api error InvalidAccessKeyId: The Access Key Id you provided does not
            exist in our records.'
          state: problem ❌
        name: minio-on-dr1
      - gathered:
          state: ok ✅
          value: true
        name: minio-on-dr2

validate clusters:

./ramenctl validate clusters --output out/v_clu_new_fail
⭐ Using config "config.yaml"
⭐ Using report "out/v_clu_new_fail"

🔎 Validate config ...
   ✅ Config validated

🔎 Validate clusters ...
   ✅ Gathered data from cluster "hub"
   ✅ Gathered data from cluster "dr2"
   ✅ Gathered data from cluster "dr1"
   ✅ Inspected S3 profiles
   ❌ Failed to check S3 profile "minio-on-dr1"
   ✅ Checked S3 profile "minio-on-dr2"
   ❌ Issues found during validation

❌ validation failed (88 ok, 0 stale, 2 problem)

report:

  s3:
    profiles:
      state: ok ✅
      value:
      - accessible:
          description: 'failed to access bucket "bucket" for profile "minio-on-dr1":
            operation error S3: HeadBucket, https response error StatusCode: 403,
            RequestID: 189504060BE07824, HostID: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8,
            api error Forbidden: Forbidden'
          state: problem ❌
        name: minio-on-dr1
      - accessible:
          state: ok ✅
          value: true
        name: minio-on-dr2

Log warning:

2026-02-10T13:27:58.409+0530	WARN	validate/clusters.go:149	Failed to get S3 secret "ramen-system/ramen-s3-secret-dr1" from cluster "hub": secrets "ramen-s3-secret-dr1" not found

Fixes #380

@parikshithb parikshithb requested a review from nirs January 30, 2026 13:41
@parikshithb parikshithb marked this pull request as draft January 30, 2026 13:41
@parikshithb parikshithb marked this pull request as ready for review February 6, 2026 16:33
@parikshithb
Copy link
Member Author

Addressed the changes as discussed, updated description with test results.

@parikshithb
Copy link
Member Author

Updates:

  1. Separated backend and mock changes to a separate commit.
  2. Better logging and console messages for S3 data gathering for test command.
  3. Tests for not found and empty secret scenarios
  4. Retested just the pass cases (no backend changes). Updated the description.

Copy link
Member

@nirs nirs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested changes:

  • rename helper secret value so we don't trigger gosec and it is clear that we don't keep secrets in the code
  • use []byte for secrets read from k8s secrets, matching the real value
  • rename Profile secrets fields to match the AWS names for clarity
  • remove unneeded Wrong helper constants
  • unify invalid/empty secrets tests
diff --git a/pkg/gather/command_test.go b/pkg/gather/command_test.go
index cefe5c0..e1dadd9 100644
--- a/pkg/gather/command_test.go
+++ b/pkg/gather/command_test.go
@@ -106,12 +106,12 @@ var (
 		},
 	}
 
-	getSecretEmpty = &validation.Mock{
+	getSecretInvalid = &validation.Mock{
 		GetSecretFunc: func(ctx validation.Context, cluster *types.Cluster, name, namespace string) (*corev1.Secret, error) {
 			return &corev1.Secret{
 				Data: map[string][]byte{
-					"AWS_ACCESS_KEY_ID":     []byte(helpers.WrongAccessKey),
-					"AWS_SECRET_ACCESS_KEY": []byte(helpers.WrongSecretKey),
+					"AWS_ACCESS_KEY_ID":     []byte("invalid id"),
+					"AWS_SECRET_ACCESS_KEY": []byte("invalid key"),
 				},
 			}, nil
 		},
@@ -357,8 +357,8 @@ func TestGatherApplicationGetSecretFailed(t *testing.T) {
 	checkItems(t, cmd.report.Steps[1], items)
 }
 
-func TestGatherApplicationGetSecretEmpty(t *testing.T) {
-	cmd := testCommand(t, getSecretEmpty)
+func TestGatherApplicationGetSecretInvalid(t *testing.T) {
+	cmd := testCommand(t, getSecretInvalid)
 	helpers.AddGatheredData(t, cmd.dataDir(), "appset-deploy-rbd", "validate-application")
 	if err := cmd.Application(drpcName, drpcNamespace); err == nil {
 		t.Fatal("command did not fail")
@@ -372,8 +372,8 @@ func TestGatherApplicationGetSecretEmpty(t *testing.T) {
 	checkStep(t, cmd.report.Steps[0], "validate config", report.Passed)
 	checkStep(t, cmd.report.Steps[1], "gather data", report.Failed)
 
-	// When GetSecret returns a secret with no data. The profile will have empty
-	// credentials causing S3 gather to fail.
+	// When GetSecret returns a secret with invalid value, causing S3 gather to
+	// fail.
 	items := []*report.Step{
 		{Name: "inspect application", Status: report.Passed},
 		{Name: "gather \"hub\"", Status: report.Passed},
diff --git a/pkg/helpers/helpers.go b/pkg/helpers/helpers.go
index 882abaa..19cfd3d 100644
--- a/pkg/helpers/helpers.go
+++ b/pkg/helpers/helpers.go
@@ -19,21 +19,13 @@ import (
 const (
 	Modified = "modified"
 
-	// Base64 decoded secret values from testdata.
-	// Both K8s and OCP testdata secrets use the same values.
-	AccessKey = "this is not a real secret"
-	//nolint:gosec
-	SecretKey = "this is not a real key"
+	// Secrets from test testdata. Both k8s and ocp use the same values.
 
-	// Invalid credentials for testing GetSecret failure scenarios.
-	WrongAccessKey = "wrong-access-key"
-	WrongSecretKey = "wrong-secret-key"
+	FakeAWSKeyID = "this is not a real secret"
+	FakeAWSKey   = "this is not a real key"
 
-	// Secret key fingerprints (SHA-256 hashes) for testdata.
-	// Both K8s and OCP testdata secrets have the same data values.
-	AccessKeyFingerprint = "F3:1C:B8:5A:2C:33:BA:C3:57:84:22:D5:11:F5:35:40:FF:A8:6A:34:B8:CD:42:AC:86:65:E2:2B:E1:05:EA:23"
-	//nolint:gosec
-	SecretKeyFingerprint = "BC:42:FE:14:DB:F0:91:1C:91:1F:8F:CF:72:AF:CE:C5:83:5C:AF:93:AC:08:40:CE:31:D8:67:CA:AC:BC:E4:16"
+	FakeAWSKeyIDFingerprint = "F3:1C:B8:5A:2C:33:BA:C3:57:84:22:D5:11:F5:35:40:FF:A8:6A:34:B8:CD:42:AC:86:65:E2:2B:E1:05:EA:23"
+	FakeAWSKeyFingerprint   = "BC:42:FE:14:DB:F0:91:1C:91:1F:8F:CF:72:AF:CE:C5:83:5C:AF:93:AC:08:40:CE:31:D8:67:CA:AC:BC:E4:16"
 )
 
 func MarshalYAML(t *testing.T, a any) string {
diff --git a/pkg/ramen/ramen.go b/pkg/ramen/ramen.go
index a9c8c28..d0f8472 100644
--- a/pkg/ramen/ramen.go
+++ b/pkg/ramen/ramen.go
@@ -276,19 +276,19 @@ func ClusterProfiles(
 // S3ProfileFromStore creates an s3.Profile from a ramen S3StoreProfile and secret.
 // If secret is nil, the profile will have empty credentials.
 func S3ProfileFromStore(storeProfile *ramenapi.S3StoreProfile, secret *corev1.Secret) *s3.Profile {
-	var accessKey, secretKey string
+	var accessKey, secretKey []byte
 	if secret != nil {
-		accessKey = string(secret.Data["AWS_ACCESS_KEY_ID"])
-		secretKey = string(secret.Data["AWS_SECRET_ACCESS_KEY"])
+		accessKey = secret.Data["AWS_ACCESS_KEY_ID"]
+		secretKey = secret.Data["AWS_SECRET_ACCESS_KEY"]
 	}
 	return &s3.Profile{
-		Name:          storeProfile.S3ProfileName,
-		Bucket:        storeProfile.S3Bucket,
-		Region:        storeProfile.S3Region,
-		Endpoint:      storeProfile.S3CompatibleEndpoint,
-		CACertificate: storeProfile.CACertificates,
-		AccessKey:     accessKey,
-		SecretKey:     secretKey,
+		Name:               storeProfile.S3ProfileName,
+		Bucket:             storeProfile.S3Bucket,
+		Region:             storeProfile.S3Region,
+		Endpoint:           storeProfile.S3CompatibleEndpoint,
+		CACertificate:      storeProfile.CACertificates,
+		AWSAccessKeyID:     accessKey,
+		AWSSecretAccessKey: secretKey,
 	}
 }
 
diff --git a/pkg/report/clusters_test.go b/pkg/report/clusters_test.go
index 90749b2..a6726c3 100644
--- a/pkg/report/clusters_test.go
+++ b/pkg/report/clusters_test.go
@@ -688,11 +688,11 @@ func testClusterStatus() *report.ClustersStatus {
 									},
 									AWSAccessKeyID: report.ValidatedFingerprint{
 										Validated: report.Validated{State: report.OK},
-										Value:     helpers.AccessKeyFingerprint,
+										Value:     helpers.FakeAWSKeyIDFingerprint,
 									},
 									AWSSecretAccessKey: report.ValidatedFingerprint{
 										Validated: report.Validated{State: report.OK},
-										Value:     helpers.SecretKeyFingerprint,
+										Value:     helpers.FakeAWSKeyFingerprint,
 									},
 								},
 								// CACertificate is optional, empty is OK if hub also has no cert.
@@ -728,11 +728,11 @@ func testClusterStatus() *report.ClustersStatus {
 									},
 									AWSAccessKeyID: report.ValidatedFingerprint{
 										Validated: report.Validated{State: report.OK},
-										Value:     helpers.AccessKeyFingerprint,
+										Value:     helpers.FakeAWSKeyIDFingerprint,
 									},
 									AWSSecretAccessKey: report.ValidatedFingerprint{
 										Validated: report.Validated{State: report.OK},
-										Value:     helpers.SecretKeyFingerprint,
+										Value:     helpers.FakeAWSKeyFingerprint,
 									},
 								},
 								CACertificate: report.ValidatedFingerprint{
@@ -824,11 +824,11 @@ func testClusterStatus() *report.ClustersStatus {
 										},
 										AWSAccessKeyID: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.AccessKeyFingerprint,
+											Value:     helpers.FakeAWSKeyIDFingerprint,
 										},
 										AWSSecretAccessKey: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.SecretKeyFingerprint,
+											Value:     helpers.FakeAWSKeyFingerprint,
 										},
 									},
 									CACertificate: report.ValidatedFingerprint{
@@ -863,11 +863,11 @@ func testClusterStatus() *report.ClustersStatus {
 										},
 										AWSAccessKeyID: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.AccessKeyFingerprint,
+											Value:     helpers.FakeAWSKeyIDFingerprint,
 										},
 										AWSSecretAccessKey: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.SecretKeyFingerprint,
+											Value:     helpers.FakeAWSKeyFingerprint,
 										},
 									},
 									CACertificate: report.ValidatedFingerprint{
@@ -958,11 +958,11 @@ func testClusterStatus() *report.ClustersStatus {
 										},
 										AWSAccessKeyID: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.AccessKeyFingerprint,
+											Value:     helpers.FakeAWSKeyIDFingerprint,
 										},
 										AWSSecretAccessKey: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.SecretKeyFingerprint,
+											Value:     helpers.FakeAWSKeyFingerprint,
 										},
 									},
 									CACertificate: report.ValidatedFingerprint{
@@ -997,11 +997,11 @@ func testClusterStatus() *report.ClustersStatus {
 										},
 										AWSAccessKeyID: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.AccessKeyFingerprint,
+											Value:     helpers.FakeAWSKeyIDFingerprint,
 										},
 										AWSSecretAccessKey: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.SecretKeyFingerprint,
+											Value:     helpers.FakeAWSKeyFingerprint,
 										},
 									},
 									CACertificate: report.ValidatedFingerprint{
diff --git a/pkg/s3/s3.go b/pkg/s3/s3.go
index e43008b..a26c76c 100644
--- a/pkg/s3/s3.go
+++ b/pkg/s3/s3.go
@@ -35,13 +35,13 @@ const (
 
 // Profile contains S3 connection and authentication information.
 type Profile struct {
-	Name          string
-	Bucket        string
-	Region        string
-	Endpoint      string
-	CACertificate []byte
-	AccessKey     string
-	SecretKey     string
+	Name               string
+	Bucket             string
+	Region             string
+	Endpoint           string
+	CACertificate      []byte
+	AWSAccessKeyID     []byte
+	AWSSecretAccessKey []byte
 }
 
 // Result represents the result of gathering from an S3 profile.
@@ -189,11 +189,14 @@ func newObjectStore(
 	configOptions := []func(*config.LoadOptions) error{
 		config.WithRegion(profile.Region),
 		config.WithBaseEndpoint(profile.Endpoint),
-		config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
-			profile.AccessKey,
-			profile.SecretKey,
-			"",
-		)),
+		config.WithCredentialsProvider(
+			// AWS credentials are always ASCII text, safe to convert to string.
+			credentials.NewStaticCredentialsProvider(
+				string(profile.AWSAccessKeyID),
+				string(profile.AWSSecretAccessKey),
+				"",
+			),
+		),
 		// Add zap logger to the config to redirect AWS SDK logs.
 		config.WithLogger(awsSDKLogger(log)),
 	}
diff --git a/pkg/testing/mock.go b/pkg/testing/mock.go
index 34d17dd..f09d89d 100644
--- a/pkg/testing/mock.go
+++ b/pkg/testing/mock.go
@@ -4,6 +4,7 @@
 package testing
 
 import (
+	"bytes"
 	"errors"
 
 	"github.com/ramendr/ramen/e2e/types"
@@ -139,8 +140,8 @@ func (m *Mock) GetSecret(
 	}
 	return &corev1.Secret{
 		Data: map[string][]byte{
-			"AWS_ACCESS_KEY_ID":     []byte(helpers.AccessKey),
-			"AWS_SECRET_ACCESS_KEY": []byte(helpers.SecretKey),
+			"AWS_ACCESS_KEY_ID":     []byte(helpers.FakeAWSKeyID),
+			"AWS_SECRET_ACCESS_KEY": []byte(helpers.FakeAWSKey),
 		},
 	}, nil
 }
@@ -157,7 +158,8 @@ func (m *Mock) GatherS3(
 	results := make(chan s3.Result, len(profiles))
 	for _, profile := range profiles {
 		// Fail if s3 secret credentials don't match expected testdata values.
-		if profile.AccessKey != helpers.AccessKey || profile.SecretKey != helpers.SecretKey {
+		if !bytes.Equal(profile.AWSAccessKeyID, []byte(helpers.FakeAWSKeyID)) ||
+			!bytes.Equal(profile.AWSSecretAccessKey, []byte(helpers.FakeAWSKey)) {
 			results <- s3.Result{ProfileName: profile.Name, Err: errors.New("invalid credentials")}
 		} else {
 			results <- s3.Result{ProfileName: profile.Name, Err: nil}
diff --git a/pkg/validate/command_test.go b/pkg/validate/command_test.go
index 051a216..04b73fd 100644
--- a/pkg/validate/command_test.go
+++ b/pkg/validate/command_test.go
@@ -176,8 +176,8 @@ var (
 		GetSecretFunc: func(ctx validation.Context, cluster *types.Cluster, name, namespace string) (*corev1.Secret, error) {
 			return &corev1.Secret{
 				Data: map[string][]byte{
-					"AWS_ACCESS_KEY_ID":     []byte(helpers.WrongAccessKey),
-					"AWS_SECRET_ACCESS_KEY": []byte(helpers.WrongSecretKey),
+					"AWS_ACCESS_KEY_ID":     []byte("invalid id"),
+					"AWS_SECRET_ACCESS_KEY": []byte("invalid key"),
 				},
 			}, nil
 		},
@@ -195,8 +195,8 @@ var (
 		GetSecretFunc: func(ctx validation.Context, cluster *types.Cluster, name, namespace string) (*corev1.Secret, error) {
 			return &corev1.Secret{
 				Data: map[string][]byte{
-					"AWS_ACCESS_KEY_ID":     []byte(helpers.WrongAccessKey),
-					"AWS_SECRET_ACCESS_KEY": []byte(helpers.WrongSecretKey),
+					"AWS_ACCESS_KEY_ID":     []byte("invalid id"),
+					"AWS_SECRET_ACCESS_KEY": []byte("invalid key"),
 				},
 			}, nil
 		},
@@ -930,11 +930,11 @@ func TestValidateClustersK8s(t *testing.T) {
 									},
 									AWSAccessKeyID: report.ValidatedFingerprint{
 										Validated: report.Validated{State: report.OK},
-										Value:     helpers.AccessKeyFingerprint,
+										Value:     helpers.FakeAWSKeyIDFingerprint,
 									},
 									AWSSecretAccessKey: report.ValidatedFingerprint{
 										Validated: report.Validated{State: report.OK},
-										Value:     helpers.SecretKeyFingerprint,
+										Value:     helpers.FakeAWSKeyFingerprint,
 									},
 								},
 								// CACertificate is optional, empty is OK if hub also has no cert.
@@ -970,11 +970,11 @@ func TestValidateClustersK8s(t *testing.T) {
 									},
 									AWSAccessKeyID: report.ValidatedFingerprint{
 										Validated: report.Validated{State: report.OK},
-										Value:     helpers.AccessKeyFingerprint,
+										Value:     helpers.FakeAWSKeyIDFingerprint,
 									},
 									AWSSecretAccessKey: report.ValidatedFingerprint{
 										Validated: report.Validated{State: report.OK},
-										Value:     helpers.SecretKeyFingerprint,
+										Value:     helpers.FakeAWSKeyFingerprint,
 									},
 								},
 								CACertificate: report.ValidatedFingerprint{
@@ -1066,11 +1066,11 @@ func TestValidateClustersK8s(t *testing.T) {
 										},
 										AWSAccessKeyID: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.AccessKeyFingerprint,
+											Value:     helpers.FakeAWSKeyIDFingerprint,
 										},
 										AWSSecretAccessKey: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.SecretKeyFingerprint,
+											Value:     helpers.FakeAWSKeyFingerprint,
 										},
 									},
 									CACertificate: report.ValidatedFingerprint{
@@ -1105,11 +1105,11 @@ func TestValidateClustersK8s(t *testing.T) {
 										},
 										AWSAccessKeyID: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.AccessKeyFingerprint,
+											Value:     helpers.FakeAWSKeyIDFingerprint,
 										},
 										AWSSecretAccessKey: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.SecretKeyFingerprint,
+											Value:     helpers.FakeAWSKeyFingerprint,
 										},
 									},
 									CACertificate: report.ValidatedFingerprint{
@@ -1200,11 +1200,11 @@ func TestValidateClustersK8s(t *testing.T) {
 										},
 										AWSAccessKeyID: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.AccessKeyFingerprint,
+											Value:     helpers.FakeAWSKeyIDFingerprint,
 										},
 										AWSSecretAccessKey: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.SecretKeyFingerprint,
+											Value:     helpers.FakeAWSKeyFingerprint,
 										},
 									},
 									CACertificate: report.ValidatedFingerprint{
@@ -1239,11 +1239,11 @@ func TestValidateClustersK8s(t *testing.T) {
 										},
 										AWSAccessKeyID: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.AccessKeyFingerprint,
+											Value:     helpers.FakeAWSKeyIDFingerprint,
 										},
 										AWSSecretAccessKey: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.SecretKeyFingerprint,
+											Value:     helpers.FakeAWSKeyFingerprint,
 										},
 									},
 									CACertificate: report.ValidatedFingerprint{
@@ -1497,11 +1497,11 @@ func TestValidateClustersOcp(t *testing.T) {
 									},
 									AWSAccessKeyID: report.ValidatedFingerprint{
 										Validated: report.Validated{State: report.OK},
-										Value:     helpers.AccessKeyFingerprint,
+										Value:     helpers.FakeAWSKeyIDFingerprint,
 									},
 									AWSSecretAccessKey: report.ValidatedFingerprint{
 										Validated: report.Validated{State: report.OK},
-										Value:     helpers.SecretKeyFingerprint,
+										Value:     helpers.FakeAWSKeyFingerprint,
 									},
 								},
 								CACertificate: report.ValidatedFingerprint{
@@ -1536,11 +1536,11 @@ func TestValidateClustersOcp(t *testing.T) {
 									},
 									AWSAccessKeyID: report.ValidatedFingerprint{
 										Validated: report.Validated{State: report.OK},
-										Value:     helpers.AccessKeyFingerprint,
+										Value:     helpers.FakeAWSKeyIDFingerprint,
 									},
 									AWSSecretAccessKey: report.ValidatedFingerprint{
 										Validated: report.Validated{State: report.OK},
-										Value:     helpers.SecretKeyFingerprint,
+										Value:     helpers.FakeAWSKeyFingerprint,
 									},
 								},
 								CACertificate: report.ValidatedFingerprint{
@@ -1632,11 +1632,11 @@ func TestValidateClustersOcp(t *testing.T) {
 										},
 										AWSAccessKeyID: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.AccessKeyFingerprint,
+											Value:     helpers.FakeAWSKeyIDFingerprint,
 										},
 										AWSSecretAccessKey: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.SecretKeyFingerprint,
+											Value:     helpers.FakeAWSKeyFingerprint,
 										},
 									},
 									CACertificate: report.ValidatedFingerprint{
@@ -1671,11 +1671,11 @@ func TestValidateClustersOcp(t *testing.T) {
 										},
 										AWSAccessKeyID: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.AccessKeyFingerprint,
+											Value:     helpers.FakeAWSKeyIDFingerprint,
 										},
 										AWSSecretAccessKey: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.SecretKeyFingerprint,
+											Value:     helpers.FakeAWSKeyFingerprint,
 										},
 									},
 									CACertificate: report.ValidatedFingerprint{
@@ -1766,11 +1766,11 @@ func TestValidateClustersOcp(t *testing.T) {
 										},
 										AWSAccessKeyID: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.AccessKeyFingerprint,
+											Value:     helpers.FakeAWSKeyIDFingerprint,
 										},
 										AWSSecretAccessKey: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.SecretKeyFingerprint,
+											Value:     helpers.FakeAWSKeyFingerprint,
 										},
 									},
 									CACertificate: report.ValidatedFingerprint{
@@ -1805,11 +1805,11 @@ func TestValidateClustersOcp(t *testing.T) {
 										},
 										AWSAccessKeyID: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.AccessKeyFingerprint,
+											Value:     helpers.FakeAWSKeyIDFingerprint,
 										},
 										AWSSecretAccessKey: report.ValidatedFingerprint{
 											Validated: report.Validated{State: report.OK},
-											Value:     helpers.SecretKeyFingerprint,
+											Value:     helpers.FakeAWSKeyFingerprint,
 										},
 									},
 									CACertificate: report.ValidatedFingerprint{
diff --git a/pkg/validation/mock.go b/pkg/validation/mock.go
index 04e2474..fb17bee 100644
--- a/pkg/validation/mock.go
+++ b/pkg/validation/mock.go
@@ -4,6 +4,7 @@
 package validation
 
 import (
+	"bytes"
 	"errors"
 
 	"github.com/ramendr/ramen/e2e/types"
@@ -73,8 +74,8 @@ func (m *Mock) GetSecret(
 	}
 	return &corev1.Secret{
 		Data: map[string][]byte{
-			"AWS_ACCESS_KEY_ID":     []byte(helpers.AccessKey),
-			"AWS_SECRET_ACCESS_KEY": []byte(helpers.SecretKey),
+			"AWS_ACCESS_KEY_ID":     []byte(helpers.FakeAWSKeyID),
+			"AWS_SECRET_ACCESS_KEY": []byte(helpers.FakeAWSKey),
 		},
 	}, nil
 }
@@ -91,7 +92,8 @@ func (m *Mock) GatherS3(
 	results := make(chan s3.Result, len(profiles))
 	for _, profile := range profiles {
 		// Fail if s3 secret credentials don't match expected testdata values.
-		if profile.AccessKey != helpers.AccessKey || profile.SecretKey != helpers.SecretKey {
+		if !bytes.Equal(profile.AWSAccessKeyID, []byte(helpers.FakeAWSKeyID)) ||
+			!bytes.Equal(profile.AWSSecretAccessKey, []byte(helpers.FakeAWSKey)) {
 			results <- s3.Result{ProfileName: profile.Name, Err: errors.New("invalid credentials")}
 		} else {
 			results <- s3.Result{ProfileName: profile.Name, Err: nil}
@@ -108,7 +110,8 @@ func (m *Mock) CheckS3(ctx Context, profiles []*s3.Profile) <-chan s3.Result {
 	results := make(chan s3.Result, len(profiles))
 	for _, profile := range profiles {
 		// Fail if s3 secret credentials don't match expected testdata values.
-		if profile.AccessKey != helpers.AccessKey || profile.SecretKey != helpers.SecretKey {
+		if !bytes.Equal(profile.AWSAccessKeyID, []byte(helpers.FakeAWSKeyID)) ||
+			!bytes.Equal(profile.AWSSecretAccessKey, []byte(helpers.FakeAWSKey)) {
 			results <- s3.Result{ProfileName: profile.Name, Err: errors.New("invalid credentials")}
 		} else {
 			results <- s3.Result{ProfileName: profile.Name, Err: nil}

Rename AccessKey and SecretKey to AWSAccessKeyID and AWSSecretAccessKey
to match AWS naming conventions.

Change credentials type from string to []byte to match the K8s secret
data type, avoiding unnecessary conversions. Convert to string only
when passing to the AWS SDK.

Signed-off-by: Parikshith <parikshithb@gmail.com>
Add GetSecret method to fetch S3 secrets from live cluster.
Default mock returns testdata credentials, allowing S3 operations
to validate credentials and fail when missing or invalid.

Signed-off-by: Parikshith <parikshithb@gmail.com>
Rename AccessKeyFingerprint and SecretKeyFingerprint to
FakeAWSKeyIDFingerprint and FakeAWSKeyFingerprint

Signed-off-by: Parikshith <parikshithb@gmail.com>
Move console.Step to start of function and add console.Error
messages for failures. Change log levels from Warn to Error.

Signed-off-by: Parikshith <parikshithb@gmail.com>
With secret sanitization (RamenDR#374), gathered secrets will contain hashes
instead of actual credentials, breaking S3 operations.

- Add GetSecret to backend interfaces to fetch secrets from live cluster
- Update ClusterProfiles to return ramenapi.S3StoreProfile directly
- Add S3ProfileFromStore helper to convert profile with credentials
- Add cancellation support when fetching S3 secrets

Add helper functions for S3 operations:
- gatherApplicationS3Data: inspect and gather, stop only on cancellation
- checkClustersS3: inspect and check, stop only on cancellation

Both helpers return false only on cancellation, allowing validation to
continue on other errors and report them in results.

This ensures S3 operations (GatherS3, CheckS3) have real credentials
even when gathered data contains sanitized secrets.

Assisted-by: Cursor/Claude Opus 4.5
Signed-off-by: Parikshith <parikshithb@gmail.com>
Signed-off-by: Parikshith <parikshithb@gmail.com>
Add tests to verify cancellation handling during S3 profile inspection:
- TestGatherApplicationInspectS3ProfilesCanceled
- TestValidateClustersInspectS3ProfilesCanceled
- TestValidateApplicationInspectS3ProfilesCanceled

Tests verify that when GetSecret is canceled, the command stops
immediately with Canceled status and skips subsequent S3 operations
and validation steps.

Signed-off-by: Parikshith <parikshithb@gmail.com>
Add tests for missing and empty secret scenarios:
- TestGatherApplicationGetSecretFailed
- TestGatherApplicationGetSecretInvalid
- TestValidateClustersGetSecretFailed
- TestValidateClustersGetSecretInvalid
- TestValidateApplicationGetSecretFailed
- TestValidateApplicationGetSecretInvalid

Tests verify that when GetSecret fails or returns invalid data,
S3 operations fail due to missing/invalid credentials.

Signed-off-by: Parikshith <parikshithb@gmail.com>
@nirs nirs merged commit f925e0d into RamenDR:main Feb 17, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Read secrets from live hub cluster for s3 operations

2 participants