From f1e31ded415a8551fb5d20d758700ffc84d46fd5 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 2 Jun 2026 23:34:10 -0400 Subject: [PATCH] fix: pin scheduler-k3s:preview deployment id to the deployed release Without this, the preview build invented a fresh timestamp for `Values.global.deployment_id` and the resulting diff showed every Deployment changing its `app.kubernetes.io/version` annotation - even when nothing semantically changed. CommandPreview now reads the deployment id out of `release.Chart.Metadata.Version` and passes it through a new `BuildOptions.OverrideDeploymentID` parameter so the previewed manifests carry the same version label as the live release. The deploy path still generates a fresh timestamp. --- plugins/scheduler-k3s/chart.go | 16 +++++++- plugins/scheduler-k3s/preview.go | 66 +++++++++++++++++++++++-------- plugins/scheduler-k3s/triggers.go | 2 +- 3 files changed, 64 insertions(+), 20 deletions(-) diff --git a/plugins/scheduler-k3s/chart.go b/plugins/scheduler-k3s/chart.go index 9a9a825a9..74205873e 100644 --- a/plugins/scheduler-k3s/chart.go +++ b/plugins/scheduler-k3s/chart.go @@ -33,6 +33,15 @@ type BuildResult struct { KustomizeRootPath string } +// BuildOptions configures non-default behavior for BuildAppChart. +type BuildOptions struct { + // OverrideDeploymentID, when non-zero, replaces the timestamp-derived + // deployment id. CommandPreview uses this to reuse the currently-deployed + // release's id so app.kubernetes.io/version annotation churn does not + // appear in the diff. + OverrideDeploymentID int64 +} + // BuildAppChart constructs a Helm chart on disk that reflects dokku's configured // state for the given app and image tag. It reads from the Kubernetes cluster // where required (for KEDA values and existing cron-job suffixes) but performs @@ -42,7 +51,7 @@ type BuildResult struct { // The returned BuildResult holds the absolute chart path and the parameters a // subsequent Helm Install/Upgrade (dry-run or real) needs. The caller must // os.RemoveAll(result.ChartDir) when finished. -func BuildAppChart(ctx context.Context, appName, imageTag string) (BuildResult, error) { +func BuildAppChart(ctx context.Context, appName, imageTag string, opts BuildOptions) (BuildResult, error) { var result BuildResult processesRes, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ @@ -142,7 +151,10 @@ func BuildAppChart(ctx context.Context, appName, imageTag string) (BuildResult, return cleanup(fmt.Errorf("Error creating chart templates directory: %w", err)) } - deploymentId := time.Now().Unix() + deploymentId := opts.OverrideDeploymentID + if deploymentId == 0 { + deploymentId = time.Now().Unix() + } imagePullSecrets := getComputedImagePullSecrets(appName) if imagePullSecrets == "" { dockerConfigPath := filepath.Join(registry.GetComputedAppRegistryConfigDir(appName), "config.json") diff --git a/plugins/scheduler-k3s/preview.go b/plugins/scheduler-k3s/preview.go index 988ab20aa..a92b10b9c 100644 --- a/plugins/scheduler-k3s/preview.go +++ b/plugins/scheduler-k3s/preview.go @@ -6,6 +6,8 @@ import ( "fmt" "os" "os/signal" + "strconv" + "strings" "syscall" "github.com/dokku/dokku/plugins/common" @@ -14,6 +16,7 @@ import ( "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage/driver" ) @@ -50,29 +53,36 @@ func CommandPreview(appName string, diffContext int, showSecrets bool, showSecre cancel() }() - chartResult, err := BuildAppChart(ctx, appName, "") + namespace := getComputedNamespace(appName) + helmAgent, err := NewHelmAgent(namespace, DevNullPrinter) + if err != nil { + return fmt.Errorf("Error creating helm agent: %w", err) + } + + currentRelease, err := fetchCurrentRelease(helmAgent, appName) + if err != nil { + return err + } + + buildOpts := BuildOptions{} + if currentRelease != nil { + buildOpts.OverrideDeploymentID = deploymentIDFromRelease(currentRelease) + } + + chartResult, err := BuildAppChart(ctx, appName, "", buildOpts) if err != nil { return err } defer os.RemoveAll(chartResult.ChartDir) - helmAgent, err := NewHelmAgent(chartResult.Namespace, DevNullPrinter) - if err != nil { - return fmt.Errorf("Error creating helm agent: %w", err) - } - chartRequested, err := loadChartForPreview(chartResult.ChartPath) if err != nil { return err } - currentManifest, releaseExists, err := fetchCurrentManifest(helmAgent, chartResult.ReleaseName) - if err != nil { - return err - } - - var proposedManifest string - if releaseExists { + var currentManifest, proposedManifest string + if currentRelease != nil { + currentManifest = currentRelease.Manifest proposedManifest, err = renderUpgradeDryRun(ctx, helmAgent, chartResult, chartRequested) } else { proposedManifest, err = renderInstallDryRun(ctx, helmAgent, chartResult, chartRequested) @@ -108,16 +118,38 @@ func loadChartForPreview(chartPath string) (*chart.Chart, error) { return chartRequested, nil } -func fetchCurrentManifest(helmAgent *HelmAgent, releaseName string) (string, bool, error) { +func fetchCurrentRelease(helmAgent *HelmAgent, releaseName string) (*release.Release, error) { getAction := action.NewGet(helmAgent.Configuration) rel, err := getAction.Run(releaseName) if err != nil { if errors.Is(err, driver.ErrReleaseNotFound) { - return "", false, nil + return nil, nil } - return "", false, fmt.Errorf("Error fetching current release: %w", err) + return nil, fmt.Errorf("Error fetching current release: %w", err) } - return rel.Manifest, true, nil + return rel, nil +} + +// deploymentIDFromRelease extracts the deployment id encoded in a release's +// chart version. BuildAppChart writes the chart version as "0.0." +// (chart.go); the parsed id lets CommandPreview pin the previewed manifests +// to the currently-deployed value so the `app.kubernetes.io/version` +// annotation does not churn in the diff. Returns 0 when the version does not +// match the expected shape - the caller then falls back to the timestamp. +func deploymentIDFromRelease(rel *release.Release) int64 { + if rel == nil || rel.Chart == nil || rel.Chart.Metadata == nil { + return 0 + } + const prefix = "0.0." + v := rel.Chart.Metadata.Version + if !strings.HasPrefix(v, prefix) { + return 0 + } + id, err := strconv.ParseInt(strings.TrimPrefix(v, prefix), 10, 64) + if err != nil { + return 0 + } + return id } func renderUpgradeDryRun(ctx context.Context, helmAgent *HelmAgent, chartResult BuildResult, chartRequested *chart.Chart) (string, error) { diff --git a/plugins/scheduler-k3s/triggers.go b/plugins/scheduler-k3s/triggers.go index 719fe81f8..5aa93409b 100644 --- a/plugins/scheduler-k3s/triggers.go +++ b/plugins/scheduler-k3s/triggers.go @@ -476,7 +476,7 @@ func TriggerSchedulerDeploy(scheduler string, appName string, imageTag string) e return fmt.Errorf("Error creating kubernetes namespace for deployment: %w", err) } - chartResult, err := BuildAppChart(ctx, appName, imageTag) + chartResult, err := BuildAppChart(ctx, appName, imageTag, BuildOptions{}) if err != nil { return err }