Skip to content

Commit 8981f72

Browse files
feat: Use single catalog across tests(#984)
* Update to support single catalog for testing * fix multiple offerings * fix names * update to handle single instances of catalog, projects * Enhance RunAddonTest to improve addon resolution and input validation
1 parent 461e50e commit 8981f72

File tree

14 files changed

+2979
-445
lines changed

14 files changed

+2979
-445
lines changed

.secrets.baseline

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "go.sum|package-lock.json|^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2025-06-19T09:49:54Z",
6+
"generated_at": "2025-07-01T11:38:17Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -286,7 +286,7 @@
286286
"hashed_secret": "11fa7c37d697f30e6aee828b4426a10f83ab2380",
287287
"is_secret": false,
288288
"is_verified": false,
289-
"line_number": 287,
289+
"line_number": 520,
290290
"type": "Secret Keyword",
291291
"verified_result": null
292292
}

cloudinfo/catalog.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -505,8 +505,10 @@ func buildHierarchicalDeploymentList(mainAddon *AddonConfig) []AddonConfig {
505505
return fmt.Sprintf("%s|%s|%s", addon.CatalogID, addon.OfferingID, addon.OfferingFlavor)
506506
}
507507

508-
// Always add the main addon first with its generated config name
509-
mainAddon.ConfigName = fmt.Sprintf("%s-%s", mainAddon.Prefix, mainAddon.OfferingName)
508+
// Always add the main addon first with its config name (generate if not set)
509+
if mainAddon.ConfigName == "" {
510+
mainAddon.ConfigName = fmt.Sprintf("%s-%s", mainAddon.Prefix, mainAddon.OfferingName)
511+
}
510512
deploymentList = append(deploymentList, *mainAddon)
511513
processedOfferings[getOfferingKey(mainAddon)] = true
512514

