|
| 1 | +package cmd |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "os" |
| 7 | + "scripts/camunda-core/pkg/logging" |
| 8 | + "scripts/deploy-camunda/config" |
| 9 | + "scripts/deploy-camunda/matrix" |
| 10 | + "scripts/prepare-helm-values/pkg/env" |
| 11 | + "strings" |
| 12 | + |
| 13 | + "github.com/spf13/cobra" |
| 14 | +) |
| 15 | + |
| 16 | +// newMatrixCommand creates the matrix parent command with list and run subcommands. |
| 17 | +func newMatrixCommand() *cobra.Command { |
| 18 | + matrixCmd := &cobra.Command{ |
| 19 | + Use: "matrix", |
| 20 | + Short: "Generate and run the CI test matrix across all active chart versions", |
| 21 | + } |
| 22 | + |
| 23 | + matrixCmd.AddCommand(newMatrixListCommand()) |
| 24 | + matrixCmd.AddCommand(newMatrixRunCommand()) |
| 25 | + |
| 26 | + return matrixCmd |
| 27 | +} |
| 28 | + |
| 29 | +// newMatrixListCommand creates the "matrix list" subcommand. |
| 30 | +func newMatrixListCommand() *cobra.Command { |
| 31 | + var ( |
| 32 | + versions []string |
| 33 | + includeDisabled bool |
| 34 | + scenarioFilter string |
| 35 | + flowFilter string |
| 36 | + outputFormat string |
| 37 | + platform string |
| 38 | + repoRoot string |
| 39 | + ) |
| 40 | + |
| 41 | + cmd := &cobra.Command{ |
| 42 | + Use: "list", |
| 43 | + Short: "List the CI test matrix for all active chart versions", |
| 44 | + Long: `List the full CI test matrix generated from chart-versions.yaml, |
| 45 | +ci-test-config.yaml (PR scenarios only), and permitted-flows.yaml. |
| 46 | +
|
| 47 | +This command does not require cluster access.`, |
| 48 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 49 | + repoRoot = resolveRepoRoot(repoRoot) |
| 50 | + if repoRoot == "" { |
| 51 | + return fmt.Errorf("--repo-root is required (or set repoRoot in config)") |
| 52 | + } |
| 53 | + |
| 54 | + entries, err := matrix.Generate(repoRoot, matrix.GenerateOptions{ |
| 55 | + Versions: versions, |
| 56 | + IncludeDisabled: includeDisabled, |
| 57 | + }) |
| 58 | + if err != nil { |
| 59 | + return err |
| 60 | + } |
| 61 | + |
| 62 | + entries = matrix.Filter(entries, matrix.FilterOptions{ |
| 63 | + ScenarioFilter: scenarioFilter, |
| 64 | + FlowFilter: flowFilter, |
| 65 | + Platform: platform, |
| 66 | + }) |
| 67 | + |
| 68 | + output, err := matrix.Print(entries, outputFormat) |
| 69 | + if err != nil { |
| 70 | + return err |
| 71 | + } |
| 72 | + fmt.Fprintln(os.Stdout, output) |
| 73 | + return nil |
| 74 | + }, |
| 75 | + } |
| 76 | + |
| 77 | + f := cmd.Flags() |
| 78 | + f.StringSliceVar(&versions, "versions", nil, "Limit to specific chart versions (comma-separated, e.g., 8.8,8.9)") |
| 79 | + f.BoolVar(&includeDisabled, "include-disabled", false, "Include disabled scenarios in the output") |
| 80 | + f.StringVar(&scenarioFilter, "scenario-filter", "", "Filter scenarios by substring match") |
| 81 | + f.StringVar(&flowFilter, "flow-filter", "", "Filter entries by exact flow name") |
| 82 | + f.StringVar(&outputFormat, "format", "table", "Output format: table, json") |
| 83 | + f.StringVar(&platform, "platform", "", "Filter entries to those supporting this platform") |
| 84 | + f.StringVar(&repoRoot, "repo-root", "", "Repository root path (or set repoRoot in config)") |
| 85 | + |
| 86 | + return cmd |
| 87 | +} |
| 88 | + |
| 89 | +// newMatrixRunCommand creates the "matrix run" subcommand. |
| 90 | +func newMatrixRunCommand() *cobra.Command { |
| 91 | + var ( |
| 92 | + versions []string |
| 93 | + includeDisabled bool |
| 94 | + scenarioFilter string |
| 95 | + flowFilter string |
| 96 | + platform string |
| 97 | + repoRoot string |
| 98 | + dryRun bool |
| 99 | + testIT bool |
| 100 | + testE2E bool |
| 101 | + testAll bool |
| 102 | + stopOnFailure bool |
| 103 | + namespacePrefix string |
| 104 | + cleanup bool |
| 105 | + kubeContext string |
| 106 | + kubeContextGKE string |
| 107 | + kubeContextEKS string |
| 108 | + ingressBaseDomain string |
| 109 | + maxParallel int |
| 110 | + envFile string |
| 111 | + envFile86 string |
| 112 | + envFile87 string |
| 113 | + envFile88 string |
| 114 | + envFile89 string |
| 115 | + logLevel string |
| 116 | + ) |
| 117 | + |
| 118 | + cmd := &cobra.Command{ |
| 119 | + Use: "run", |
| 120 | + Short: "Run the CI test matrix against a live cluster", |
| 121 | + Long: `Run the full CI test matrix, deploying each scenario + flow combination sequentially. |
| 122 | +Each entry gets its own namespace (<prefix>-<version>-<shortname>). |
| 123 | +
|
| 124 | +Use --cleanup to automatically delete all created namespaces after the run finishes. |
| 125 | +Cleanup runs regardless of whether entries succeeded or failed. |
| 126 | +
|
| 127 | +This command calls deploy.Execute() for each matrix entry.`, |
| 128 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 129 | + // Setup logging |
| 130 | + if err := logging.Setup(logging.Options{ |
| 131 | + LevelString: logLevel, |
| 132 | + ColorEnabled: logging.IsTerminal(os.Stdout.Fd()), |
| 133 | + }); err != nil { |
| 134 | + return err |
| 135 | + } |
| 136 | + |
| 137 | + // Load .env file — use flag value if set, otherwise default to .env. |
| 138 | + // This loads the fallback env file for vars shared across all versions. |
| 139 | + envFileToLoad := envFile |
| 140 | + if envFileToLoad == "" { |
| 141 | + envFileToLoad = ".env" |
| 142 | + } |
| 143 | + logging.Logger.Debug(). |
| 144 | + Str("envFile", envFileToLoad). |
| 145 | + Msg("Loading environment file") |
| 146 | + _ = env.Load(envFileToLoad) |
| 147 | + |
| 148 | + repoRoot = resolveRepoRoot(repoRoot) |
| 149 | + if repoRoot == "" { |
| 150 | + return fmt.Errorf("--repo-root is required (or set repoRoot in config)") |
| 151 | + } |
| 152 | + |
| 153 | + // Validate ingress base domain early so the user gets immediate feedback. |
| 154 | + if ingressBaseDomain != "" { |
| 155 | + valid := false |
| 156 | + for _, d := range config.ValidIngressBaseDomains { |
| 157 | + if d == ingressBaseDomain { |
| 158 | + valid = true |
| 159 | + break |
| 160 | + } |
| 161 | + } |
| 162 | + if !valid { |
| 163 | + return fmt.Errorf("--ingress-base-domain must be one of: %s", strings.Join(config.ValidIngressBaseDomains, ", ")) |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + entries, err := matrix.Generate(repoRoot, matrix.GenerateOptions{ |
| 168 | + Versions: versions, |
| 169 | + IncludeDisabled: includeDisabled, |
| 170 | + }) |
| 171 | + if err != nil { |
| 172 | + return err |
| 173 | + } |
| 174 | + |
| 175 | + entries = matrix.Filter(entries, matrix.FilterOptions{ |
| 176 | + ScenarioFilter: scenarioFilter, |
| 177 | + FlowFilter: flowFilter, |
| 178 | + Platform: platform, |
| 179 | + }) |
| 180 | + |
| 181 | + if len(entries) == 0 { |
| 182 | + fmt.Fprintln(os.Stdout, "No matrix entries matched the filters.") |
| 183 | + return nil |
| 184 | + } |
| 185 | + |
| 186 | + // Show what will be run |
| 187 | + output, _ := matrix.Print(entries, "table") |
| 188 | + fmt.Fprintln(os.Stdout, output) |
| 189 | + |
| 190 | + // Build platform-to-context map from per-platform flags |
| 191 | + kubeContexts := make(map[string]string) |
| 192 | + if kubeContextGKE != "" { |
| 193 | + kubeContexts["gke"] = kubeContextGKE |
| 194 | + } |
| 195 | + if kubeContextEKS != "" { |
| 196 | + kubeContexts["eks"] = kubeContextEKS |
| 197 | + } |
| 198 | + |
| 199 | + // Build version-to-env-file map from per-version flags |
| 200 | + envFiles := make(map[string]string) |
| 201 | + for version, path := range map[string]string{ |
| 202 | + "8.6": envFile86, |
| 203 | + "8.7": envFile87, |
| 204 | + "8.8": envFile88, |
| 205 | + "8.9": envFile89, |
| 206 | + } { |
| 207 | + if path != "" { |
| 208 | + envFiles[version] = path |
| 209 | + } |
| 210 | + } |
| 211 | + |
| 212 | + results, err := matrix.Run(context.Background(), entries, matrix.RunOptions{ |
| 213 | + DryRun: dryRun, |
| 214 | + StopOnFailure: stopOnFailure, |
| 215 | + Cleanup: cleanup, |
| 216 | + KubeContexts: kubeContexts, |
| 217 | + KubeContext: kubeContext, |
| 218 | + NamespacePrefix: namespacePrefix, |
| 219 | + Platform: platform, |
| 220 | + MaxParallel: maxParallel, |
| 221 | + TestIT: testIT, |
| 222 | + TestE2E: testE2E, |
| 223 | + TestAll: testAll, |
| 224 | + RepoRoot: repoRoot, |
| 225 | + EnvFiles: envFiles, |
| 226 | + EnvFile: envFile, |
| 227 | + IngressBaseDomain: ingressBaseDomain, |
| 228 | + LogLevel: logLevel, |
| 229 | + }) |
| 230 | + |
| 231 | + fmt.Fprintln(os.Stdout, matrix.PrintRunSummary(results)) |
| 232 | + |
| 233 | + return err |
| 234 | + }, |
| 235 | + } |
| 236 | + |
| 237 | + f := cmd.Flags() |
| 238 | + f.StringSliceVar(&versions, "versions", nil, "Limit to specific chart versions (comma-separated, e.g., 8.8,8.9)") |
| 239 | + f.BoolVar(&includeDisabled, "include-disabled", false, "Include disabled scenarios in the output") |
| 240 | + f.StringVar(&scenarioFilter, "scenario-filter", "", "Filter scenarios by substring match") |
| 241 | + f.StringVar(&flowFilter, "flow-filter", "", "Filter entries by exact flow name") |
| 242 | + f.StringVar(&platform, "platform", "", "Filter entries to those supporting this platform (also sets deploy platform)") |
| 243 | + f.StringVar(&repoRoot, "repo-root", "", "Repository root path (or set repoRoot in config)") |
| 244 | + f.BoolVar(&dryRun, "dry-run", false, "Log what would be deployed without actually deploying") |
| 245 | + f.BoolVar(&testIT, "test-it", false, "Run integration tests after each deployment") |
| 246 | + f.BoolVar(&testE2E, "test-e2e", false, "Run e2e tests after each deployment") |
| 247 | + f.BoolVar(&testAll, "test-all", false, "Run both integration and e2e tests after each deployment") |
| 248 | + f.BoolVar(&stopOnFailure, "stop-on-failure", false, "Stop the run on the first failure") |
| 249 | + f.StringVar(&namespacePrefix, "namespace-prefix", "matrix", "Prefix for generated namespaces") |
| 250 | + f.BoolVar(&cleanup, "cleanup", false, "Delete all created namespaces after the run completes") |
| 251 | + f.StringVar(&kubeContext, "kube-context", "", "Default Kubernetes context for all platforms (overridden by --kube-context-gke/--kube-context-eks)") |
| 252 | + f.StringVar(&kubeContextGKE, "kube-context-gke", "", "Kubernetes context for GKE entries") |
| 253 | + f.StringVar(&kubeContextEKS, "kube-context-eks", "", "Kubernetes context for EKS entries") |
| 254 | + f.StringVar(&ingressBaseDomain, "ingress-base-domain", "", "Base domain for ingress hosts; each entry gets <namespace>.<base-domain>") |
| 255 | + f.IntVar(&maxParallel, "max-parallel", 1, "Maximum number of entries to run concurrently (1 = sequential)") |
| 256 | + f.StringVar(&envFile, "env-file", "", "Default .env file for all versions (overridden by --env-file-X.Y)") |
| 257 | + f.StringVar(&envFile86, "env-file-8.6", "", "Path to .env file for 8.6 entries") |
| 258 | + f.StringVar(&envFile87, "env-file-8.7", "", "Path to .env file for 8.7 entries") |
| 259 | + f.StringVar(&envFile88, "env-file-8.8", "", "Path to .env file for 8.8 entries") |
| 260 | + f.StringVar(&envFile89, "env-file-8.9", "", "Path to .env file for 8.9 entries") |
| 261 | + f.StringVarP(&logLevel, "log-level", "l", "info", "Log level (debug, info, warn, error)") |
| 262 | + |
| 263 | + registerIngressBaseDomainCompletion(cmd) |
| 264 | + registerKubeContextCompletion(cmd) |
| 265 | + registerKubeContextCompletionForFlag(cmd, "kube-context-gke") |
| 266 | + registerKubeContextCompletionForFlag(cmd, "kube-context-eks") |
| 267 | + _ = cmd.RegisterFlagCompletionFunc("log-level", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { |
| 268 | + return completeLogLevels(toComplete) |
| 269 | + }) |
| 270 | + |
| 271 | + return cmd |
| 272 | +} |
| 273 | + |
| 274 | +// registerKubeContextCompletionForFlag adds tab completion for a named kube-context flag. |
| 275 | +func registerKubeContextCompletionForFlag(cmd *cobra.Command, flagName string) { |
| 276 | + _ = cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { |
| 277 | + contexts, err := getKubeContexts() |
| 278 | + if err != nil { |
| 279 | + return nil, cobra.ShellCompDirectiveNoFileComp |
| 280 | + } |
| 281 | + |
| 282 | + var completions []string |
| 283 | + for _, ctx := range contexts { |
| 284 | + if toComplete == "" || strings.HasPrefix(ctx, toComplete) { |
| 285 | + completions = append(completions, ctx) |
| 286 | + } |
| 287 | + } |
| 288 | + return completions, cobra.ShellCompDirectiveNoFileComp |
| 289 | + }) |
| 290 | +} |
| 291 | + |
| 292 | +// resolveRepoRoot resolves the repository root from the flag or config file. |
| 293 | +func resolveRepoRoot(flagValue string) string { |
| 294 | + if flagValue != "" { |
| 295 | + return flagValue |
| 296 | + } |
| 297 | + |
| 298 | + // Try to resolve from config file |
| 299 | + var tempFlags config.RuntimeFlags |
| 300 | + if _, err := config.LoadAndMerge(configFile, false, &tempFlags); err == nil { |
| 301 | + if tempFlags.RepoRoot != "" { |
| 302 | + return tempFlags.RepoRoot |
| 303 | + } |
| 304 | + } |
| 305 | + |
| 306 | + return "" |
| 307 | +} |
0 commit comments