diff --git a/internal/storage/driver/cfgmaps.go b/internal/storage/driver/cfgmaps.go index d4886361f..73076fdfe 100644 --- a/internal/storage/driver/cfgmaps.go +++ b/internal/storage/driver/cfgmaps.go @@ -255,6 +255,7 @@ func (cfgmaps *ConfigMaps) Delete(key string) (rls release.Releaser, err error) // "status" - status of the release (see pkg/release/status.go for variants) // "owner" - owner of the configmap, currently "helm". // "name" - name of the release. +// "helm.sh/release-version" - release object schema version, see releaseVersionLabel. func newConfigMapsObject(key string, rls *rspb.Release, lbs labels) (*v1.ConfigMap, error) { const owner = "helm" @@ -276,6 +277,7 @@ func newConfigMapsObject(key string, rls *rspb.Release, lbs labels) (*v1.ConfigM lbs.set("owner", owner) lbs.set("status", rls.Info.Status.String()) lbs.set("version", strconv.Itoa(rls.Version)) + lbs.set(releaseVersionLabel, releaseVersion) // create and return configmap object return &v1.ConfigMap{ diff --git a/internal/storage/driver/cfgmaps_test.go b/internal/storage/driver/cfgmaps_test.go index ddec42965..5b97e8213 100644 --- a/internal/storage/driver/cfgmaps_test.go +++ b/internal/storage/driver/cfgmaps_test.go @@ -72,6 +72,24 @@ func TestUncompressedConfigMapGet(t *testing.T) { assert.Equalf(t, rel, got, "Expected {%v}, got {%v}", rel, got) } +func TestConfigMapReleaseVersionLabel(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) + + cfgmap, err := newConfigMapsObject(key, rel, nil) + require.NoError(t, err, "Failed to create configmap") + + // ConfigMaps have no Type field, so the release schema version is a label. + assert.Equal(t, releaseVersion, cfgmap.Labels[releaseVersionLabel]) + + // The label is a system label and must not leak into the release's own labels. + assert.True(t, isSystemLabel(releaseVersionLabel)) + assert.NotContains(t, filterSystemLabels(cfgmap.Labels), releaseVersionLabel) +} + func convertReleaserToV1(t *testing.T, rel release.Releaser) *rspb.Release { t.Helper() switch r := rel.(type) { diff --git a/internal/storage/driver/util.go b/internal/storage/driver/util.go index c15f9d746..10cf1adf1 100644 --- a/internal/storage/driver/util.go +++ b/internal/storage/driver/util.go @@ -31,7 +31,17 @@ var b64 = base64.StdEncoding var magicGzip = []byte{0x1f, 0x8b, 0x08} -var systemLabels = []string{"name", "owner", "status", "version", "createdAt", "modifiedAt"} +// releaseVersionLabel carries the release object schema version on storage +// objects that have no native field for it. Secrets record it in their Type +// field, but ConfigMaps have no equivalent, so the marker is a label instead. +// Absence of the label means release v1, so records written before this label +// existed continue to read correctly. +const releaseVersionLabel = "helm.sh/release-version" + +// releaseVersion is the schema version written by this package. +const releaseVersion = "v2" + +var systemLabels = []string{"name", "owner", "status", "version", "createdAt", "modifiedAt", releaseVersionLabel} // encodeRelease encodes a release returning a base64 encoded // gzipped string representation, or error. diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 1d7fb9103..04e863e2c 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -30,11 +30,23 @@ import ( "helm.sh/helm/v4/pkg/storage/driver" ) -// HelmStorageType is the type field of the Kubernetes storage object which stores the Helm release -// version. It is modified slightly replacing the '/': sh.helm/release.v1 -// Note: The version 'v1' is incremented if the release object metadata is -// modified between major releases. -// This constant is used as a prefix for the Kubernetes storage object name. +// HelmStorageType is the prefix used for the name of the Kubernetes storage object +// that holds a release. It is derived from that object's 'Type' field +// (helm.sh/release.v1) by reversing the domain and replacing the '/' with a '.'. +// +// The prefix is deliberately decoupled from the 'Type' field +// (helm.sh/release.v1, helm.sh/release.v2). Type records the schema +// of the encoded release body, so it tracks the release object version. This +// prefix only keeps release names unique, and was added to stop Helm 3 records +// colliding with Helm 2 ones (https://github.com/helm/helm/issues/6435). +// +// The prefix is therefore not incremented with the release object version. +// Kubernetes objects cannot be renamed in place, so doing so would mean +// recreating every record and deleting the old one, per revision, per namespace. +// Until that completed, Get, Update and Delete would all miss, since they address +// objects by exact key, while List, Query and History would keep returning the old +// records because they select on labels. The only thing gained is seeing the +// release object version in the object name, which Type already records. const HelmStorageType = "sh.helm.release.v1" // Storage represents a storage engine for a Release.