@@ -519,9 +521,11 @@ func buildHierarchicalDeploymentList(mainAddon *AddonConfig) []AddonConfig {
519521

520522
// Only process enabled dependencies that haven't been seen before (by offering identity)
521523
if dep.Enabled != nil && *dep.Enabled && !processedOfferings[offeringKey] {
522-
// Generate a unique config name for this dependency
523-
randomPostfix := strings.ToLower(random.UniqueId())
524-
dep.ConfigName = fmt.Sprintf("%s-%s", dep.OfferingName, randomPostfix)
524+
// Generate a unique config name for this dependency if not already set
525+
if dep.ConfigName == "" {
526+
randomPostfix := strings.ToLower(random.UniqueId())
527+
dep.ConfigName = fmt.Sprintf("%s-%s", dep.OfferingName, randomPostfix)
528+
}
525529

526530
// Add to deployment list and mark as processed
527531
deploymentList = append(deploymentList, dep)

common/git.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,3 +592,29 @@ func changesToBePush(testing *testing.T, repoDir string, git gitOps) (bool, []st
592592

593593
return hasUncommittedChanges || hasUnpushedCommits, allChangedFiles, nil
594594
}
595+
596+
// CheckRemoteBranchExists checks if a specific branch exists in a remote Git repository
597+
// repoURL: the HTTPS URL of the repository (e.g., "https://github.com/user/repo")
598+
// branchName: the name of the branch to check (e.g., "main", "feature-branch")
599+
// Returns true if the branch exists, false otherwise, and an error if the check fails
600+
func CheckRemoteBranchExists(repoURL, branchName string) (bool, error) {
601+
if repoURL == "" || branchName == "" {
602+
return false, fmt.Errorf("repository URL and branch name must not be empty")
603+
}
604+
605+
// Use git ls-remote to check if the branch exists without cloning the repo
606+
cmd := exec.Command("git", "ls-remote", "--heads", repoURL, branchName)
607+
output, err := cmd.Output()
608+
if err != nil {
609+
// Check if it's a repository access error
610+
if exitError, ok := err.(*exec.ExitError); ok {
611+
return false, fmt.Errorf("failed to access repository '%s': %s", repoURL, string(exitError.Stderr))
612+
}
613+
return false, fmt.Errorf("failed to check remote branch: %w", err)
614+
}
615+
616+
// If output is empty, the branch doesn't exist
617+
// If output has content, the branch exists (git ls-remote returns "commit_hash refs/heads/branch_name")
618+
result := strings.TrimSpace(string(output))
619+
return result != "", nil
620+
}

docs/projects/addons/configuration.md

Lines changed: 294 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,180 @@ options.CatalogUseExisting = true
100100
options.CatalogName = "existing-catalog-name" // Required when using existing
101101
```
102102

103+
### Catalog Sharing Control
104+
105+
The `SharedCatalog` option controls catalog and offering sharing behavior:
106+
107+
```golang
108+
options := testaddons.TestAddonsOptionsDefault(&testaddons.TestAddonOptions{
109+
Testing: t,
110+
Prefix: "test",
111+
ResourceGroup: "my-rg",
112+
SharedCatalog: core.BoolPtr(false), // Default: false for individual tests
113+
})
114+
```
115+
116+
**SharedCatalog Settings:**
117+
118+
- `false` (default): Each test creates its own catalog and offering for complete isolation and automatic cleanup
119+
- `true`: Catalogs and offerings are shared across tests using the same `TestOptions` object (requires manual cleanup)
120+
121+
**Sharing Behavior:**
122+
123+
```golang
124+
// SharedCatalog = false (default) - isolated tests with automatic cleanup
125+
isolatedOptions := testaddons.TestAddonsOptionsDefault(&testaddons.TestAddonOptions{
126+
Testing: t,
127+
Prefix: "isolated-test",
128+
ResourceGroup: "my-rg",
129+
SharedCatalog: core.BoolPtr(false), // Can be omitted as it's the default
130+
})
131+
132+
// Each test creates and cleans up its own catalog + offering
133+
err1 := isolatedOptions.RunAddonTest() // Creates & deletes catalog A
134+
err2 := isolatedOptions.RunAddonTest() // Creates & deletes catalog B
135+
136+
// SharedCatalog = true - efficient sharing (requires manual cleanup)
137+
baseOptions := testaddons.TestAddonsOptionsDefault(&testaddons.TestAddonOptions{
138+
Testing: t,
139+
Prefix: "shared-test",
140+
ResourceGroup: "my-rg",
141+
SharedCatalog: core.BoolPtr(true),
142+
})
143+
144+
// First test creates catalog + offering, second test reuses them
145+
err3 := baseOptions.RunAddonTest() // Creates catalog
146+
err4 := baseOptions.RunAddonTest() // Reuses catalog (manual cleanup needed)
147+
```
148+
149+
### Automatic Catalog Sharing (Matrix Tests)
150+
151+
When using matrix testing with `RunAddonTestMatrix()`, catalogs and offerings are automatically shared across all test cases for improved efficiency:
152+
153+
```golang
154+
// Matrix tests automatically share catalogs - no additional configuration needed
155+
baseOptions := testaddons.TestAddonsOptionsDefault(&testaddons.TestAddonOptions{
156+
Testing: t,
157+
Prefix: "matrix-test",
158+
ResourceGroup: "my-resource-group",
159+
})
160+
161+
baseOptions.RunAddonTestMatrix(matrix) // Catalog automatically shared across all test cases
162+
```
163+
164+
**Benefits:**
165+
166+
- **Resource Efficiency**: Creates only 1 catalog for all test cases instead of N catalogs
167+
- **Time Savings**: Reduced catalog creation time and API calls
168+
- **Automatic Cleanup**: Shared resources cleaned up after all matrix tests complete
169+
170+
**Individual vs Matrix Tests:**
171+
172+
- **Individual Tests**: Respect the `SharedCatalog` setting (default: false - not shared)
173+
- **Matrix Tests**: Always share catalogs regardless of `SharedCatalog` setting
174+
175+
### Catalog Cleanup Behavior
176+
177+
Understanding when catalogs are cleaned up is important for resource management:
178+
179+
**Matrix Tests (RunAddonTestMatrix):**
180+
181+
- Catalogs are automatically cleaned up after all test cases complete
182+
- Uses Go's `t.Cleanup()` mechanism to ensure cleanup happens
183+
184+
**Individual Tests with SharedCatalog=false (default):**
185+
186+
- Each test creates and deletes its own catalog
187+
- Automatic cleanup with guaranteed isolation
188+
- Use for most individual tests and when isolation is important
189+
190+
**Individual Tests with SharedCatalog=true:**
191+
192+
- Catalogs are shared and persist after test completion
193+
- Efficient for development workflows and sequential test runs
194+
- **Manual cleanup required** - catalogs will persist until manually deleted
195+
196+
**Best Practices:**
197+
198+
```golang
199+
// For most tests - automatic cleanup with isolation (recommended)
200+
options.SharedCatalog = core.BoolPtr(false) // Default
201+
202+
// For development and sequential tests - efficient sharing
203+
options.SharedCatalog = core.BoolPtr(true) // Manual cleanup required
204+
205+
// For matrix tests - automatic sharing and cleanup (recommended)
206+
baseOptions.RunAddonTestMatrix(matrix)
207+
```
208+
209+
**When to use each approach:**
210+
211+
- **SharedCatalog=false**: Most individual tests, CI pipelines, when automatic cleanup is needed
212+
- **SharedCatalog=true**: Development workflows, sequential tests with same prefix
213+
- **Matrix tests**: Multiple test cases with variations (automatic sharing + cleanup)
214+
215+
### Manual Cleanup for Shared Catalogs
216+
217+
When using `SharedCatalog=true` with individual tests, you can manually clean up shared resources using `CleanupSharedResources()`:
218+
219+
```golang
220+
func TestMultipleAddonsWithSharedCatalog(t *testing.T) {
221+
baseOptions := testaddons.TestAddonsOptionsDefault(&testaddons.TestAddonOptions{
222+
Testing: t,
223+
Prefix: "shared-test",
224+
ResourceGroup: "my-resource-group",
225+
SharedCatalog: core.BoolPtr(true), // Enable sharing
226+
})
227+
228+
// Ensure cleanup happens at the end
229+
defer baseOptions.CleanupSharedResources()
230+
231+
// Run multiple tests that share the catalog
232+
t.Run("TestScenario1", func(t *testing.T) {
233+
options1 := baseOptions
234+
options1.AddonConfig = cloudinfo.NewAddonConfigTerraform(/* config */)
235+
err := options1.RunAddonTest()
236+
require.NoError(t, err)
237+
})
238+
239+
t.Run("TestScenario2", func(t *testing.T) {
240+
options2 := baseOptions
241+
options2.AddonConfig = cloudinfo.NewAddonConfigTerraform(/* different config */)
242+
err := options2.RunAddonTest()
243+
require.NoError(t, err)
244+
})
245+
246+
// CleanupSharedResources() called automatically via defer
247+
}
248+
```
249+
250+
**Benefits of manual cleanup:**
251+
252+
- Guaranteed resource cleanup regardless of test failures
253+
- Works with any number of individual test variations
254+
- Simple defer pattern ensures cleanup runs even if tests panic
255+
256+
#### Alternative: Cleanup in TestMain
257+
258+
For package-level cleanup across multiple test functions:
259+
260+
```golang
261+
func TestMain(m *testing.M) {
262+
// Setup shared options if needed
263+
sharedOptions := setupSharedOptions()
264+
265+
// Run tests
266+
code := m.Run()
267+
268+
// Cleanup shared resources
269+
if sharedOptions != nil {
270+
sharedOptions.CleanupSharedResources()
271+
}
272+
273+
os.Exit(code)
274+
}
275+
```
276+
103277
## Addon Configuration
104278

105279
### Terraform Addon (Primary Use Case)
@@ -265,18 +439,77 @@ options.LocalChangesIgnorePattern = []string{
265439
### Validation Error Output
266440

267441
```golang
268-
// Show detailed individual error messages
442+
// Enhanced dependency tree visualization with validation status
443+
options.EnhancedTreeValidationOutput = true
444+
445+
// Detailed error messages for validation failures
269446
options.VerboseValidationErrors = true
447+
```
270448

271-
// Show dependency tree with validation status
272-
options.EnhancedTreeValidationOutput = true
449+
### Input Validation Retry Configuration
450+
451+
```golang
452+
// Configure retry behavior for input validation (handles database timing issues)
453+
options.InputValidationRetries = 5 // Default: 3 retries
454+
options.InputValidationRetryDelay = 3 * time.Second // Default: 2 seconds
273455
```
274456

275-
**Validation output priority:**
457+
**Note:** Input validation includes automatic retry logic to handle cases where the backend database hasn't been updated yet after configuration changes. This prevents false failures due to timing issues between configuration updates and validation checks.
458+
459+
### Enhanced Debug Output
460+
461+
When input validation fails, the framework automatically provides detailed debug information to help diagnose issues:
462+
463+
**Debug Information Includes:**
464+
465+
- Current configuration state and inputs (with sensitive values redacted)
466+
- All configurations in the project and their current state
467+
- Expected addon configuration details
468+
- Required input validation attempts and results
469+
- Clear identification of configurations in "waiting on inputs" state
470+
471+
**Debug Output Triggers:**
472+
473+
- Missing required inputs detected during validation
474+
- Configurations found in "awaiting_input" state (timing issues)
475+
- Configuration matching failures during validation
476+
477+
**State Detection Improvements:**
478+
479+
The framework now correctly identifies only configurations that are truly in the `awaiting_input` state, avoiding false positives for configurations in other valid states like `awaiting_member_deployment` or `awaiting_validation`.
480+
481+
**Example Debug Output:**
276482

277-
1. **Enhanced Tree Output**: Visual dependency tree with status indicators
278-
2. **Verbose Mode**: Detailed individual error messages
279-
3. **Consolidated Summary** (default): Clean grouped error summary
483+
```text
484+
=== INPUT VALIDATION FAILURE DEBUG INFO ===
485+
Found 2 configurations in project:
486+
Config: my-kms-config (ID: abc123) [IN WAITING LIST]
487+
State: awaiting_input
488+
StateCode: awaiting_input
489+
LocatorID: catalog.def456.version.ghi789
490+
Current Inputs:
491+
region: us-south
492+
prefix: test-prefix
493+
ibmcloud_api_key: [REDACTED]
494+
key_protect_plan: (missing)
495+
496+
Config: my-base-config (ID: def456)
497+
State: awaiting_member_deployment
498+
StateCode: awaiting_member_deployment
499+
LocatorID: catalog.abc123.version.xyz789
500+
Current Inputs:
501+
region: us-south
502+
prefix: test-prefix
503+
504+
Expected addon configuration details:
505+
Main Addon Name: deploy-arch-ibm-kms
506+
Main Addon Version: v1.2.3
507+
Main Addon Config Name: my-kms-config
508+
Prefix: test-prefix
509+
=== END DEBUG INFO ===
510+
```
511+
512+
This debug output helps identify exactly which inputs are missing, configuration state issues, and timing problems with the backend systems. Configurations marked with `[IN WAITING LIST]` are those actually flagged as requiring input attention.
280513

281514
## Environment Variables
282515

@@ -475,3 +708,57 @@ func TestComprehensiveAddon(t *testing.T) {
475708
assert.NoError(t, err)
476709
}
477710
```
711+
712+
## Project Management
713+
714+
### Project Creation and Isolation
715+
716+
The framework always creates temporary projects for each test to ensure complete isolation. Each test gets its own dedicated project for maximum safety and ease of debugging:
717+
718+
```golang
719+
options := testaddons.TestAddonsOptionsDefault(&testaddons.TestAddonOptions{
720+
Testing: t,
721+
Prefix: "test",
722+
ResourceGroup: "my-rg",
723+
})
724+
```
725+
726+
**Project Behavior:**
727+
728+
Each test automatically:
729+
730+
1. Creates a new temporary project with a unique name
731+
2. Deploys the addon configuration within that project
732+
3. Runs validation tests
733+
4. Cleans up the project and all resources
734+
735+
```golang
736+
options := testaddons.TestAddonsOptionsDefault(&testaddons.TestAddonOptions{
737+
Testing: t,
738+
Prefix: "isolated-test",
739+
ResourceGroup: "my-rg",
740+
})
741+
742+
// Each test creates and cleans up its own temporary project
743+
err1 := options.RunAddonTest() // Creates project A, runs test, deletes project A
744+
err2 := options.RunAddonTest() // Creates project B, runs test, deletes project B
745+
```
746+
747+
**Benefits of Project Isolation:**
748+
749+
- **Complete isolation**: Tests cannot interfere with each other
750+
- **Reliable cleanup**: Each test cleans up its own resources
751+
- **Easier debugging**: Failed tests don't affect other tests
752+
- **CI/CD friendly**: Safe for parallel execution in pipelines
753+
754+
**Resource Sharing Options:**
755+
756+
While projects are always isolated, catalogs can optionally be shared for efficiency:
757+
758+
```golang
759+
// Default: each test creates its own catalog and project
760+
options.SharedCatalog = core.BoolPtr(false) // Each test creates own catalog
761+
762+
// Efficient: share catalogs between tests (projects still isolated)
763+
options.SharedCatalog = core.BoolPtr(true) // Share catalogs between tests
764+
```

0 commit comments

Comments
 (0)