Skip to content

Commit 38a834e

Browse files
chore(matrix): derive CI scenario validation from filesystem
Eliminates the four coupling points called out in #6340 between data under charts/<version>/test/ and the deploy-camunda Go CLI: adding or removing a scenario, persistence layer, fixture, or pre-install script no longer forces a CLI edit + rebuild. Changes: - scenarios.Validate(scenariosDir) discovers valid identity/persistence/ platform/feature names from values/<sub>/*.yaml at call time, replacing three hardcoded slices that had already drifted from the filesystem. Per-version: a name accepted for 8.10 may now be rejected for 8.7 if the corresponding values file doesn't exist there. - BuildDeploymentConfig takes scenariosDir and forwards it to Validate. Four production call sites already had the path on hand and pass it through. - Shell completions and flag help text in cmd/{root,prepare_values}.go no longer carry duplicate hardcoded name lists; they discover via scenarios.List* + a chart-scan fallback. - The orphan-file allowlist moves out of Go (lifecycle_allowlist.go, deleted) and into per-chart .orphan-allowlist.yaml under test/integration/scenarios/. Loader: matrix.LoadOrphanAllowlist. Adding an exempt file is a YAML edit + a one-line reason; no Go rebuild required. RegistryValidator and TestLifecycleFixtures both consume the loader and additionally assert that every allowlist entry matches an existing file (dead-entry guard). - TestGenerate_PropagatesPreInstall no longer pins the rdbms scenario; it iterates every scenario whose config declares a pre-install hook and asserts the fixtures/script payload propagates to the Generate'd entry. - TestLifecycleFixtures iterates chart-versions.yaml's ActiveVersions() (alpha + supportStandard) rather than every directory on disk; supportExtended series (8.3-8.6) and EOL series are skipped because they are not in the active CI matrix. Closes #6340
1 parent 391e80e commit 38a834e

17 files changed

Lines changed: 440 additions & 287 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Files in pre-setup-scripts/ and common/resources/ permitted to exist
2+
# without being referenced by any LifecycleHook in test/ci-test-config.yaml.
3+
# Loaded by scripts/deploy-camunda/matrix.LoadOrphanAllowlist.
4+
#
5+
# Adding an entry requires a one-line reason — reviewers should be able to
6+
# tell from a YAML diff alone why the exemption exists.
7+
pre-setup-scripts:
8+
- name: pre-install-upgrade.sh
9+
reason: sed-target marker for values uncommenting (alpha8 backcompat); not invoked by the matrix runner.
10+
- name: create-opensearch-tls-secrets.sh
11+
reason: helper sourced by pre-install-opensearch-self-signed*.sh; never invoked by the runner directly.
12+
common-resources:
13+
- name: postgres-createdb-job.yaml
14+
reason: fixture staged for the disabled rdbms-external scenario; kept to avoid a separate re-add PR when it is enabled.
15+
- name: postgresql-cluster-tls.yaml
16+
reason: applied by pre-install-rdbms-self-signed.sh via envsubst+kubectl, not via the runner's declarative fixtures pipeline.
17+
- name: gateway-proxy-settings.yaml
18+
reason: applied by post-deploy-gateway-keycloak.sh via envsubst+kubectl, not via the runner's declarative fixtures pipeline.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Files in pre-setup-scripts/ and common/resources/ permitted to exist
2+
# without being referenced by any LifecycleHook in test/ci-test-config.yaml.
3+
# Loaded by scripts/deploy-camunda/matrix.LoadOrphanAllowlist.
4+
#
5+
# Adding an entry requires a one-line reason — reviewers should be able to
6+
# tell from a YAML diff alone why the exemption exists.
7+
pre-setup-scripts:
8+
- name: pre-install-upgrade.sh
9+
reason: sed-target marker for values uncommenting (alpha8 backcompat); not invoked by the matrix runner.
10+
- name: create-elasticsearch-tls-secrets.sh
11+
reason: helper sourced by pre-install-elasticsearch-self-signed*.sh; never invoked by the runner directly.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Files in pre-setup-scripts/ and common/resources/ permitted to exist
2+
# without being referenced by any LifecycleHook in test/ci-test-config.yaml.
3+
# Loaded by scripts/deploy-camunda/matrix.LoadOrphanAllowlist.
4+
#
5+
# Adding an entry requires a one-line reason — reviewers should be able to
6+
# tell from a YAML diff alone why the exemption exists.
7+
pre-setup-scripts:
8+
- name: pre-install-upgrade.sh
9+
reason: sed-target marker for values uncommenting (alpha8 backcompat); not invoked by the matrix runner.
10+
- name: create-elasticsearch-tls-secrets.sh
11+
reason: helper sourced by pre-install-elasticsearch-self-signed-upgrade.sh; never invoked by the runner directly.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Files in pre-setup-scripts/ and common/resources/ permitted to exist
2+
# without being referenced by any LifecycleHook in test/ci-test-config.yaml.
3+
# Loaded by scripts/deploy-camunda/matrix.LoadOrphanAllowlist.
4+
#
5+
# Adding an entry requires a one-line reason — reviewers should be able to
6+
# tell from a YAML diff alone why the exemption exists.
7+
pre-setup-scripts:
8+
- name: pre-install-upgrade.sh
9+
reason: sed-target marker for values uncommenting (alpha8 backcompat); not invoked by the matrix runner.
10+
- name: create-elasticsearch-tls-secrets.sh
11+
reason: helper sourced by pre-install-elasticsearch-self-signed.sh; never invoked by the runner directly.
12+
common-resources:
13+
- name: postgres-createdb-job.yaml
14+
reason: fixture staged for the disabled rdbms-external scenario; kept to avoid a separate re-add PR when it is enabled.

