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.
This commit is contained in:
Jose Diaz-Gonzalez
2026-06-02 23:34:10 -04:00
parent cd66361895
commit f1e31ded41
3 changed files with 64 additions and 20 deletions

View File

@@ -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")

View File

@@ -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.<id>"
// (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) {

View File

@@ -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
}