diff --git a/cmd/main.go b/cmd/main.go index 8f43addd..529f6f15 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -251,6 +251,7 @@ func run(cfg *Config) error { redirectRevisions, cfg.Debug, cfg.FailOnDuplicateGeneratedApplications, + cfg.SkipAppSetGenerationErrors, appSelectionOptions, ) if err != nil { diff --git a/cmd/options.go b/cmd/options.go index 3ff4b6f5..51254a2e 100644 --- a/cmd/options.go +++ b/cmd/options.go @@ -85,6 +85,7 @@ var ( DefaultOutputBranchManifests = false DefaultTraverseAppOfApps = false DefaultFailOnDuplicateGeneratedApplications = false + DefaultSkipAppSetGenerationErrors = false ) // RawOptions holds the raw CLI/env inputs - used only for parsing @@ -136,6 +137,7 @@ type RawOptions struct { OutputBranchManifests bool `mapstructure:"output-branch-manifests"` TraverseAppOfApps bool `mapstructure:"traverse-app-of-apps"` FailOnDuplicateGeneratedApplications bool `mapstructure:"fail-on-duplicate-generated-applications"` + SkipAppSetGenerationErrors bool `mapstructure:"skip-appset-generation-errors"` } // Config is the final, validated, ready-to-use configuration @@ -181,6 +183,7 @@ type Config struct { OutputBranchManifests bool TraverseAppOfApps bool FailOnDuplicateGeneratedApplications bool + SkipAppSetGenerationErrors bool // Parsed/processed fields - no "parsed" prefix needed FileRegex *regexp.Regexp @@ -278,6 +281,7 @@ func Parse() *Config { viper.SetDefault("output-branch-manifests", DefaultOutputBranchManifests) viper.SetDefault("traverse-app-of-apps", DefaultTraverseAppOfApps) viper.SetDefault("fail-on-duplicate-generated-applications", DefaultFailOnDuplicateGeneratedApplications) + viper.SetDefault("skip-appset-generation-errors", DefaultSkipAppSetGenerationErrors) // Basic flags rootCmd.Flags().BoolP("debug", "d", false, "Activate debug mode") @@ -338,6 +342,7 @@ func Parse() *Config { rootCmd.Flags().Bool("output-branch-manifests", DefaultOutputBranchManifests, "Write all application manifests per branch to a single file (output/base-branch.yaml and output/target-branch.yaml)") rootCmd.Flags().Bool("traverse-app-of-apps", DefaultTraverseAppOfApps, "Recursively render child Applications discovered in rendered manifests (app-of-apps pattern). Only supported with --render-method=repo-server-api") rootCmd.Flags().Bool("fail-on-duplicate-generated-applications", DefaultFailOnDuplicateGeneratedApplications, "Fail when a single ApplicationSet generates multiple Applications with the same name") + rootCmd.Flags().Bool("skip-appset-generation-errors", DefaultSkipAppSetGenerationErrors, "Skip ApplicationSets that fail to generate (log a warning and continue) instead of aborting the whole run. Useful for ApplicationSets whose generators need cluster secrets or live data unavailable in the ephemeral cluster (e.g. pullRequest/scmProvider generators)") // Check if version flag was specified directly for _, arg := range os.Args[1:] { @@ -427,6 +432,7 @@ func (o *RawOptions) ToConfig() (*Config, error) { OutputBranchManifests: o.OutputBranchManifests, TraverseAppOfApps: o.TraverseAppOfApps, FailOnDuplicateGeneratedApplications: o.FailOnDuplicateGeneratedApplications, + SkipAppSetGenerationErrors: o.SkipAppSetGenerationErrors, } var err error @@ -783,4 +789,7 @@ func (o *Config) LogConfig() { if o.FailOnDuplicateGeneratedApplications { log.Info().Msgf("✨ - fail-on-duplicate-generated-applications: %t", o.FailOnDuplicateGeneratedApplications) } + if o.SkipAppSetGenerationErrors { + log.Info().Msgf("✨ - skip-appset-generation-errors: %t", o.SkipAppSetGenerationErrors) + } } diff --git a/docs/options.md b/docs/options.md index a44f5897..6dadbcc2 100644 --- a/docs/options.md +++ b/docs/options.md @@ -29,6 +29,7 @@ argocd-diff-preview [FLAGS] [OPTIONS] (--repo | --repo-regex ) --t | `--ignore-invalid-watch-pattern` | `IGNORE_INVALID_WATCH_PATTERN` | `false` | Ignore invalid watch-pattern Regex on Applications | | `--keep-cluster-alive` | `KEEP_CLUSTER_ALIVE` | `false` | Keep cluster alive after the tool finishes | | `--fail-on-duplicate-generated-applications` | `FAIL_ON_DUPLICATE_GENERATED_APPLICATIONS` | `false` | Fail when a single ApplicationSet generates multiple Applications with the same name | +| `--skip-appset-generation-errors` | `SKIP_APPSET_GENERATION_ERRORS` | `false` | Skip ApplicationSets that fail to generate (log a warning and continue) instead of aborting the run. Useful for generators that need cluster secrets or live data unavailable in the ephemeral cluster (e.g. `pullRequest`/`scmProvider`) | | `--kind-internal` | `KIND_INTERNAL` | `false` | Use the kind cluster's internal address in the kubeconfig (allows connecting to the cluster when running the CLI in a container) | | `--version`, `-v` | - | - | Prints version information | | `--output-app-manifests` | `OUTPUT_APP_MANIFESTS` | `false` | Write each application's manifests to its own file under `output/base/` and `output/target/` | diff --git a/pkg/argoapplication/application_sets.go b/pkg/argoapplication/application_sets.go index c5829938..9c9beb86 100644 --- a/pkg/argoapplication/application_sets.go +++ b/pkg/argoapplication/application_sets.go @@ -26,6 +26,7 @@ func ConvertAppSetsToAppsInBothBranches( redirectRevisions []string, debug bool, failOnDuplicateGeneratedApplications bool, + skipAppSetGenerationErrors bool, appSelectionOptions ApplicationSelectionOptions, ) (*ArgoSelection, *ArgoSelection, time.Duration, error) { startTime := time.Now() @@ -40,6 +41,7 @@ func ConvertAppSetsToAppsInBothBranches( baseTempFolder, debug, failOnDuplicateGeneratedApplications, + skipAppSetGenerationErrors, appSelectionOptions, repoSelector, redirectRevisions, @@ -58,6 +60,7 @@ func ConvertAppSetsToAppsInBothBranches( targetTempFolder, debug, failOnDuplicateGeneratedApplications, + skipAppSetGenerationErrors, appSelectionOptions, repoSelector, redirectRevisions, @@ -79,6 +82,7 @@ func processAppSets( tempFolder string, debug bool, failOnDuplicateGeneratedApplications bool, + skipAppSetGenerationErrors bool, appSelectionOptions ApplicationSelectionOptions, repoSelector repository.Selector, redirectRevisions []string, @@ -91,6 +95,7 @@ func processAppSets( tempFolder, debug, failOnDuplicateGeneratedApplications, + skipAppSetGenerationErrors, ) if err != nil { log.Error().Str("branch", branch.Name).Msg("❌ Failed to generate apps") @@ -207,6 +212,7 @@ func convertAppSetsToApps( tempFolder string, debug bool, failOnDuplicateGeneratedApplications bool, + skipAppSetGenerationErrors bool, ) (*AppSetConversionResult, error) { log.Debug().Str("branch", branch.Name).Msg("🤖 Generating Applications from ApplicationSets") @@ -270,9 +276,19 @@ func convertAppSetsToApps( // the original onlyAppSets slice, regardless of goroutine scheduling. orderedResults := make([][]ArgoResource, len(onlyAppSets)) + skippedAppSetsCount := 0 for res := range results { if res.err != nil { - return nil, res.err + if !skipAppSetGenerationErrors { + return nil, res.err + } + log.Warn(). + Err(res.err). + Str("branch", branch.Name). + Str(onlyAppSets[res.index].Kind.ShortName(), onlyAppSets[res.index].GetLongName()). + Msg("⚠️ Skipping ApplicationSet that failed to generate because --skip-appset-generation-errors is enabled") + skippedAppSetsCount++ + continue } generatedApplicationsCount += len(res.apps) orderedResults[res.index] = res.apps @@ -285,7 +301,7 @@ func convertAppSetsToApps( } return &AppSetConversionResult{ - appSetsProcessedCount: len(onlyAppSets), + appSetsProcessedCount: len(onlyAppSets) - skippedAppSetsCount, originalApplicationsCount: len(plainApps), generatedApplicationsCount: generatedApplicationsCount, argoResource: appsNew,