scripts/camunda-core/pkg/scenarios/scenarios.go

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -50,42 +50,39 @@ type DeploymentConfig struct {
5050
Flow string // Deployment flow: install, upgrade-patch, upgrade-minor
5151
}
5252

53-
// Validate checks that required fields are set and feature constraints are satisfied.
54-
func (c *DeploymentConfig) Validate() error {
53+
// Validate checks that required fields are set and feature constraints are
54+
// satisfied. The set of valid identity/persistence/platform/feature names is
55+
// discovered from the scenariosDir at call time — adding values/<dir>/foo.yaml
56+
// in a chart is sufficient to make `foo` a valid selection for that version,
57+
// with no code change required. A name accepted by one chart version may be
58+
// rejected by another if the corresponding values file does not exist there.
59+
func (c *DeploymentConfig) Validate(scenariosDir string) error {
5560
if c.Identity == "" {
56-
return errors.New("--identity is required (keycloak, oidc, auth0, basic, hybrid)")
61+
return errors.New("--identity is required")
5762
}
5863
if c.Persistence == "" {
59-
return errors.New("--persistence is required (elasticsearch, opensearch, rdbms, rdbms-external, rdbms-oracle)")
64+
return errors.New("--persistence is required")
6065
}
6166
if c.Platform == "" {
62-
return errors.New("--platform is required (gke, eks, openshift)")
67+
return errors.New("--platform is required")
6368
}
64-
65-
// Validate identity values
66-
validIdentities := []string{"keycloak", "oidc", "auth0", "basic", "hybrid"}
67-
if !contains(validIdentities, c.Identity) {
68-
return fmt.Errorf("invalid --identity value %q: must be one of: %s", c.Identity, strings.Join(validIdentities, ", "))
69+
if scenariosDir == "" {
70+
return errors.New("scenariosDir is required for validation")
6971
}
7072

71-
// Validate persistence values.
72-
// Note: this list is global across chart versions. Some values resolve to
73-
// values files that exist only in 8.10+ — `elasticsearch-self-signed`,
74-
// `opensearch-self-signed`, `opensearch-self-signed-os-trust`,
75-
// `rdbms-self-signed`. Selecting one of
76-
// these against an older chart version passes Validate() but produces no
77-
// persistence layer (ResolvePaths skips missing files), so the deploy
78-
// proceeds with no TLS wiring. Treated as an 8.10-only scope intentionally;
79-
// when 8.10 becomes the only supported series this comment can be removed.
80-
validPersistence := []string{"elasticsearch", "elasticsearch-self-signed", "no-elasticsearch", "opensearch", "opensearch-embedded", "opensearch-self-signed", "opensearch-self-signed-os-trust", "rdbms", "rdbms-external", "rdbms-oracle", "rdbms-self-signed"}
81-
if !contains(validPersistence, c.Persistence) {
82-
return fmt.Errorf("invalid --persistence value %q: must be one of: %s", c.Persistence, strings.Join(validPersistence, ", "))
73+
if err := validateAgainstDir(c.Identity, scenariosDir, IdentityDir, "--identity"); err != nil {
74+
return err
8375
}
84-
85-
// Validate platform values
86-
validPlatforms := []string{"gke", "eks", "openshift"}
87-
if !contains(validPlatforms, c.Platform) {
88-
return fmt.Errorf("invalid --platform value %q: must be one of: %s", c.Platform, strings.Join(validPlatforms, ", "))
76+
if err := validateAgainstDir(c.Persistence, scenariosDir, PersistenceDir, "--persistence"); err != nil {
77+
return err
78+
}
79+
if err := validateAgainstDir(c.Platform, scenariosDir, PlatformDir, "--platform"); err != nil {
80+
return err
81+
}
82+
for _, feature := range c.Features {
83+
if err := validateAgainstDir(feature, scenariosDir, FeaturesDir, "--features"); err != nil {
84+
return err
85+
}
8986
}
9087

9188
// Feature constraints
@@ -96,6 +93,28 @@ func (c *DeploymentConfig) Validate() error {
9693
return nil
9794
}
9895

96+
// validateAgainstDir confirms that <scenariosDir>/values/<subDir>/<name>.yaml
97+
// exists. Listing the directory contents (instead of stat-ing the single file)
98+
// lets the error message enumerate the actually-available choices, which is
99+
// the whole point of discovery-based validation. If the directory itself does
100+
// not exist, validation is skipped (the scenario does not declare a vocabulary
101+
// for this dimension, so any name is permissive); this matches ResolvePaths's
102+
// behaviour of silently skipping missing values files.
103+
func validateAgainstDir(name, scenariosDir, subDir, flag string) error {
104+
dir := filepath.Join(scenariosDir, ValuesDir, subDir)
105+
if _, err := os.Stat(dir); os.IsNotExist(err) {
106+
return nil
107+
}
108+
available, err := listYamlFiles(dir)
109+
if err != nil {
110+
return fmt.Errorf("cannot list %s in %s: %w", subDir, scenariosDir, err)
111+
}
112+
if !contains(available, name) {
113+
return fmt.Errorf("invalid %s value %q: must be one of: %s", flag, name, strings.Join(available, ", "))
114+
}
115+
return nil
116+
}
117+
99118
// ResolvePaths returns the ordered list of values files based on the configuration.
100119
// Files are returned in order: base -> base modifiers -> identity -> persistence -> platform -> infra -> features -> migrator -> image-tags
101120
func (c *DeploymentConfig) ResolvePaths(scenariosDir string) ([]string, error) {
@@ -445,7 +464,7 @@ type BuilderOverrides struct {
445464
// prepare-values, dry-run, and coverage — goes through the same gate. This
446465
// prevents situations where a value (e.g. persistence: "no-elasticsearch") works
447466
// locally but fails in CI because only the CI path called Validate().
448-
func BuildDeploymentConfig(scenario string, ov BuilderOverrides) (*DeploymentConfig, error) {
467+
func BuildDeploymentConfig(scenario, scenariosDir string, ov BuilderOverrides) (*DeploymentConfig, error) {
449468
cfg := MapScenarioToConfig(scenario)
450469

451470
// Apply non-zero overrides.
@@ -483,7 +502,7 @@ func BuildDeploymentConfig(scenario string, ov BuilderOverrides) (*DeploymentCon
483502
cfg.ChartVersion = ov.ChartVersion
484503
}
485504

486-
if err := cfg.Validate(); err != nil {
505+
if err := cfg.Validate(scenariosDir); err != nil {
487506
return nil, fmt.Errorf("deployment config validation failed for scenario %q: %w", scenario, err)
488507
}
489508

scripts/camunda-core/pkg/scenarios/scenarios_test.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,16 +434,45 @@ func TestDeploymentConfigValidate(t *testing.T) {
434434
},
435435
}
436436

437+
scenariosDir := makeFakeScenariosDir(t)
437438
for _, tt := range tests {
438439
t.Run(tt.name, func(t *testing.T) {
439-
err := tt.config.Validate()
440+
err := tt.config.Validate(scenariosDir)
440441
if (err != nil) != tt.wantErr {
441442
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
442443
}
443444
})
444445
}
445446
}
446447

448+
// makeFakeScenariosDir builds a values/ tree containing the names referenced
449+
// by the validation tests, and excluding the names that those tests assert
450+
// should be rejected (mongodb, opensearch-external, elasticsearch-external,
451+
// keycloak-external). Adding/removing a name here mirrors what a chart
452+
// maintainer would do by adding/removing a values file in a real chart.
453+
func makeFakeScenariosDir(t *testing.T) string {
454+
t.Helper()
455+
dir := t.TempDir()
456+
tree := map[string][]string{
457+
IdentityDir: {"keycloak", "oidc", "auth0", "basic", "hybrid"},
458+
PersistenceDir: {"elasticsearch", "elasticsearch-self-signed", "no-elasticsearch", "opensearch", "opensearch-embedded", "opensearch-self-signed", "opensearch-self-signed-os-trust", "rdbms", "rdbms-external", "rdbms-oracle", "rdbms-self-signed"},
459+
PlatformDir: {"gke", "eks", "openshift"},
460+
FeaturesDir: {"multitenancy", "rba", "documentstore", "mcp", "license", "tasklist-v1"},
461+
}
462+
for sub, names := range tree {
463+
full := filepath.Join(dir, ValuesDir, sub)
464+
if err := os.MkdirAll(full, 0o755); err != nil {
465+
t.Fatalf("mkdir %s: %v", full, err)
466+
}
467+
for _, n := range names {
468+
if err := os.WriteFile(filepath.Join(full, n+".yaml"), nil, 0o644); err != nil {
469+
t.Fatalf("write %s: %v", n, err)
470+
}
471+
}
472+
}
473+
return dir
474+
}
475+
447476
func TestDeploymentConfigResolvePaths(t *testing.T) {
448477
// Create a temporary directory structure to test path resolution
449478
tmpDir := t.TempDir()
@@ -889,7 +918,7 @@ func TestBuildDeploymentConfig_ImageTagsAutoDetection(t *testing.T) {
889918

890919
for _, tt := range tests {
891920
t.Run(tt.name, func(t *testing.T) {
892-
cfg, err := BuildDeploymentConfig("qa-elasticsearch", BuilderOverrides{
921+
cfg, err := BuildDeploymentConfig("qa-elasticsearch", makeFakeScenariosDir(t), BuilderOverrides{
893922
Identity: "keycloak",
894923
Persistence: "elasticsearch",
895924
Platform: "gke",

scripts/deploy-camunda/cmd/prepare_values.go

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"path/filepath"
88
"scripts/camunda-core/pkg/logging"
99
"scripts/camunda-core/pkg/scenarios"
10-
"scripts/deploy-camunda/config"
1110
"scripts/deploy-camunda/deploy"
1211
"scripts/prepare-helm-values/pkg/env"
1312
"scripts/prepare-helm-values/pkg/values"
@@ -88,11 +87,11 @@ All diagnostic output goes to stderr via the logger.`,
8887
f.StringVar(&pv.scenarioPath, "scenario-path", "", "Path to the scenario directory (e.g., chart-full-setup)")
8988
f.StringVar(&pv.chartPath, "chart-path", "", "Path to the Camunda chart directory (used to derive scenario-path if not set)")
9089
f.StringVar(&pv.scenario, "scenario", "chart-full-setup", "Scenario name (used to derive defaults from naming conventions)")
91-
f.StringVar(&pv.identity, "identity", "", "Identity selection: keycloak, oidc, basic, hybrid")
92-
f.StringVar(&pv.persistence, "persistence", "", "Persistence selection: elasticsearch, elasticsearch-self-signed, no-elasticsearch, opensearch, opensearch-embedded, opensearch-self-signed, opensearch-self-signed-os-trust, rdbms, rdbms-external, rdbms-oracle, rdbms-self-signed")
93-
f.StringVar(&pv.testPlatform, "test-platform", "", "Test platform selection: gke, eks, openshift")
94-
f.StringVar(&pv.platform, "platform", "gke", "Deploy platform: gke, rosa, eks (fallback for --test-platform)")
95-
f.StringSliceVar(&pv.features, "features", nil, "Feature selections (comma-separated): multitenancy, rba, documentstore")
90+
f.StringVar(&pv.identity, "identity", "", "Identity selection (one of values/identity/*.yaml under the scenario)")
91+
f.StringVar(&pv.persistence, "persistence", "", "Persistence selection (one of values/persistence/*.yaml under the scenario)")
92+
f.StringVar(&pv.testPlatform, "test-platform", "", "Test platform selection (one of values/platform/*.yaml under the scenario)")
93+
f.StringVar(&pv.platform, "platform", "gke", "Deploy platform (fallback for --test-platform)")
94+
f.StringSliceVar(&pv.features, "features", nil, "Feature selections, comma-separated (each one of values/features/*.yaml under the scenario)")
9695
f.BoolVar(&pv.qa, "qa", false, "Enable QA configuration (test users, etc.)")
9796
f.BoolVar(&pv.imageTags, "image-tags", false, "Enable image tag overrides from env vars")
9897
f.BoolVar(&pv.upgradeFlow, "upgrade-flow", false, "Enable upgrade flow configuration")
@@ -105,18 +104,20 @@ All diagnostic output goes to stderr via the logger.`,
105104
f.BoolVar(&pv.interactive, "interactive", false, "Enable interactive prompts for missing variables")
106105
f.StringVarP(&pv.logLevel, "log-level", "l", "info", "Log level")
107106

108-
// Register completions for selection flags
107+
// Selection completions: discovered from the scenario directory's values/
108+
// tree at completion time (see discoverCompletions). Adding a values file
109+
// in any chart is sufficient to make its name completable here.
109110
_ = cmd.RegisterFlagCompletionFunc("identity", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
110-
return []string{"keycloak", "oidc", "basic", "hybrid"}, cobra.ShellCompDirectiveNoFileComp
111+
return discoverCompletions(cmd, scenarios.ListIdentities), cobra.ShellCompDirectiveNoFileComp
111112
})
112113
_ = cmd.RegisterFlagCompletionFunc("persistence", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
113-
return []string{"elasticsearch", "elasticsearch-self-signed", "no-elasticsearch", "opensearch", "opensearch-embedded", "opensearch-self-signed", "opensearch-self-signed-os-trust", "rdbms", "rdbms-external", "rdbms-oracle", "rdbms-self-signed"}, cobra.ShellCompDirectiveNoFileComp
114+
return discoverCompletions(cmd, scenarios.ListPersistence), cobra.ShellCompDirectiveNoFileComp
114115
})
115116
_ = cmd.RegisterFlagCompletionFunc("test-platform", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
116-
return config.TestPlatforms, cobra.ShellCompDirectiveNoFileComp
117+
return discoverCompletions(cmd, scenarios.ListPlatforms), cobra.ShellCompDirectiveNoFileComp
117118
})
118119
_ = cmd.RegisterFlagCompletionFunc("features", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
119-
return completeMultiSelect(toComplete, []string{"multitenancy", "rba", "documentstore", "arm", "migrator"})
120+
return completeMultiSelect(toComplete, discoverCompletions(cmd, scenarios.ListFeatures))
120121
})
121122
_ = cmd.RegisterFlagCompletionFunc("log-level", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
122123
return completeLogLevels(toComplete)
@@ -218,7 +219,7 @@ func runPrepareValuesLayered(pv *prepareValuesFlags, scenarioDir, outputDir stri
218219
effectivePlatform = pv.platform
219220
}
220221

221-
deployConfig, err := scenarios.BuildDeploymentConfig(pv.scenario, scenarios.BuilderOverrides{
222+
deployConfig, err := scenarios.BuildDeploymentConfig(pv.scenario, scenarioDir, scenarios.BuilderOverrides{
222223
Identity: pv.identity,
223224
Persistence: pv.persistence,
224225
Platform: effectivePlatform,

scripts/deploy-camunda/cmd/prepare_values_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ func writeTempFile(t *testing.T, dir, name, content string) string {
1919
return p
2020
}
2121

22+
2223
func captureStdout(t *testing.T, fn func() error) (string, error) {
2324
t.Helper()
2425
oldStdout := os.Stdout

0 commit comments

Comments
 (0)