From fd29a44679b0a5a3d5e1856f5e655e8c2770afec Mon Sep 17 00:00:00 2001 From: vkalapov <87693906+vkalapov@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:39:11 +0200 Subject: [PATCH] Add --dry-run flag with predicted-changes summary and deployment outputs --- clients/models/operation.go | 18 +++++++++++ commands/deploy_command.go | 63 ++++++++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/clients/models/operation.go b/clients/models/operation.go index 6cb51378..89d48973 100644 --- a/clients/models/operation.go +++ b/clients/models/operation.go @@ -53,6 +53,24 @@ type Operation struct { // user User string `json:"user,omitempty"` + + // dry run result + DryRunResult *DryRunResult `json:"dryRunResult,omitempty"` + + // deployment outputs (e.g. app URLs) + Outputs map[string]string `json:"outputs,omitempty"` +} + +// DryRunResult holds the predicted changes from a dry-run deploy +type DryRunResult struct { + AppsToCreate []string `json:"appsToCreate"` + AppsToUpdate []string `json:"appsToUpdate"` + AppsToDelete []string `json:"appsToDelete"` + ServicesToCreate []string `json:"servicesToCreate"` + ServicesToUpdate []string `json:"servicesToUpdate"` + ServicesToDelete []string `json:"servicesToDelete"` + RoutesToCreate []string `json:"routesToCreate"` + RoutesToDelete []string `json:"routesToDelete"` } /* polymorph Operation acquiredLock false */ diff --git a/commands/deploy_command.go b/commands/deploy_command.go index 5ab58374..76d340d4 100644 --- a/commands/deploy_command.go +++ b/commands/deploy_command.go @@ -58,6 +58,7 @@ const ( shouldBackupPreviousVersionOpt = "backup-previous-version" requireSecureParameters = "require-secure-parameters" disposableUserProvidedServiceOpt = "disposable-user-provided-service" + dryRunOpt = "dry-run" ) type listFlag struct { @@ -161,6 +162,7 @@ func (c *DeployCommand) GetPluginCommand() plugin.Command { util.GetShortOption(dependencyAwareStopOrderOpt): "(EXPERIMENTAL) (STRATEGY: BLUE-GREEN, INCREMENTAL-BLUE-GREEN) Stop apps in a dependency-aware order during the resume phase of a blue-green deployment", util.GetShortOption(requireSecureParameters): "(EXPERIMENTAL) Pass secrets to the deploy service in a secure way", util.GetShortOption(disposableUserProvidedServiceOpt): "Deploy when --require-secure-parameters flag is active for disposable UPS to be created and then deleted at the of the operation", + util.GetShortOption(dryRunOpt): "Simulate deployment and show predicted changes without applying them", }, }, } @@ -188,6 +190,7 @@ func deployProcessParametersSetter() ProcessParametersSetter { processBuilder.Parameter("appsTaskExecutionTimeout", GetStringOpt(taskExecutionTimeoutOpt, flags)) processBuilder.Parameter("isSecurityEnabled", strconv.FormatBool(GetBoolOpt(requireSecureParameters, flags))) processBuilder.Parameter("isDisposableUserProvidedServiceEnabled", strconv.FormatBool(GetBoolOpt(disposableUserProvidedServiceOpt, flags))) + processBuilder.Parameter("dryRun", strconv.FormatBool(GetBoolOpt(dryRunOpt, flags))) var lastSetValue string = "" for i := 0; i < len(os.Args); i++ { @@ -245,6 +248,7 @@ func (c *DeployCommand) defineCommandOptions(flags *flag.FlagSet) { flags.Bool(dependencyAwareStopOrderOpt, false, "") flags.Bool(requireSecureParameters, false, "") flags.Bool(disposableUserProvidedServiceOpt, false, "") + flags.Bool(dryRunOpt, false, "") } func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, flags *flag.FlagSet, cfTarget util.CloudFoundryTarget) ExecutionStatus { @@ -424,7 +428,15 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, return Failure } executionMonitor := NewExecutionMonitorFromLocationHeader(c.name, responseHeader.Location.String(), retries, []*models.Message{}, mtaClient) - return executionMonitor.Monitor() + status := executionMonitor.Monitor() + if GetBoolOpt(dryRunOpt, flags) { + op, err := mtaClient.GetMtaOperation(executionMonitor.operationID, "") + if err == nil && op.DryRunResult != nil { + printDryRunResult(op) + return Success + } + } + return status } func setUpSpecificsForDeploymentUsingSecrets(flags *flag.FlagSet, c *DeployCommand, mtaId, namespace, schemaVersion string, disposableUserProvidedServiceName *string, yamlBytes *[]byte) ExecutionStatus { @@ -854,3 +866,52 @@ func rateLimitMessage(retryAfter time.Duration) string { } return base + " later." } + +func printDryRunResult(op *models.Operation) { + dr := op.DryRunResult + ui.Say("\n%s", terminal.WarningColor("Dry run — no changes were applied.")) + ui.Say("") + + type section struct { + label string + prefix string + color func(string) string + items []string + } + + sections := []section{ + {"Apps to create", "+ ", terminal.SuccessColor, dr.AppsToCreate}, + {"Apps to update", "~ ", terminal.WarningColor, dr.AppsToUpdate}, + {"Apps to delete", "- ", terminal.FailureColor, dr.AppsToDelete}, + {"Services to create", "+ ", terminal.SuccessColor, dr.ServicesToCreate}, + {"Services to update", "~ ", terminal.WarningColor, dr.ServicesToUpdate}, + {"Services to delete", "- ", terminal.FailureColor, dr.ServicesToDelete}, + {"Routes to create", "+ ", terminal.SuccessColor, dr.RoutesToCreate}, + {"Routes to delete", "- ", terminal.FailureColor, dr.RoutesToDelete}, + } + + anyChange := false + for _, s := range sections { + if len(s.items) == 0 { + continue + } + anyChange = true + ui.Say(" %s:", terminal.HeaderColor(s.label)) + for _, item := range s.items { + ui.Say(" %s", s.color(s.prefix+item)) + } + } + + if !anyChange { + ui.Say(" No changes detected.") + } + + if len(op.Outputs) > 0 { + ui.Say("") + ui.Say(" %s:", terminal.HeaderColor("Deployment outputs")) + for k, v := range op.Outputs { + ui.Say(" %s: %s", k, terminal.EntityNameColor(v)) + } + } + ui.Say("") +}