🧪 Add e2e test for migrate namespace with multiple secret types#635
🧪 Add e2e test for migrate namespace with multiple secret types#635Tamar-Dinavetsky wants to merge 5 commits into
Conversation
|
Warning Review limit reached
Next review available in: 20 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds a reusable Kubernetes Secret verification helper and a tier1 end-to-end test covering creation, migration, application, and validation of Opaque, TLS, and docker-registry Secrets. ChangesSecret migration validation
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related issues
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Test Coverage ReportTotal: 47.8% Per-package coverage
Full function-level detailsPosted by CI |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
e2e-tests/framework/test_helpers.go (1)
99-112: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
unstructured.Unstructuredfor this secret check Replace the manualmap[string]anydecode and type assertions withunstructured.Unstructuredplus the nested access helpers; it’s the idiomatic shape for dynamic Kubernetes objects and keeps the checks safer and clearer.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@e2e-tests/framework/test_helpers.go` around lines 99 - 112, Update the secret validation logic around the JSON decode to use an unstructured.Unstructured object instead of map[string]any. Reuse its nested access helpers to read and validate the type field and data contents, preserving the existing error behavior for invalid JSON, missing or mismatched types, and empty data.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@e2e-tests/framework/test_helpers.go`:
- Around line 93-118: Update VerifySecret so every error message includes the
Kubernetes secret namespace and secret name. For the type assertion and
expected-type mismatch checks, report the actual received value/type explicitly,
including when the field is missing or not a string, while preserving the
existing validation behavior and wrapped command/JSON errors.
---
Nitpick comments:
In `@e2e-tests/framework/test_helpers.go`:
- Around line 99-112: Update the secret validation logic around the JSON decode
to use an unstructured.Unstructured object instead of map[string]any. Reuse its
nested access helpers to read and validate the type field and data contents,
preserving the existing error behavior for invalid JSON, missing or mismatched
types, and empty data.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 65aef3e4-7fde-482e-bbdf-47774977aaa1
📒 Files selected for processing (2)
e2e-tests/framework/test_helpers.goe2e-tests/tests/tier1/mta_842_secrets_migration_test.go
|
Tested on OCP successfully. |
|
/rfr |
|
|
@istein1 fixed , thanks for the review. |
| // Fetches a secret by name from the given namespace and cluster, parses the JSON response, and verifies the type | ||
| // field matches the expected value. Returns a descriptive error if the secret cannot be fetched, the JSON cannot | ||
| // be parsed, or the type does not match. | ||
| func VerifySecret(kubectl KubectlRunner, namespace, secretName, expectedType string) error { | ||
| secretJson, err := kubectl.Run("get", "secret", secretName, "-n", namespace, "-o", "json") | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get secret %s: %w", secretName, err) | ||
| } | ||
|
|
||
| var secretObj map[string]any | ||
| if err := json.Unmarshal([]byte(secretJson), &secretObj); err != nil { | ||
| return fmt.Errorf("failed to parse secret %s JSON: %w", secretName, err) | ||
| } | ||
|
|
||
| actualType, ok := secretObj["type"].(string) | ||
| if !ok { | ||
| return fmt.Errorf("secret %s/%s: type field is not a string, got %T", namespace, secretName, secretObj["type"]) | ||
| } | ||
| if actualType != expectedType { | ||
| return fmt.Errorf("secret %s/%s: expected type %q but got %q", namespace, secretName, expectedType, actualType) | ||
| } | ||
| data, ok := secretObj["data"].(map[string]any) | ||
| if !ok || len(data) == 0 { | ||
| return fmt.Errorf("secret %s/%s: data field is missing or empty, got %T", namespace, secretName, secretObj["data"]) | ||
| } | ||
|
|
||
| log.Printf("Secret verified: name=%s type=%s\n", secretName, actualType) | ||
| return nil | ||
| } |
There was a problem hiding this comment.
Nice helper. One gap: it only checks data is non-empty, not that the content actually matches the source secret, a base64 re-encoding or truncation bug would slip through silently. The PVC tests in this suite (MTA-806/807) do full checksum comparison for the same reason; suggest doing the same here since secrets are even cheaper to compare exactly.
| // Fetches a secret by name from the given namespace and cluster, parses the JSON response, and verifies the type | |
| // field matches the expected value. Returns a descriptive error if the secret cannot be fetched, the JSON cannot | |
| // be parsed, or the type does not match. | |
| func VerifySecret(kubectl KubectlRunner, namespace, secretName, expectedType string) error { | |
| secretJson, err := kubectl.Run("get", "secret", secretName, "-n", namespace, "-o", "json") | |
| if err != nil { | |
| return fmt.Errorf("failed to get secret %s: %w", secretName, err) | |
| } | |
| var secretObj map[string]any | |
| if err := json.Unmarshal([]byte(secretJson), &secretObj); err != nil { | |
| return fmt.Errorf("failed to parse secret %s JSON: %w", secretName, err) | |
| } | |
| actualType, ok := secretObj["type"].(string) | |
| if !ok { | |
| return fmt.Errorf("secret %s/%s: type field is not a string, got %T", namespace, secretName, secretObj["type"]) | |
| } | |
| if actualType != expectedType { | |
| return fmt.Errorf("secret %s/%s: expected type %q but got %q", namespace, secretName, expectedType, actualType) | |
| } | |
| data, ok := secretObj["data"].(map[string]any) | |
| if !ok || len(data) == 0 { | |
| return fmt.Errorf("secret %s/%s: data field is missing or empty, got %T", namespace, secretName, secretObj["data"]) | |
| } | |
| log.Printf("Secret verified: name=%s type=%s\n", secretName, actualType) | |
| return nil | |
| } | |
| // Fetches a secret by name from the given namespace and cluster, parses the JSON response, and verifies the type | |
| // field matches the expected value and, when expectedData is non-nil, that every key's base64-encoded value | |
| // matches exactly. Returns a descriptive error if the secret cannot be fetched, the JSON cannot be parsed, the | |
| // type does not match, or any data key is missing/mismatched. | |
| func VerifySecret(kubectl KubectlRunner, namespace, secretName, expectedType string, expectedData map[string]string) error { | |
| secretJson, err := kubectl.Run("get", "secret", secretName, "-n", namespace, "-o", "json") | |
| if err != nil { | |
| return fmt.Errorf("failed to get secret %s: %w", secretName, err) | |
| } | |
| var secretObj map[string]any | |
| if err := json.Unmarshal([]byte(secretJson), &secretObj); err != nil { | |
| return fmt.Errorf("failed to parse secret %s JSON: %w", secretName, err) | |
| } | |
| actualType, ok := secretObj["type"].(string) | |
| if !ok { | |
| return fmt.Errorf("secret %s/%s: type field is not a string, got %T", namespace, secretName, secretObj["type"]) | |
| } | |
| if actualType != expectedType { | |
| return fmt.Errorf("secret %s/%s: expected type %q but got %q", namespace, secretName, expectedType, actualType) | |
| } | |
| data, ok := secretObj["data"].(map[string]any) | |
| if !ok || len(data) == 0 { | |
| return fmt.Errorf("secret %s/%s: data field is missing or empty, got %T", namespace, secretName, secretObj["data"]) | |
| } | |
| for key, expectedValue := range expectedData { | |
| actualValue, ok := data[key].(string) | |
| if !ok { | |
| return fmt.Errorf("secret %s/%s: expected data key %q not found or not a string", namespace, secretName, key) | |
| } | |
| if actualValue != expectedValue { | |
| return fmt.Errorf("secret %s/%s: data key %q does not match source value", namespace, secretName, key) | |
| } | |
| } | |
| log.Printf("Secret verified: name=%s type=%s\n", secretName, actualType) | |
| return nil | |
| } | |
| // GetSecretData fetches a secret by name and returns its data map with values left base64-encoded, | |
| // exactly as returned by the Kubernetes API, suitable for direct comparison via VerifySecret's | |
| // expectedData parameter without needing to decode/re-encode. | |
| func GetSecretData(kubectl KubectlRunner, namespace, secretName string) (map[string]string, error) { | |
| secretJson, err := kubectl.Run("get", "secret", secretName, "-n", namespace, "-o", "json") | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to get secret %s: %w", secretName, err) | |
| } | |
| var secretObj struct { | |
| Data map[string]string `json:"data"` | |
| } | |
| if err := json.Unmarshal([]byte(secretJson), &secretObj); err != nil { | |
| return nil, fmt.Errorf("failed to parse secret %s JSON: %w", secretName, err) | |
| } | |
| return secretObj.Data, nil | |
| } |
| By("Create docker-registry secret on source") | ||
| _, err = kubectlSrc.Run("create", "secret", "docker-registry", "docker-secret", "--docker-server=quay.io", "--docker-username=user", "--docker-password=pass", "-n", namespace) | ||
| Expect(err).NotTo(HaveOccurred()) | ||
|
|
There was a problem hiding this comment.
Capture each secret's source data before the pipeline runs, so it can be compared exactly on target below.
| By("Capture source secret data for exact post-migration comparison") | |
| opaqueData, err := GetSecretData(kubectlSrc, namespace, "opaque-secret") | |
| Expect(err).NotTo(HaveOccurred()) | |
| tlsData, err := GetSecretData(kubectlSrc, namespace, "tls-secret") | |
| Expect(err).NotTo(HaveOccurred()) | |
| dockerData, err := GetSecretData(kubectlSrc, namespace, "docker-secret") | |
| Expect(err).NotTo(HaveOccurred()) | |
|
|
||
| By("Create docker-registry secret on source") | ||
| _, err = kubectlSrc.Run("create", "secret", "docker-registry", "docker-secret", "--docker-server=quay.io", "--docker-username=user", "--docker-password=pass", "-n", namespace) | ||
| Expect(err).NotTo(HaveOccurred()) |
There was a problem hiding this comment.
While reviewing, dug into how crane actually handles ServiceAccount token secrets: it only whites out the default SA's token secret (via the kubernetes.io/service-account.name: default annotation, gated by StripDefaultRBAC), not by .type == kubernetes.io/service-account-token. So a token secret for a custom SA currently migrates as-is, token data included, which is stale/meaningless on the target. That behavior has zero test coverage today. Not asking to block this PR on it, but worth a follow-up case here or a separate test, e.g.:
By("Verify default SA token secret is excluded from output")
matches, _ := filepath.Glob(filepath.Join(paths.OutputDir, "resources", namespace, "Secret__*default-token*"))
Expect(matches).To(BeEmpty(), "default SA token secret should be whited out")9e249a8 to
0551e66
Compare
Summary
multiple types (Opaque, TLS, dockerconfigjson)
types intact
cluster
Test plan
Framework changes
verifies type and data fields
Polarion: MTA-842
#502
Summary by CodeRabbit