diff --git a/cmd/helm.go b/cmd/helm.go index fe4d138f..62e45769 100644 --- a/cmd/helm.go +++ b/cmd/helm.go @@ -412,7 +412,7 @@ func (d *diffCmd) template(isUpgrade bool) ([]byte, error) { subcmd = "template" filter = func(s []byte) []byte { - return s + return stripOCIPullProgress(s) } } @@ -494,6 +494,36 @@ func extractManifestFromHelmUpgradeDryRunOutput(s []byte, noHooks bool) []byte { return r } +// ociPullProgressRE matches Helm's OCI chart pull progress lines that Helm writes +// to stdout before the rendered manifests when the chart, or one of its subcharts, +// is pulled from an OCI registry. +// +// The lines reported in the wild are "Pulled: ..." and "Digest: ..."; +// "Pulling: ..." is matched defensively too. +// +// These lines are emitted at the start of a line and are not valid Kubernetes +// manifests, so they are safe to strip. Top-level manifest keys are +// apiVersion/kind/metadata/spec and never "Pulled", "Digest" or "Pulling"; +// any homonymous keys inside a manifest are indented and therefore not matched. +// +// See https://github.com/databus23/helm-diff/issues/1040 +var ociPullProgressRE = regexp.MustCompile(`(?m)^(?:Pulled|Digest|Pulling):[^\n]*\n?`) + +// stripOCIPullProgress removes Helm's OCI chart pull progress output that leaks +// into the rendered manifest buffer when the chart (or a subchart) is pulled +// from an OCI registry. +// +// Without this, the progress lines (e.g. "Pulled: ...", "Digest: ...") are +// parsed as a YAML document lacking a Kind and break the downstream three-way +// merge / kubeclient.Build(): +// +// unable to decode "": Object 'Kind' is missing in '{"Digest":"...","Pulled":"..."}' +// +// See https://github.com/databus23/helm-diff/issues/1040 +func stripOCIPullProgress(s []byte) []byte { + return ociPullProgressRE.ReplaceAll(s, []byte("")) +} + // serverSideFlags returns the --server-side flag(s) to forward to helm. // // The flag is Helm v4 only: diff --git a/cmd/helm_test.go b/cmd/helm_test.go index cddf73dd..f88e46ea 100644 --- a/cmd/helm_test.go +++ b/cmd/helm_test.go @@ -8,6 +8,78 @@ import ( "github.com/google/go-cmp/cmp" ) +func TestStripOCIPullProgress(t *testing.T) { + manifest := `--- +# Source: karpenter/templates/cm.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: karpenter + namespace: karpenter +data: + Pulled: preserved + Digest: preserved +--- +# Source: karpenter/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: karpenter + namespace: karpenter +` + + cases := []struct { + name string + in string + want string + }{ + { + name: "strips OCI pull progress prepended to manifests", + in: "Pulled: public.ecr.aws/karpenter/karpenter:1.9.0\n" + + "Digest: sha256:8e3952caafd208cb888fbf97467cd04a4a024a3fba64c84af73039040cc6a371\n" + + manifest, + want: manifest, + }, + { + name: "strips Pulling/Pulled/Digest progress lines", + in: "Pulling: public.ecr.aws/karpenter/karpenter:1.9.0\n" + + "Pulled: public.ecr.aws/karpenter/karpenter:1.9.0\n" + + "Digest: sha256:abc123\n" + + manifest, + want: manifest, + }, + { + name: "does not modify output without OCI progress", + in: manifest, + want: manifest, + }, + { + name: "preserves indented Pulled/Digest keys inside manifests", + in: manifest, + want: manifest, + }, + { + name: "strips OCI progress even with no trailing manifests", + in: "Pulled: registry.example.com/chart:1.0.0\nDigest: sha256:deadbeef\n", + want: "", + }, + { + name: "returns empty for empty input", + in: "", + want: "", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := stripOCIPullProgress([]byte(tc.in)) + if d := cmp.Diff(tc.want, string(got)); d != "" { + t.Errorf("unexpected diff: %s", d) + } + }) + } +} + type dryRunFlagsConfig struct { isHelmV4 bool supportsDryRunLookup bool diff --git a/cmd/local.go b/cmd/local.go index 43a8a73e..942aacc2 100644 --- a/cmd/local.go +++ b/cmd/local.go @@ -250,5 +250,6 @@ func (l *local) renderChart(chartPath string) ([]byte, error) { helmBin = "helm" } cmd := exec.Command(helmBin, args...) - return outputWithRichError(cmd) + out, err := outputWithRichError(cmd) + return stripOCIPullProgress(out), err }