fix: split env config and image pull secret into separate helm releases

Bundling these Secrets in the app helm chart caused two bugs in the scheduler-k3s plugin: a chart rollback could delete Secrets that older ReplicaSets still referenced by exact timestamped name (`env-{app}.{ts}` and `ims-{app}.{ts}`), hard-crashing pods until manual intervention; and the strategic-merge `patchMergeKey` on `imagePullSecrets` let stale entries leak into the live Deployment until the list pointed at many nonexistent Secrets. Each Secret now lives in its own helm release with a stable name (`config-{app}` and `pull-secret-{app}`), installed before the app chart on every deploy. The deployment trigger also prunes any leaked `imagePullSecrets` entries from the live Deployment so the next deploy lands on a clean list, and the rename and destroy paths uninstall the new releases (and the previously-leaked TLS release on rename) under the old app name.
This commit is contained in:
Jose Diaz-Gonzalez
2026-04-29 09:46:29 -04:00
parent f06048a266
commit ef9bdc0379
18 changed files with 1086 additions and 56 deletions

View File

@@ -808,6 +808,63 @@ func getComputedImagePullSecrets(appName string) string {
return imagePullSecrets
}
// pruneStaleImagePullSecretsFromDeployments rewrites the imagePullSecrets list on existing app
// Deployments to contain only the names in keepNames. This removes references that helm has
// lost track of due to strategic-merge accumulation on PodSpec.ImagePullSecrets, which is the
// root cause of stale-secret pod hard crashes after rollbacks. The patch is a no-op when the
// live list already matches.
func pruneStaleImagePullSecretsFromDeployments(ctx context.Context, clientset KubernetesClient, namespace string, appName string, keepNames []string) error {
deployments, err := clientset.ListDeployments(ctx, ListDeploymentsInput{
Namespace: namespace,
LabelSelector: fmt.Sprintf("app.kubernetes.io/part-of=%s", appName),
})
if err != nil {
return fmt.Errorf("error listing deployments: %w", err)
}
keepSet := map[string]struct{}{}
for _, name := range keepNames {
if name != "" {
keepSet[name] = struct{}{}
}
}
for _, deployment := range deployments {
live := deployment.Spec.Template.Spec.ImagePullSecrets
if needsImagePullSecretsPrune(live, keepSet) {
common.LogVerboseQuiet(fmt.Sprintf("Pruning stale imagePullSecrets entries from deployment %s", deployment.Name))
err := clientset.SetDeploymentImagePullSecrets(ctx, SetDeploymentImagePullSecretsInput{
Name: deployment.Name,
Namespace: namespace,
ImagePullSecrets: keepNames,
})
if err != nil {
return fmt.Errorf("error pruning deployment %s: %w", deployment.Name, err)
}
}
}
return nil
}
// needsImagePullSecretsPrune reports whether the live imagePullSecrets list differs from the
// desired keep-set (either contains entries not in the keep-set or is missing entries in it).
func needsImagePullSecretsPrune(live []corev1.LocalObjectReference, keepSet map[string]struct{}) bool {
if len(live) != len(keepSet) {
return true
}
seen := map[string]struct{}{}
for _, ref := range live {
if _, ok := keepSet[ref.Name]; !ok {
return true
}
seen[ref.Name] = struct{}{}
}
return len(seen) != len(keepSet)
}
func getGlobalIngressClass() string {
return common.PropertyGetDefault("scheduler-k3s", "--global", "ingress-class", DefaultIngressClass)
}